diff --git a/shared/chat/conversation/header-area/index.tsx b/shared/chat/conversation/header-area/index.tsx
index bcb42c00574c..bf2c775acacd 100644
--- a/shared/chat/conversation/header-area/index.tsx
+++ b/shared/chat/conversation/header-area/index.tsx
@@ -51,7 +51,7 @@ const HeaderAreaRight = (props: HeaderConversationProps) => {
noShrink={true}
style={Kb.Styles.collapseStyles([styles.headerRight, {opacity: pendingWaiting ? 0 : 1}])}
>
-
+
)
diff --git a/shared/chat/conversation/list-area/index.tsx b/shared/chat/conversation/list-area/index.tsx
index 0ec316422864..7c066639942f 100644
--- a/shared/chat/conversation/list-area/index.tsx
+++ b/shared/chat/conversation/list-area/index.tsx
@@ -1,7 +1,7 @@
import * as C from '@/constants'
import * as Kb from '@/common-adapters'
import * as React from 'react'
-import * as T from '@/constants/types'
+import type * as T from '@/constants/types'
import * as TestIDs from '@/tests/e2e/shared/test-ids'
import Separator from '../messages/separator'
import SpecialBottomMessage from '../messages/special-bottom-message'
@@ -30,21 +30,27 @@ import {copyToClipboard} from '@/util/storeless-actions'
import noop from 'lodash/noop'
import {LegendList} from '@legendapp/list/react'
import type {LegendListRef} from '@/common-adapters'
-import {FlatList} from 'react-native'
-import type {ScrollViewProps} from 'react-native'
import {mobileTypingContainerHeight} from '../input-area/normal/typing'
import {
- KeyboardChatScrollView,
- useKeyboardState,
- useReanimatedKeyboardAnimation,
-} from 'react-native-keyboard-controller'
-import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'
+ KeyboardAwareLegendList,
+ useKeyboardScrollToEnd,
+} from '@legendapp/list/keyboard'
+import {useReanimatedKeyboardAnimation} from 'react-native-keyboard-controller'
+import Animated, {interpolate, useAnimatedReaction, useAnimatedStyle} from 'react-native-reanimated'
+import {scheduleOnRN} from 'react-native-worklets'
import {ThreadSearchOverlayContext} from '../thread-search-overlay-context'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
type ItemType = T.Chat.Ordinal
const noOrdinals: ReadonlyArray = []
+// Stable config so it doesn't churn props each render. Empty = enable adaptive render with defaults.
+const adaptiveRenderConfig = {}
+
+// Stable MVCP config (anchor visible rows across data prepends). Referenced by native; desktop
+// inlines an equivalent.
+const mvcpData = {data: true} as const
+
const keyExtractor = (ordinal: ItemType) => String(ordinal)
// Item type for list recycling pool separation. A message that leads its author group renders an
@@ -138,8 +144,6 @@ const usePagination = (p: {
return {onEndReached, onStartReached}
}
-const centerTolerancePx = 8
-
// When a centeredOrdinal is set at mount, start there; otherwise start at the end (newest).
const useInitialScrollIndex = (
messageOrdinals: ReadonlyArray,
@@ -210,6 +214,44 @@ const HighlightableRow = React.memo(({ordinal}: {ordinal: T.Chat.Ordinal}) => {
})
HighlightableRow.displayName = 'HighlightableRow'
+// Sending the list to the centered ordinal: a search hit, a reply-quote jump, a pinned message.
+//
+// Centring on the raw ordinal change is unreliable: navigating to a hit reloads the thread centred
+// on it, so the target is briefly absent from messageOrdinals when the ordinal changes. Wait for it
+// to arrive, then scroll once per target and no more. Re-issuing when the target's index moves looks
+// reasonable - a prepend does shift it - but scrolling is what triggers that prepend, so it would
+// re-centre the list out from under someone reading around the hit. The list holds the target in
+// place while rows measure, and maintainVisibleContentPosition holds it across prepends.
+//
+// Reset per dataset rather than per conversation: re-centring on the ordinal already stored still
+// clears and reloads the thread, so the list has to be sent to it again.
+const useScrollToCentered = (p: {
+ centeredOrdinal: T.Chat.Ordinal | undefined
+ datasetKey: string
+ listRef: React.RefObject
+ messageOrdinals: ReadonlyArray
+ ready: boolean
+}) => {
+ const {centeredOrdinal, datasetKey, listRef, messageOrdinals, ready} = p
+ const lastScrolledRef = React.useRef(undefined)
+ React.useLayoutEffect(() => {
+ lastScrolledRef.current = undefined
+ }, [datasetKey])
+
+ React.useEffect(() => {
+ if (!ready || centeredOrdinal === undefined) {
+ lastScrolledRef.current = undefined
+ return
+ }
+ if (lastScrolledRef.current === centeredOrdinal) return
+ if (sortedIndexOf(messageOrdinals as unknown as number[], centeredOrdinal as unknown as number) < 0) {
+ return
+ }
+ lastScrolledRef.current = centeredOrdinal
+ void listRef.current?.scrollToItem({animated: false, item: centeredOrdinal, viewPosition: 0.5})
+ }, [centeredOrdinal, datasetKey, listRef, messageOrdinals, ready])
+}
+
const DesktopThreadWrapper = function DesktopThreadWrapper() {
const desktopStyles = useDesktopStyles()
const editingOrdinal = InputState.useConversationInput(s => s.editing)
@@ -219,12 +261,9 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() {
const {clearVersion, containsLatestMessage, messageOrdinals, loaded} = data
// Centered loads (search hit, reply-quote jump, pinned message) clear the thread before
- // refetching, so the list sees a non-empty -> empty -> non-empty transition. dataKey is the
- // library's answer to that, but it cannot be used here: the fresh-data reset drops
- // readyToRender and restores it inside one layout-effect pass, so the containers never render
- // in the not-ready state that useFreshDataTransitionVisibility waits for before clearing its
- // pending flag. The wrapper then stays at opacity 0 with the thread fully measured behind it.
- // Remounting sidesteps it because a fresh mount seeds that flag from the current epoch.
+ // refetching, so the list sees a non-empty -> empty -> non-empty transition it cannot recover
+ // from on its own. dataKey tells it the data is a new dataset, which is what makes it reset
+ // rather than wait for a container layout that never comes.
const datasetKey = `${conversationIDKey}:${clearVersion}`
const listRef = React.useRef(null)
@@ -303,111 +342,7 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() {
[onScroll]
)
- // Scroll to centered ordinal when it changes (search / thread navigation).
- // Use a "last scrolled to" ref rather than a "did it change" ref so we still
- // scroll when loaded becomes true after centeredOrdinal was already set.
- // Reset per dataset, not per conversation: re-centering on the ordinal we are already parked
- // on still reloads the thread, so the list has to scroll to it again.
- const lastScrolledCenteredRef = React.useRef(undefined)
- React.useLayoutEffect(() => {
- lastScrolledCenteredRef.current = undefined
- }, [datasetKey])
-
- // Owns the in-flight centering loop. It has to outlive re-renders: the messages that make
- // centering accurate arrive after it starts, so the loop must not be torn down by an effect
- // cleanup when messageOrdinals changes. Only a new target or unmount stops it.
- const centerLoopRef = React.useRef<{cancelled: boolean} | undefined>(undefined)
- // The loop re-centers for up to ~3s; a user scrolling in that window must win.
- const abortCentering = React.useCallback(() => {
- if (centerLoopRef.current) centerLoopRef.current.cancelled = true
- }, [])
- React.useEffect(() => abortCentering, [abortCentering])
-
- // Closed loop, not one shot: rows enter at estimatedItemSize and only settle as they measure, so
- // the first scroll lands off by however wrong the estimates above the target were. Measure the
- // row's real offset from the viewport center and correct until it holds still, then get out of
- // the way: maintainVisibleContentPosition owns the offset from then on. Two controllers fighting
- // over the same scroll offset would oscillate.
- //
- // Correct via LegendList's own scrollToOffset, never scrollIntoView: touching scrollTop directly
- // desyncs LegendList's internal scroll state, and the next time it recomputes item positions it
- // snaps somewhere unrelated.
- const scrollToCentered = React.useEffectEvent((target: T.Chat.Ordinal) => {
- abortCentering()
- const loop = {cancelled: false}
- centerLoopRef.current = loop
- const run = async () => {
- let settled = 0
- let pinnedChecks = 0
- let scrollAtLastRequest: number | undefined
- for (let elapsed = 0; elapsed < 3000 && !loop.cancelled; ) {
- const wrapper = wrapperRef.current as unknown as {
- getBoundingClientRect: () => {height: number; top: number}
- querySelector: (s: string) => {getBoundingClientRect: () => {height: number; top: number}} | null
- } | null
- const el = wrapper ? wrapper.querySelector(`[data-ordinal="${target}"]`) : null
- if (!wrapper || !el) {
- // Target is outside the rendered window; get it mounted first.
- const idx = sortedIndexOf(
- messageOrdinalsRef.current as unknown as number[],
- target as unknown as number
- )
- if (idx >= 0) {
- void listRef.current?.scrollToIndex({animated: false, index: idx, viewPosition: 0.5})
- }
- settled = 0
- pinnedChecks = 0
- await new Promise(resolve => setTimeout(resolve, 100))
- elapsed += 100
- continue
- }
- const elRect = el.getBoundingClientRect()
- const wrapRect = wrapper.getBoundingClientRect()
- const offBy = elRect.top + elRect.height / 2 - (wrapRect.top + wrapRect.height / 2)
- const scroll = listRef.current?.getState().scroll
- // Deadband, not exact centering: below this the row reads as centered, and chasing the
- // remainder only fights maintainVisibleContentPosition's own sub-pixel adjustments.
- if (Math.abs(offBy) <= centerTolerancePx || scroll === undefined) {
- pinnedChecks = 0
- // Only the iteration right after a correction can diagnose a clamp.
- scrollAtLastRequest = undefined
- if (++settled >= 3) return
- } else if (scroll === scrollAtLastRequest) {
- // A hit near either end of the thread cannot be centered: the offset we ask for gets
- // clamped and the row never reaches the middle. Our last correction moved the scroll
- // position not at all, so we are pinned against an edge — stop rather than spin.
- if (++pinnedChecks >= 3) return
- } else {
- pinnedChecks = 0
- scrollAtLastRequest = scroll
- void listRef.current?.scrollToOffset({animated: false, offset: scroll + offBy})
- }
- await new Promise(resolve => setTimeout(resolve, 50))
- elapsed += 50
- }
- }
- void run()
- })
-
- React.useEffect(() => {
- if (!loaded) return
- if (centeredOrdinal !== undefined) {
- if (lastScrolledCenteredRef.current === centeredOrdinal) return
- const idx = sortedIndexOf(
- messageOrdinalsRef.current as unknown as number[],
- centeredOrdinal as unknown as number
- )
- if (idx < 0) return
- lastScrolledCenteredRef.current = centeredOrdinal
- scrollToCentered(centeredOrdinal)
- } else if (lastScrolledCenteredRef.current !== undefined) {
- lastScrolledCenteredRef.current = undefined
- abortCentering()
- if (containsLatestMessage) {
- void listRef.current?.scrollToEnd({animated: false})
- }
- }
- }, [abortCentering, centeredOrdinal, loaded, containsLatestMessage, messageOrdinals])
+ useScrollToCentered({centeredOrdinal, datasetKey, listRef, messageOrdinals, ready: loaded})
// Scroll to the message being edited
const lastEditingOrdinalRef = React.useRef(undefined)
@@ -511,12 +446,6 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() {
const initialScrollIndex = useInitialScrollIndex(messageOrdinals, centeredOrdinal)
- // A wheel means the user took over: stop centering so we don't scroll them away from where
- // they landed.
- const onWheel = React.useCallback(() => {
- abortCentering()
- }, [abortCentering])
-
return (
}
data={messageOrdinals as unknown as T.Chat.Ordinal[]}
renderItem={renderItem}
@@ -543,10 +471,13 @@ const DesktopThreadWrapper = function DesktopThreadWrapper() {
style={Kb.Styles.castStyleDesktop(desktopStyles.list)}
initialScrollAtEnd={initialScrollIndex === undefined}
initialScrollIndex={initialScrollIndex}
+ // headerLayout because SpecialTopMessage measures larger than its estimate a frame after
+ // the list has already scrolled to the end, which pushes every message down by the
+ // difference and leaves the newest one off screen.
maintainScrollAtEnd={
centeredOrdinal !== undefined
? false
- : {on: {dataChange: true, footerLayout: true, itemLayout: true}}
+ : {on: {dataChange: true, footerLayout: true, headerLayout: true, itemLayout: true}}
}
// Stays on while centered: the full thread response lands after the cached one and
// re-measures rows above the target, which slides it out of view unless anchored.
@@ -594,174 +525,76 @@ const DesktopThreadWrapperWithProfiler = () => (
// ==================== NATIVE ====================
-type RNFlatListRef = {
- scrollToOffset: (opts: {animated: boolean; offset: number}) => void
- scrollToItem: (opts: {animated: boolean; item: unknown; viewPosition?: number}) => void
-}
-
-const useInvertedMessageOrdinals = (messageOrdinals?: ReadonlyArray) => {
- const source = messageOrdinals ?? noOrdinals
- return React.useMemo(() => (source.length > 1 ? [...source].reverse() : source), [source])
-}
-
const useNativeScrolling = (p: {
- centeredOrdinal: T.Chat.Ordinal
- messageOrdinals: ReadonlyArray
- listRef: React.RefObject
+ scrollMessageToEnd: (o: {animated: boolean; closeKeyboard: boolean}) => Promise
}) => {
- const {listRef, centeredOrdinal, messageOrdinals} = p
- const numOrdinals = messageOrdinals.length
- const loadOlderMessages = useConversationThreadLoadOlderMessagesDueToScroll()
- const getThreadLoadStatusOptions = useThreadLoadStatusOptionsGetter()
+ const {scrollMessageToEnd} = p
- // KeyboardChatScrollView sets contentInset.top = K - insets.bottom and
- // contentOffset.y = -(K - insets.bottom) when keyboard is open. Scrolling to
- // offset=0 would place content K-insets.bottom pixels lower (behind the keyboard).
- // We compute the correct resting offset: keyboardHeight.value (negative) + insets.bottom.
- // When keyboard is closed keyboardHeight.value = 0 so the result is clamped to 0.
- const {height: keyboardAnimHeight} = useReanimatedKeyboardAnimation()
- const {bottom: insetsBottom} = useSafeAreaInsets()
+ // scrollMessageToEnd freezes the keyboard-aware scroll view, scrolls to the end,
+ // then unfreezes — so the newest message stays pinned above the input bar even
+ // while the keyboard is open.
const scrollToBottom = React.useCallback(() => {
- const offset = Math.min(keyboardAnimHeight.value + insetsBottom, 0)
- listRef.current?.scrollToOffset({animated: false, offset})
- }, [insetsBottom, keyboardAnimHeight, listRef])
+ void scrollMessageToEnd({animated: false, closeKeyboard: false})
+ }, [scrollMessageToEnd])
const {setScrollRef} = React.useContext(ThreadRefsContext)
React.useEffect(() => {
setScrollRef({scrollDown: noop, scrollToBottom, scrollUp: noop})
}, [setScrollRef, scrollToBottom])
- // only scroll to center once per
- const lastScrollToCentered = React.useRef(-1)
- React.useEffect(() => {
- if (T.Chat.ordinalToNumber(centeredOrdinal) < 0) {
- lastScrollToCentered.current = -1
- }
- }, [centeredOrdinal])
-
- const centeredOrdinalRef = React.useRef(centeredOrdinal)
- // reset per centered target so each new search hit gets a fresh batch of retries
- const scrollFailRetryRef = React.useRef(0)
- React.useEffect(() => {
- centeredOrdinalRef.current = centeredOrdinal
- scrollFailRetryRef.current = 0
- }, [centeredOrdinal])
- const [scrollToCentered] = React.useState(() => () => {
- const co = centeredOrdinalRef.current
- if (lastScrollToCentered.current === co) {
- return
- }
- lastScrollToCentered.current = co
- // coarse: scrollToItem lands at the wrong offset for tall variable-height rows,
- // but it gets the target area rendered. The closed-loop corrector in the
- // component refines from there using the real viewable index range.
- const reassert = (delay: number) =>
- setTimeout(() => {
- const list = listRef.current
- const cur = centeredOrdinalRef.current
- if (!list || cur !== co || T.Chat.ordinalToNumber(cur) <= 0) {
- return
- }
- list.scrollToItem({animated: false, item: cur, viewPosition: 0.5})
- }, delay)
- ;[50, 250].forEach(reassert)
- })
-
- // The centered hit may be outside the rendered window, so scrollToItem fails
- // silently. Wait for more rows to render and retry centering (capped) until it lands.
- const [onScrollToIndexFailed] = React.useState(() => () => {
- if (scrollFailRetryRef.current > 5) {
- return
- }
- scrollFailRetryRef.current += 1
- setTimeout(() => {
- const co = centeredOrdinalRef.current
- if (T.Chat.ordinalToNumber(co) > 0) {
- listRef.current?.scrollToItem({animated: false, item: co, viewPosition: 0.5})
- }
- }, 200)
- })
-
- const onEndReached = () => {
- loadOlderMessages(numOrdinals, getThreadLoadStatusOptions())
- }
-
return {
- onEndReached,
- onScrollToIndexFailed,
scrollToBottom,
- scrollToCentered,
}
}
-// The maintainVisibleContentPosition prop must ALWAYS be set (never toggled to undefined):
-// RN Fabric only re-snapshots the MVP anchor while the prop is set, so an unset->set
-// transition adjusts contentOffset against a stale anchor frame from before the prop was
-// unset — a spurious jump + autoscroll animation of the whole list (seen after dismissing
-// the keyboard following a send). Instead we swap between two configs:
-// - closed (keyboard hidden): autoscrollToTopThreshold=1 so new messages at the bottom
-// auto-reveal when the user is pinned there.
-// - noAutoscroll (keyboard open, or centered on a search hit, or empty list): MVP still
-// anchors content, but autoscroll-to-top is off because:
-// 1. with the keyboard open contentOffset.y = -(K-insets.bottom) <= 1, so the threshold
-// would fire on insert and scroll to y=0, hiding new messages behind the keyboard.
-// 2. while centered on a search hit, autoscroll yanks the centered row.
-// With the keyboard open, MVP's insert adjustment briefly holds old content in place;
-// the deferred scrollToBottom layout effect below re-pins the newest message.
-const maintainVisibleContentPositionClosed = {
- autoscrollToTopThreshold: 1,
- minIndexForVisible: 0,
-}
-const maintainVisibleContentPositionNoAutoscroll = {
- minIndexForVisible: 0,
-}
+// Reads the centered highlight itself (like desktop's HighlightableRow) so renderItem stays
+// referentially stable — a renderItem identity change re-renders every visible row at once.
+const NativeRow = React.memo(function NativeRow({ordinal}: {ordinal: T.Chat.Ordinal}) {
+ const {centeredHighlightOrdinal} = useConversationCenter()
+ return (
+ <>
+
+
+ >
+ )
+})
+
+const nativeRenderItem = ({item: ordinal}: {item: T.Chat.Ordinal}) =>
const NativeConversationList = function NativeConversationList() {
const nativeStyles = useNativeStyles()
- const List = FlatList as unknown as React.ComponentType<
- Record & {ref?: React.Ref}
- >
-
const conversationIDKey = useConversationThreadID()
- const listData = useConversationThreadSelector(
- C.useShallow(s => ({
- loaded: s.loaded,
- messageOrdinals: s.messageOrdinals,
- }))
- )
- const {centeredHighlightOrdinal, centeredOrdinal} = useConversationCenter()
- const noCenteredOrdinal = T.Chat.numberToOrdinal(-1)
- const centeredOrdinalOrNone = centeredOrdinal ?? noCenteredOrdinal
- const centeredHighlightOrdinalOrNone = centeredHighlightOrdinal ?? noCenteredOrdinal
- const {loaded} = listData
-
- const messageOrdinals = useInvertedMessageOrdinals(listData.messageOrdinals)
+ const listData = useThreadListData()
+ const {centeredOrdinal} = useConversationCenter()
+ const {clearVersion, loaded, containsLatestMessage, messageOrdinals} = listData
+ // Same reason as desktop: a centered load empties the thread before refilling it, and the list
+ // needs to be told that is a new dataset rather than left waiting on layout for rows it already
+ // threw away.
+ const datasetKey = `${conversationIDKey}:${clearVersion}`
+ const hasCentered = centeredOrdinal !== undefined
- const listRef = React.useRef(null)
+ const listRef = React.useRef(null)
const markInitiallyLoadedThreadAsRead = useConversationThreadMarkThreadAsRead()
- const keyExtractor = (ordinal: ItemType) => {
- return String(ordinal)
- }
-
- const renderItem = (info?: {item?: ItemType}) => {
- const ordinal = info?.item
- if (!ordinal) {
- return null
- }
- return
- }
-
- const numOrdinals = messageOrdinals.length
-
const getItemType = useGetItemType()
const insets = useSafeAreaInsets()
- const isKeyboardVisible = useKeyboardState((s: {isVisible: boolean}) => s.isVisible)
- // While the thread-search bar is open it overlays the bottom of the list. Reserve
- // that height as extra content padding so centered/newest messages clear it.
+ // While the thread-search bar is open it overlays the bottom of the list. Reserve that height
+ // as extra content padding (so the newest message clears it) and lift the jump-to-recent button
+ // above both the keyboard and the bar. searchOverlayHeight is a reanimated SharedValue set by
+ // the search bar's onLayout; mirror it to state for the (static) content padding.
const searchOverlayHeight = React.useContext(ThreadSearchOverlayContext)
+ const [searchPad, setSearchPad] = React.useState(0)
+ useAnimatedReaction(
+ () => searchOverlayHeight?.value ?? 0,
+ (h, prev) => {
+ if (h !== prev) {
+ scheduleOnRN(setSearchPad, h)
+ }
+ },
+ [searchOverlayHeight]
+ )
const {height: keyboardAnimHeight, progress: keyboardProgress} = useReanimatedKeyboardAnimation()
const insetsBottom = insets.bottom
// The input/search bar lives in a KeyboardStickyView with offset
@@ -777,119 +610,18 @@ const NativeConversationList = function NativeConversationList() {
],
}))
- const {scrollToCentered, scrollToBottom, onEndReached, onScrollToIndexFailed} = useNativeScrolling({
- centeredOrdinal: centeredOrdinalOrNone,
- listRef,
+ const {onStartReached, onEndReached} = usePagination({
+ containsLatestMessage,
messageOrdinals,
})
- // Closed-loop centering corrector. scrollToItem/scrollToIndex lands at the wrong
- // offset here (inverted list + custom keyboard scrollview + tall variable-height
- // image rows), so instead we read the actual viewable index range each frame and
- // scrollToOffset by the item-delta until the target sits at viewport center.
- const scrollOffsetRef = React.useRef(0)
- const contentHeightRef = React.useRef(0)
- const centeredRef = React.useRef(centeredOrdinalOrNone)
- React.useEffect(() => {
- centeredRef.current = centeredOrdinalOrNone
- }, [centeredOrdinalOrNone])
- const ordsRef = React.useRef(messageOrdinals)
- React.useEffect(() => {
- ordsRef.current = messageOrdinals
- }, [messageOrdinals])
- // {active, iters}: correcting toward a centered hit and how many steps taken
- const correctRef = React.useRef({active: false, iters: 0})
- const vFirstRef = React.useRef(undefined)
- const vLastRef = React.useRef(undefined)
- const [correctCenter] = React.useState(
- () => (first: number | null | undefined, last: number | null | undefined) => {
- const st = correctRef.current
- if (!st.active) return
- const co = centeredRef.current
- const ords = ordsRef.current
- const num = ords.length
- if (co <= 0 || !num || first == null || last == null) return
- const targetIdx = ords.indexOf(co)
- if (targetIdx < 0) return
- const centerIdx = (first + last) / 2
- const diff = targetIdx - centerIdx
- if (Math.abs(diff) <= 0.5 || st.iters > 12) {
- st.active = false
- return
- }
- st.iters += 1
- const avgH = contentHeightRef.current / num
- // damp by 0.9 to avoid overshoot/oscillation; higher index = older = higher offset
- const newOffset = Math.max(0, scrollOffsetRef.current + diff * avgH * 0.9)
- listRef.current?.scrollToOffset({animated: false, offset: newOffset})
- }
- )
- const [onScrollNative] = React.useState(
- () =>
- (e: {nativeEvent: {contentOffset: {y: number}; contentSize: {height: number}}}) => {
- scrollOffsetRef.current = e.nativeEvent.contentOffset.y
- contentHeightRef.current = e.nativeEvent.contentSize.height
- }
- )
- const [onContentSizeChangeNative] = React.useState(() => (_w: number, h: number) => {
- contentHeightRef.current = h
- })
- // user touched the list: stop fighting them
- const [onScrollBeginDrag] = React.useState(() => () => {
- correctRef.current.active = false
- })
+ const {freeze, scrollMessageToEnd} = useKeyboardScrollToEnd({listRef})
+
+ const {scrollToBottom} = useNativeScrolling({scrollMessageToEnd})
const jumpToRecent = useJumpToRecent(scrollToBottom, messageOrdinals.length)
- // When keyboard is open, maintainVisibleContentPosition adjusts contentOffset by the new
- // message height when a message is added, undoing the scrollToBottom from onSubmit.
- // Defer the re-scroll past the native MPV adjustment (which runs on the UI thread after
- // React's commit) so the newest message stays visible.
- const prevNumOrdinalsRef = React.useRef(numOrdinals)
- // Tracks which conversation prevNumOrdinalsRef's baseline belongs to so the
- // baseline resets on a real conversation switch (value compare) rather than on
- // a react-native-screens freeze/thaw, which re-mounts effects.
- const numBaselineConvRef = React.useRef(conversationIDKey)
- const isKeyboardVisibleRef = React.useRef(isKeyboardVisible)
- React.useLayoutEffect(() => {
- isKeyboardVisibleRef.current = isKeyboardVisible
- })
- React.useLayoutEffect(() => {
- const sameConv = numBaselineConvRef.current === conversationIDKey
- numBaselineConvRef.current = conversationIDKey
- const prev = prevNumOrdinalsRef.current
- prevNumOrdinalsRef.current = numOrdinals
- if (sameConv && numOrdinals > prev && isKeyboardVisibleRef.current) {
- const id = setTimeout(() => {
- if (isKeyboardVisibleRef.current) {
- scrollToBottom()
- }
- }, 0)
- return () => clearTimeout(id)
- }
- return undefined
- }, [conversationIDKey, numOrdinals, scrollToBottom])
-
- // Center on the search hit once it actually appears in the loaded list. Centering
- // on the raw centeredOrdinal change is unreliable: navigating to a hit reloads the
- // thread centered on it, so messageOrdinals is briefly empty (idx -1) when the
- // ordinal changes. Wait for the target to load, then scroll (scrollToCentered
- // guards against repeats and re-asserts across frames).
- React.useEffect(() => {
- if (!(centeredOrdinalOrNone > 0 && messageOrdinals.includes(centeredOrdinalOrNone))) {
- return undefined
- }
- // coarse scroll to get the target area rendered, then run the closed-loop
- // corrector which refines via the real viewable index range
- scrollToCentered()
- correctRef.current = {active: true, iters: 0}
- const ids = [50, 250, 500, 900].map(d =>
- setTimeout(() => correctCenter(vFirstRef.current, vLastRef.current), d)
- )
- return () => {
- ids.forEach(clearTimeout)
- }
- }, [centeredOrdinalOrNone, messageOrdinals, scrollToCentered, correctCenter])
+ useScrollToCentered({centeredOrdinal, datasetKey, listRef, messageOrdinals, ready: true})
// These refs store the conversation they last applied to (not a boolean) so a
// freeze/thaw of this screen — which re-mounts effects without a real
@@ -910,99 +642,68 @@ const NativeConversationList = function NativeConversationList() {
markedConvRef.current = conversationIDKey
markInitiallyLoadedThreadAsRead()
}
+ }, [conversationIDKey, loaded, markInitiallyLoadedThreadAsRead])
- if (centeredOrdinalOrNone > 0) {
- scrollToCentered()
- setTimeout(() => {
- scrollToCentered()
- }, 100)
- } else if (numOrdinals > 0) {
- scrollToBottom()
- setTimeout(() => {
- scrollToBottom()
- }, 100)
- }
- }, [
- conversationIDKey,
- centeredOrdinalOrNone,
- loaded,
- markInitiallyLoadedThreadAsRead,
- numOrdinals,
- scrollToBottom,
- scrollToCentered,
- ])
-
- const onViewableItemsChanged = useNativeSafeOnViewableItemsChanged(onEndReached, messageOrdinals.length)
- const [onViewableItemsChangedNative] = React.useState(
- () => (info: {viewableItems: Array<{index: number | null}>}) => {
- onViewableItemsChanged.current(info)
- const first = info.viewableItems.at(0)?.index
- const last = info.viewableItems.at(-1)?.index
- vFirstRef.current = first
- vLastRef.current = last
- correctCenter(first, last)
- }
- )
+ const initialScrollIndex = useInitialScrollIndex(messageOrdinals, centeredOrdinal)
- const renderScrollComponent = React.useCallback(
- (props: ScrollViewProps) => (
-
- ),
- [insets.bottom, searchOverlayHeight]
+ // Reserve bottom space so the newest message clears the sticky input bar, which is pulled up
+ // over the list bottom (KeyboardStickyView offset -insets.bottom) plus the floating typing
+ // indicator. Without this the list scrolls to its content end but the newest row sits behind
+ // the input bar.
+ const listContentStyle = React.useMemo(
+ () => ({paddingBottom: mobileTypingContainerHeight + insets.bottom + searchPad}),
+ [insets.bottom, searchPad]
)
- const mvpAutoscroll = !(centeredOrdinalOrNone > 0 || !numOrdinals || isKeyboardVisible)
-
- const nativeContentContainerStyle = React.useMemo(
- () => ({
- paddingBottom: 0,
- paddingTop: mobileTypingContainerHeight + insets.bottom,
- }),
- [insets.bottom]
- )
+ // The input bar (KeyboardStickyView, closed offset -insets.bottom) overlaps the bottom of the
+ // list by insets.bottom, so without this the scroll indicator runs down behind it. Inset the
+ // indicator by exactly that overlap (NOT the full content padding, which also reserves space for
+ // the floating typing indicator that the scrollbar doesn't need to clear).
+ const scrollIndicatorInsets = React.useMemo(() => ({bottom: insets.bottom}), [insets.bottom])
return (
-
{jumpToRecent && (
@@ -1027,42 +728,4 @@ const useNativeStyles = Kb.Styles.createStyleHook(
}) as const
)
-const minTimeDelta = 1000
-const minDistanceFromEnd = 10
-
-const useNativeSafeOnViewableItemsChanged = (onEndReached: () => void, numOrdinals: number) => {
- const nextCallbackRef = React.useRef(new Date().getTime())
- const onEndReachedRef = React.useRef(onEndReached)
- React.useEffect(() => {
- onEndReachedRef.current = onEndReached
- }, [onEndReached])
- const numOrdinalsRef = React.useRef(numOrdinals)
- React.useEffect(() => {
- numOrdinalsRef.current = numOrdinals
- nextCallbackRef.current = new Date().getTime() + minTimeDelta
- }, [numOrdinals])
-
- // this can't change ever, so we have to use refs to keep in sync
- const onViewableItemsChanged = React.useRef(
- ({viewableItems}: {viewableItems: Array<{index: number | null}>}) => {
- const idx = viewableItems.at(-1)?.index ?? 0
- const lastIdx = numOrdinalsRef.current - 1
- const offset = numOrdinalsRef.current > 50 ? minDistanceFromEnd : 1
- const deltaIdx = idx - lastIdx + offset
- // not far enough from the end
- if (deltaIdx < 0) {
- return
- }
- const t = new Date().getTime()
- const deltaT = t - nextCallbackRef.current
- // enough time elapsed?
- if (deltaT > 0) {
- nextCallbackRef.current = t + minTimeDelta
- onEndReachedRef.current()
- }
- }
- )
- return onViewableItemsChanged
-}
-
export default isMobile ? NativeConversationList : DesktopThreadWrapperWithProfiler
diff --git a/shared/chat/conversation/messages/special-top-message.tsx b/shared/chat/conversation/messages/special-top-message.tsx
index 6b6c4bc021eb..808bd3947e53 100644
--- a/shared/chat/conversation/messages/special-top-message.tsx
+++ b/shared/chat/conversation/messages/special-top-message.tsx
@@ -15,6 +15,7 @@ import {
import {useConversationParticipantsSelector} from '../data-hooks'
import * as FS from '@/constants/fs'
import {useCurrentUserState} from '@/stores/current-user'
+import * as TestIDs from '@/tests/e2e/shared/test-ids'
const ErrorMessage = () => {
const styles = useStyles()
@@ -153,7 +154,13 @@ function SpecialTopMessage() {
}
return (
-
+
{hasLoadedEver && loadMoreType === 'noMoreToLoad' && showRetentionNotice && }
{hasOlderResetConversation && }
diff --git a/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx b/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx
index 0e1f2dee7469..cfcc77593336 100644
--- a/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx
+++ b/shared/chat/conversation/messages/wrapper/long-pressable/index.tsx
@@ -16,6 +16,7 @@ type Props = {
import {useConversationThreadToggleSearch} from '../../../thread-context'
import Swipeable, {type SwipeableMethods} from '@/common-adapters/swipeable-row'
import {ThreadRefsContext} from '@/chat/conversation/normal/context'
+import {useAdaptiveRender} from '@legendapp/list/react-native'
function ReplyIcon({progress}: {progress: Animated.Value}) {
const styles = useStyles()
@@ -28,16 +29,23 @@ function ReplyIcon({progress}: {progress: Animated.Value}) {
}
function LongPressable(props: Props & {ref?: React.Ref}) {
+ if (!isMobile) {
+ return
+ }
+ return
+}
+
+function LongPressableMobile(props: Props & {ref?: React.Ref}) {
const styles = useStyles()
const toggleThreadSearch = useConversationThreadToggleSearch()
const setReplyTo = InputState.useConversationInputDispatch(s => s.setReplyTo)
const ordinal = useOrdinal()
const {focusInput} = React.useContext(ThreadRefsContext)
const swipeRef = React.useRef(null)
-
- if (!isMobile) {
- return
- }
+ // Velocity-driven signal from LegendList: during fast scroll it flips to "light". We keep the
+ // Swipeable mounted (toggling its tree would remount children and flash images) and instead just
+ // disable its pan handlers in light mode, shedding the per-row touch evaluation during the fling.
+ const adaptiveMode = useAdaptiveRender()
const {children, onLongPress, style} = props
@@ -68,6 +76,7 @@ function LongPressable(props: Props & {ref?: React.Ref}) {
return (
diff --git a/shared/chat/conversation/messages/wrapper/sent.native.tsx b/shared/chat/conversation/messages/wrapper/sent.native.tsx
index 4629e2b1f6dc..785adfd9965d 100644
--- a/shared/chat/conversation/messages/wrapper/sent.native.tsx
+++ b/shared/chat/conversation/messages/wrapper/sent.native.tsx
@@ -1,11 +1,11 @@
import type * as React from 'react'
import Animated, {FadeInDown} from 'react-native-reanimated'
-// Slide-up + fade for a message you just sent. The thread list is an inverted
-// FlatList (cells are flipped with scaleY: -1), so FadeInDown renders on screen
-// as sliding up from below. Runs entirely on the UI thread with no re-renders.
-// The entering animation only plays when this Animated.View MOUNTS — callers must
-// key it per message (recycled containers reuse instances).
+// Slide-up + fade for a message you just sent. The thread list (LegendList) is
+// NOT inverted, so FadeInDown (enters from 25px below, sliding up into place)
+// reads as the row rising from the input bar. Runs entirely on the UI thread
+// with no re-renders. The entering animation only plays when this Animated.View
+// MOUNTS — callers must key it per message (recycled containers reuse instances).
export function Sent(p: {children: React.ReactNode}) {
return (
diff --git a/shared/chat/conversation/messages/wrapper/wrapper.tsx b/shared/chat/conversation/messages/wrapper/wrapper.tsx
index 42cb9dd9ef89..a339e2e9108f 100644
--- a/shared/chat/conversation/messages/wrapper/wrapper.tsx
+++ b/shared/chat/conversation/messages/wrapper/wrapper.tsx
@@ -34,6 +34,7 @@ import {emptyParticipantInfo} from '../../data-hooks'
import {useInboxMetadataState} from '@/chat/inbox/metadata'
import type {ConversationInputState} from '../../input-area/input-state'
import {useChatTeamMemberRole} from '../../team-hooks'
+import * as TestIDs from '@/tests/e2e/shared/test-ids'
type AccountsInfoMap = ReadonlyMap
type PaymentStatusMap = ReadonlyMap
@@ -1018,7 +1019,13 @@ export function WrapperMessage(p: WrapperMessageProps) {
const messageContext = {isHighlighted: showCenteredHighlight, ordinal}
const row = (
-
+
{inProgress && }
+ {/* collapsable={false}: Android view flattening would drop this testID'd wrapper and
+ leave the count unreadable to the e2e suite. */}
{hasResults && (
-
+
{noResults ? 'No results' : `${selectedIndex + 1} of ${hits.length}`}
@@ -506,13 +514,28 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps)
return (
-
+
Cancel
-
+ {/* collapsable={false}: keep this testID'd wrapper (and the EditText under it) as a real
+ view on Android, where view flattening would otherwise render it as an empty leaf. */}
+
{inProgress && }
+ {/* collapsable={false}: Android view flattening would drop this testID'd wrapper and
+ leave the count unreadable to the e2e suite. */}
{hasResults && (
-
+
{status === 'done' && numHits === 0 ? 'No results' : `${selectedIndex + 1} of ${numHits}`}
@@ -541,11 +571,13 @@ const ThreadSearchMobileInner = function ThreadSearchMobileInner(p: CommonProps)
color={numHits > 0 ? theme.blue : theme.black_50}
onClick={onUp}
type="iconfont-arrow-up"
+ testID={TestIDs.CHAT_THREAD_SEARCH_PREV}
/>
0 ? theme.blue : theme.black_50}
onClick={onDown}
type="iconfont-arrow-down"
+ testID={TestIDs.CHAT_THREAD_SEARCH_NEXT}
/>
diff --git a/shared/chat/inbox-and-conversation-header.tsx b/shared/chat/inbox-and-conversation-header.tsx
index c37177055a14..4805c487fb35 100644
--- a/shared/chat/inbox-and-conversation-header.tsx
+++ b/shared/chat/inbox-and-conversation-header.tsx
@@ -16,6 +16,7 @@ import {navToPath} from '@/constants/fs'
import {showConversationInfoPanel, toggleConversationThreadSearch} from '@/chat/conversation/thread-context'
import {muteConversation} from '@/chat/conversation/status-actions'
import AccountSwitchHeaderAvatar from '@/router-v2/account-switch-header-avatar'
+import * as TestIDs from '@/tests/e2e/shared/test-ids'
const emptyMeta = Chat.makeConversationMeta()
const emptyParticipantInfo = Chat.uiParticipantsToParticipantInfo([])
@@ -245,7 +246,12 @@ const Header = () => {
direction="vertical"
tooltip={`Search in this chat (${C.shortcutSymbol}F)`}
>
-
+
{
) : null
return (
-
+
5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5683,13 +5683,11 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) {
- return /* @__PURE__ */ React2__namespace.createElement(ContainerSlotBase, { ...props });
- });
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = React2.useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = React2.useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- React2.useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -6402,7 +6400,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
-diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs
-index 5315f44..5776876 100644
---- a/node_modules/@legendapp/list/react-native.mjs
-+++ b/node_modules/@legendapp/list/react-native.mjs
-@@ -4812,7 +4812,7 @@ function applyItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem) {
- if (!needsRecalculate && state.containerItemKeys.has(itemKey)) {
- needsRecalculate = true;
- }
-- if (prevSizeKnown !== void 0 && Math.abs(prevSizeKnown - size) > 5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5662,13 +5662,11 @@ var ContainerSlot = typedMemo(function ContainerSlot2(props) {
- return /* @__PURE__ */ React2.createElement(ContainerSlotBase, { ...props });
- });
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -6381,7 +6379,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
-diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js
-index ea03488..e70e58c 100644
---- a/node_modules/@legendapp/list/react-native.web.js
-+++ b/node_modules/@legendapp/list/react-native.web.js
-@@ -4850,7 +4850,7 @@ function applyItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem) {
- if (!needsRecalculate && state.containerItemKeys.has(itemKey)) {
- needsRecalculate = true;
- }
-- if (prevSizeKnown !== void 0 && Math.abs(prevSizeKnown - size) > 5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5750,13 +5750,11 @@ function useDOMOrder(ref) {
- }, [ctx]);
- }
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = React3.useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = React3.useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- React3.useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -7054,7 +7052,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
-diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs
-index c96345b..e93d72d 100644
---- a/node_modules/@legendapp/list/react-native.web.mjs
-+++ b/node_modules/@legendapp/list/react-native.web.mjs
-@@ -4829,7 +4829,7 @@ function applyItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem) {
- if (!needsRecalculate && state.containerItemKeys.has(itemKey)) {
- needsRecalculate = true;
- }
-- if (prevSizeKnown !== void 0 && Math.abs(prevSizeKnown - size) > 5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5729,13 +5729,11 @@ function useDOMOrder(ref) {
- }, [ctx]);
- }
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -7033,7 +7031,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
-diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js
-index ea03488..e70e58c 100644
---- a/node_modules/@legendapp/list/react.js
-+++ b/node_modules/@legendapp/list/react.js
-@@ -4850,7 +4850,7 @@ function applyItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem) {
- if (!needsRecalculate && state.containerItemKeys.has(itemKey)) {
- needsRecalculate = true;
- }
-- if (prevSizeKnown !== void 0 && Math.abs(prevSizeKnown - size) > 5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5750,13 +5750,11 @@ function useDOMOrder(ref) {
- }, [ctx]);
- }
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = React3.useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = React3.useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- React3.useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -7054,7 +7052,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
-diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs
-index c96345b..e93d72d 100644
---- a/node_modules/@legendapp/list/react.mjs
-+++ b/node_modules/@legendapp/list/react.mjs
-@@ -4829,7 +4829,7 @@ function applyItemSize(ctx, itemKey, sizeObj, resolvedMeasurementItem) {
- if (!needsRecalculate && state.containerItemKeys.has(itemKey)) {
- needsRecalculate = true;
- }
-- if (prevSizeKnown !== void 0 && Math.abs(prevSizeKnown - size) > 5) {
-+ if (prevSizeKnown === void 0 || Math.abs(prevSizeKnown - size) > 5) {
- shouldMaintainScrollAtEnd = true;
- }
- onItemSizeChanged == null ? void 0 : onItemSizeChanged({
-@@ -5729,13 +5729,11 @@ function useDOMOrder(ref) {
- }, [ctx]);
- }
- function useFreshDataTransitionVisibility(readyToRender, transitionEpoch) {
-- const completedTransitionEpoch = useRef(transitionEpoch);
-- const isTransitionPending = completedTransitionEpoch.current !== transitionEpoch;
-+ const [completedTransitionEpoch, setCompletedTransitionEpoch] = useState(transitionEpoch);
-+ const isTransitionPending = completedTransitionEpoch !== transitionEpoch;
- useLayoutEffect(() => {
-- if (!readyToRender) {
-- completedTransitionEpoch.current = transitionEpoch;
-- }
-- }, [readyToRender, transitionEpoch]);
-+ setCompletedTransitionEpoch(transitionEpoch);
-+ }, [transitionEpoch]);
- return readyToRender && !isTransitionPending;
- }
-
-@@ -7033,7 +7031,8 @@ var ScheduledWork = class {
- const work = this.work.get(key);
- if (work) {
- this.work.delete(key);
-- work[1](work[0]);
-+ const [handle, cancelWork] = work;
-+ cancelWork(handle);
- }
- }
- has(key) {
diff --git a/shared/patches/@legendapp+list+3.3.5.patch b/shared/patches/@legendapp+list+3.3.5.patch
new file mode 100644
index 000000000000..3c66b008d707
--- /dev/null
+++ b/shared/patches/@legendapp+list+3.3.5.patch
@@ -0,0 +1,10034 @@
+diff --git a/node_modules/@legendapp/list/.DS_Store b/node_modules/@legendapp/list/.DS_Store
+deleted file mode 100644
+index b86e710..0000000
+Binary files a/node_modules/@legendapp/list/.DS_Store and /dev/null differ
+diff --git a/node_modules/@legendapp/list/animated.d.ts b/node_modules/@legendapp/list/animated.d.ts
+index 14beb98..56eefce 100644
+--- a/node_modules/@legendapp/list/animated.d.ts
++++ b/node_modules/@legendapp/list/animated.d.ts
+@@ -488,6 +488,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
+diff --git a/node_modules/@legendapp/list/react-native.d.ts b/node_modules/@legendapp/list/react-native.d.ts
+index ce1fe00..9a4311c 100644
+--- a/node_modules/@legendapp/list/react-native.d.ts
++++ b/node_modules/@legendapp/list/react-native.d.ts
+@@ -488,6 +488,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
+diff --git a/node_modules/@legendapp/list/react-native.js b/node_modules/@legendapp/list/react-native.js
+index b3c5a30..9d7ad6d 100644
+--- a/node_modules/@legendapp/list/react-native.js
++++ b/node_modules/@legendapp/list/react-native.js
+@@ -457,611 +457,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-
+-// src/core/adaptiveRender.ts
+-var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3;
+-var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1;
+-var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const isWeb = Platform.OS === "web";
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1265,58 +660,279 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1357,6 +973,124 @@ function finishScrollTo(ctx) {
+ }
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++
+ // src/core/checkFinishedScroll.ts
+ var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20;
+ var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1;
+@@ -1549,6 +1283,69 @@ function doScrollTo(ctx, params) {
+ }
+ }
+
++// src/core/adaptiveRender.ts
++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3;
++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1;
++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const isWeb = Platform.OS === "web";
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1575,9 +1372,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1972,11 +1772,32 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
+-// src/platform/flushSync.native.ts
+-var flushSync = (fn) => {
+- fn();
+-};
+-
++// src/platform/flushSync.native.ts
++var flushSync = (fn) => {
++ fn();
++};
++
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2224,7 +2045,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2246,6 +2067,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2256,6 +2078,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2263,7 +2094,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2276,6 +2107,310 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4320,6 +4455,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4337,8 +4473,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -5831,6 +5977,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -5840,6 +5987,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -6926,13 +7075,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -7652,6 +7802,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs
+index 40e87cd..8e6f6d0 100644
+--- a/node_modules/@legendapp/list/react-native.mjs
++++ b/node_modules/@legendapp/list/react-native.mjs
+@@ -436,611 +436,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-
+-// src/core/adaptiveRender.ts
+-var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3;
+-var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1;
+-var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const isWeb = Platform.OS === "web";
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1244,58 +639,279 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android" || state.lastLayout)) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1336,6 +952,124 @@ function finishScrollTo(ctx) {
+ }
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++
+ // src/core/checkFinishedScroll.ts
+ var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20;
+ var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1;
+@@ -1528,6 +1262,69 @@ function doScrollTo(ctx, params) {
+ }
+ }
+
++// src/core/adaptiveRender.ts
++var DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY = 3;
++var DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY = 1;
++var DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const isWeb = Platform.OS === "web";
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY : DEFAULT_ADAPTIVE_RENDER_ENTER_VELOCITY;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY : DEFAULT_ADAPTIVE_RENDER_EXIT_VELOCITY;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : isWeb ? DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY : DEFAULT_ADAPTIVE_RENDER_EXIT_DELAY;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1554,9 +1351,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1951,11 +1751,32 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
+-// src/platform/flushSync.native.ts
+-var flushSync = (fn) => {
+- fn();
+-};
+-
++// src/platform/flushSync.native.ts
++var flushSync = (fn) => {
++ fn();
++};
++
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2203,7 +2024,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2225,6 +2046,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2235,6 +2057,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2242,7 +2073,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2255,6 +2086,310 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = Platform.OS === "web" && ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4299,6 +4434,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4316,8 +4452,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -5810,6 +5956,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return Platform.OS === "web" && props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -5819,6 +5966,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -6905,13 +7054,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -7631,6 +7781,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/react-native.web.d.ts b/node_modules/@legendapp/list/react-native.web.d.ts
+index b6c7481..ace847e 100644
+--- a/node_modules/@legendapp/list/react-native.web.d.ts
++++ b/node_modules/@legendapp/list/react-native.web.d.ts
+@@ -511,6 +511,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
+diff --git a/node_modules/@legendapp/list/react-native.web.js b/node_modules/@legendapp/list/react-native.web.js
+index 914d2da..ccb19f4 100644
+--- a/node_modules/@legendapp/list/react-native.web.js
++++ b/node_modules/@legendapp/list/react-native.web.js
+@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) {
+ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll
+ // src/components/ListComponentScrollView.tsx
+ var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style";
+ var SCROLL_END_FALLBACK_MS = 200;
++var SCROLL_EXTENT_EPSILON = 1;
++var REACHABLE_RETRY_FRAMES = 30;
+ var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`;
+ function ensureScrollbarHiddenStyle() {
+ if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) {
+@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ }
+ return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop;
+ }, [getMaxScrollOffset, horizontal, isWindowScroll]);
++ const reissueHandleRef = React3.useRef(0);
++ const scrollUntilReachable = React3.useCallback(
++ (offset, animated, run) => {
++ cancelAnimationFrame(reissueHandleRef.current);
++ let attempts = 0;
++ let previousMaxOffset = Number.NEGATIVE_INFINITY;
++ const attempt = () => {
++ const liveMaxOffset = getMaxScrollOffset();
++ run(clampOffset(offset, liveMaxOffset));
++ const isStillCommitting = liveMaxOffset > previousMaxOffset;
++ previousMaxOffset = liveMaxOffset;
++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) {
++ reissueHandleRef.current = requestAnimationFrame(attempt);
++ }
++ };
++ attempt();
++ },
++ [getMaxScrollOffset]
++ );
++ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []);
+ const scrollToLocalOffset = React3.useCallback(
+ (offset, animated) => {
+ const scrollElement = scrollRef.current;
+@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ if (!target || typeof target.scrollTo !== "function") {
+ return;
+ }
+- const maxOffset = getMaxScrollOffset();
+- const clampedOffset = clampOffset(offset, maxOffset);
+ const behavior = animated ? "smooth" : "auto";
+ const options = { behavior };
+ if (isWindowScroll) {
+ const scroll = getWindowScrollPosition();
+ const listPos = getElementDocumentPosition(scrollElement, scroll);
+ const { left, top } = resolveWindowScrollTarget({
+- clampedOffset,
++ clampedOffset: clampOffset(offset, getMaxScrollOffset()),
+ horizontal,
+ listPos,
+ scroll
+ });
+ options.left = left;
+ options.top = top;
+- } else if (horizontal) {
+- options.left = clampedOffset;
+- } else {
+- options.top = clampedOffset;
++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset());
++ target.scrollTo(options);
++ return;
+ }
+- target.scrollTo(options);
++ scrollUntilReachable(offset, animated, (reachableOffset) => {
++ if (horizontal) {
++ options.left = reachableOffset;
++ } else {
++ options.top = reachableOffset;
++ }
++ ctx.state.lastIssuedScrollOffset = reachableOffset;
++ target.scrollTo(options);
++ });
+ },
+- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll]
++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable]
+ );
+ React3.useImperativeHandle(ref, () => {
+ const api = {
+@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ },
+ scrollToEnd: (options = {}) => {
+ const { animated = true } = options;
+- const endOffset = getMaxScrollOffset();
+- scrollToLocalOffset(endOffset, animated);
++ scrollToLocalOffset(getMaxScrollOffset(), animated);
+ },
+ scrollToOffset: (params) => {
+ const { offset, animated = true } = params;
+@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs
+index 95465f2..4fb1e41 100644
+--- a/node_modules/@legendapp/list/react-native.web.mjs
++++ b/node_modules/@legendapp/list/react-native.web.mjs
+@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) {
+ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll
+ // src/components/ListComponentScrollView.tsx
+ var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style";
+ var SCROLL_END_FALLBACK_MS = 200;
++var SCROLL_EXTENT_EPSILON = 1;
++var REACHABLE_RETRY_FRAMES = 30;
+ var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`;
+ function ensureScrollbarHiddenStyle() {
+ if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) {
+@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ }
+ return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop;
+ }, [getMaxScrollOffset, horizontal, isWindowScroll]);
++ const reissueHandleRef = useRef(0);
++ const scrollUntilReachable = useCallback(
++ (offset, animated, run) => {
++ cancelAnimationFrame(reissueHandleRef.current);
++ let attempts = 0;
++ let previousMaxOffset = Number.NEGATIVE_INFINITY;
++ const attempt = () => {
++ const liveMaxOffset = getMaxScrollOffset();
++ run(clampOffset(offset, liveMaxOffset));
++ const isStillCommitting = liveMaxOffset > previousMaxOffset;
++ previousMaxOffset = liveMaxOffset;
++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) {
++ reissueHandleRef.current = requestAnimationFrame(attempt);
++ }
++ };
++ attempt();
++ },
++ [getMaxScrollOffset]
++ );
++ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []);
+ const scrollToLocalOffset = useCallback(
+ (offset, animated) => {
+ const scrollElement = scrollRef.current;
+@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ if (!target || typeof target.scrollTo !== "function") {
+ return;
+ }
+- const maxOffset = getMaxScrollOffset();
+- const clampedOffset = clampOffset(offset, maxOffset);
+ const behavior = animated ? "smooth" : "auto";
+ const options = { behavior };
+ if (isWindowScroll) {
+ const scroll = getWindowScrollPosition();
+ const listPos = getElementDocumentPosition(scrollElement, scroll);
+ const { left, top } = resolveWindowScrollTarget({
+- clampedOffset,
++ clampedOffset: clampOffset(offset, getMaxScrollOffset()),
+ horizontal,
+ listPos,
+ scroll
+ });
+ options.left = left;
+ options.top = top;
+- } else if (horizontal) {
+- options.left = clampedOffset;
+- } else {
+- options.top = clampedOffset;
++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset());
++ target.scrollTo(options);
++ return;
+ }
+- target.scrollTo(options);
++ scrollUntilReachable(offset, animated, (reachableOffset) => {
++ if (horizontal) {
++ options.left = reachableOffset;
++ } else {
++ options.top = reachableOffset;
++ }
++ ctx.state.lastIssuedScrollOffset = reachableOffset;
++ target.scrollTo(options);
++ });
+ },
+- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll]
++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable]
+ );
+ useImperativeHandle(ref, () => {
+ const api = {
+@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ },
+ scrollToEnd: (options = {}) => {
+ const { animated = true } = options;
+- const endOffset = getMaxScrollOffset();
+- scrollToLocalOffset(endOffset, animated);
++ scrollToLocalOffset(getMaxScrollOffset(), animated);
+ },
+ scrollToOffset: (params) => {
+ const { offset, animated = true } = params;
+@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/react.d.ts b/node_modules/@legendapp/list/react.d.ts
+index b6c7481..ace847e 100644
+--- a/node_modules/@legendapp/list/react.d.ts
++++ b/node_modules/@legendapp/list/react.d.ts
+@@ -511,6 +511,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
+diff --git a/node_modules/@legendapp/list/react.js b/node_modules/@legendapp/list/react.js
+index 914d2da..ccb19f4 100644
+--- a/node_modules/@legendapp/list/react.js
++++ b/node_modules/@legendapp/list/react.js
+@@ -434,605 +434,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1234,58 +635,273 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1425,6 +1041,181 @@ function listenForScrollEnd(ctx, params) {
+ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1451,9 +1242,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1809,6 +1603,27 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2069,7 +1884,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2091,6 +1906,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2101,6 +1917,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2108,7 +1933,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2121,6 +1946,316 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4350,6 +4485,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4367,8 +4503,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -6006,6 +6152,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll
+ // src/components/ListComponentScrollView.tsx
+ var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style";
+ var SCROLL_END_FALLBACK_MS = 200;
++var SCROLL_EXTENT_EPSILON = 1;
++var REACHABLE_RETRY_FRAMES = 30;
+ var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`;
+ function ensureScrollbarHiddenStyle() {
+ if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) {
+@@ -6094,6 +6242,26 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ }
+ return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop;
+ }, [getMaxScrollOffset, horizontal, isWindowScroll]);
++ const reissueHandleRef = React3.useRef(0);
++ const scrollUntilReachable = React3.useCallback(
++ (offset, animated, run) => {
++ cancelAnimationFrame(reissueHandleRef.current);
++ let attempts = 0;
++ let previousMaxOffset = Number.NEGATIVE_INFINITY;
++ const attempt = () => {
++ const liveMaxOffset = getMaxScrollOffset();
++ run(clampOffset(offset, liveMaxOffset));
++ const isStillCommitting = liveMaxOffset > previousMaxOffset;
++ previousMaxOffset = liveMaxOffset;
++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) {
++ reissueHandleRef.current = requestAnimationFrame(attempt);
++ }
++ };
++ attempt();
++ },
++ [getMaxScrollOffset]
++ );
++ React3.useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []);
+ const scrollToLocalOffset = React3.useCallback(
+ (offset, animated) => {
+ const scrollElement = scrollRef.current;
+@@ -6101,29 +6269,34 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ if (!target || typeof target.scrollTo !== "function") {
+ return;
+ }
+- const maxOffset = getMaxScrollOffset();
+- const clampedOffset = clampOffset(offset, maxOffset);
+ const behavior = animated ? "smooth" : "auto";
+ const options = { behavior };
+ if (isWindowScroll) {
+ const scroll = getWindowScrollPosition();
+ const listPos = getElementDocumentPosition(scrollElement, scroll);
+ const { left, top } = resolveWindowScrollTarget({
+- clampedOffset,
++ clampedOffset: clampOffset(offset, getMaxScrollOffset()),
+ horizontal,
+ listPos,
+ scroll
+ });
+ options.left = left;
+ options.top = top;
+- } else if (horizontal) {
+- options.left = clampedOffset;
+- } else {
+- options.top = clampedOffset;
++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset());
++ target.scrollTo(options);
++ return;
+ }
+- target.scrollTo(options);
++ scrollUntilReachable(offset, animated, (reachableOffset) => {
++ if (horizontal) {
++ options.left = reachableOffset;
++ } else {
++ options.top = reachableOffset;
++ }
++ ctx.state.lastIssuedScrollOffset = reachableOffset;
++ target.scrollTo(options);
++ });
+ },
+- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll]
++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable]
+ );
+ React3.useImperativeHandle(ref, () => {
+ const api = {
+@@ -6149,8 +6322,7 @@ var ListComponentScrollView = React3.forwardRef(function ListComponentScrollView
+ },
+ scrollToEnd: (options = {}) => {
+ const { animated = true } = options;
+- const endOffset = getMaxScrollOffset();
+- scrollToLocalOffset(endOffset, animated);
++ scrollToLocalOffset(getMaxScrollOffset(), animated);
+ },
+ scrollToOffset: (params) => {
+ const { offset, animated = true } = params;
+@@ -6499,6 +6671,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -6508,6 +6681,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7570,13 +7745,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -8288,6 +8464,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs
+index 95465f2..4fb1e41 100644
+--- a/node_modules/@legendapp/list/react.mjs
++++ b/node_modules/@legendapp/list/react.mjs
+@@ -413,605 +413,6 @@ var EDGE_POSITION_EPSILON = 1;
+ var ENABLE_DEVMODE = IS_DEV && false;
+ var ENABLE_DEBUG_VIEW = IS_DEV && false;
+
+-// src/core/cancelImperativeScroll.ts
+-function cancelScrollCompletionChecks({ scheduledWork }) {
+- scheduledWork.cancel("checkFinishedScrollFrame");
+- scheduledWork.cancel("checkFinishedScrollRetryFrame");
+- scheduledWork.cancel("checkFinishedScrollFallback");
+- scheduledWork.cancel("platformScrollCompletion");
+-}
+-function settlePendingImperativeScroll(state) {
+- var _a3, _b;
+- const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
+- state.pendingScrollResolve = void 0;
+- state.pendingScrollToEnd = void 0;
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+-}
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
+-}
+-
+-// src/core/deferredPublicOnScroll.ts
+-function withResolvedContentOffset(state, event, resolvedOffset) {
+- return {
+- ...event,
+- nativeEvent: {
+- ...event.nativeEvent,
+- contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
+- }
+- };
+-}
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const deferredEvent = state.deferredPublicOnScrollEvent;
+- state.deferredPublicOnScrollEvent = void 0;
+- if (deferredEvent) {
+- (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
+- _c,
+- withResolvedContentOffset(
+- state,
+- deferredEvent,
+- (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
+- )
+- );
+- }
+-}
+-
+-// src/core/initialScrollSession.ts
+-var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
+-function hasInitialScrollSessionCompletion(completion) {
+- return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
+-}
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
+-}
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
+-}
+-function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
+- var _a4, _b2;
+- if (!state.initialScrollSession) {
+- state.initialScrollSession = createInitialScrollSession({
+- completion: {},
+- kind,
+- previousDataLength: 0
+- });
+- } else if (state.initialScrollSession.kind !== kind) {
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
+- completion: state.initialScrollSession.completion,
+- kind,
+- previousDataLength: state.initialScrollSession.previousDataLength
+- });
+- }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
+-}
+-var initialScrollCompletion = {
+- didDispatchNativeScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
+- },
+- didRetrySilentInitialScroll(state) {
+- var _a3, _b;
+- return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
+- },
+- markInitialScrollNativeDispatch(state) {
+- ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
+- },
+- markSilentInitialScrollRetry(state) {
+- ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
+- },
+- resetFlags(state) {
+- if (!state.initialScrollSession) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
+- completion.didDispatchNativeScroll = void 0;
+- completion.didRetrySilentInitialScroll = void 0;
+- }
+-};
+-var initialScrollWatchdog = {
+- clear(state) {
+- initialScrollWatchdog.set(state, void 0);
+- },
+- didReachTarget(newScroll, watchdog) {
+- const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
+- return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- get(state) {
+- var _a3, _b;
+- return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
+- },
+- hasNonZeroTargetOffset(targetOffset) {
+- return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- isAtZeroTargetOffset(targetOffset) {
+- return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
+- },
+- set(state, watchdog) {
+- var _a3, _b;
+- if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
+- return;
+- }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+- }
+-};
+-function setInitialScrollSession(state, options = {}) {
+- var _a3, _b, _c, _d;
+- const existingSession = state.initialScrollSession;
+- const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
+- const completion = existingSession == null ? void 0 : existingSession.completion;
+- const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
+- const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
+- if (!kind) {
+- return clearInitialScrollSession(state);
+- }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
+- }
+- const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
+- state.initialScrollSession = createInitialScrollSession({
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- });
+- return state.initialScrollSession;
+-}
+-
+-// src/utils/checkThreshold.ts
+-var HYSTERESIS_MULTIPLIER = 1.3;
+-function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
+- const absDistance = Math.abs(distance);
+- return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
+-}
+-var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
+- const absDistance = Math.abs(distance);
+- const within = atThreshold || threshold > 0 && absDistance <= threshold;
+- const updateSnapshot = () => {
+- setSnapshot({
+- atThreshold,
+- contentSize: context.contentSize,
+- dataLength: context.dataLength,
+- scrollPosition: context.scrollPosition
+- });
+- };
+- if (!wasReached) {
+- if (!within) {
+- return false;
+- }
+- onReached(distance);
+- updateSnapshot();
+- return true;
+- }
+- const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
+- if (reset) {
+- setSnapshot(void 0);
+- return false;
+- }
+- if (within) {
+- const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
+- if (changed) {
+- updateSnapshot();
+- }
+- }
+- return true;
+-};
+-
+-// src/utils/edgeReachedGate.ts
+-function resetEdgeLatch(ctx, edge) {
+- const state = ctx.state;
+- if (edge === "start") {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- } else {
+- state.isEndReached = false;
+- state.endReachedSnapshot = void 0;
+- }
+-}
+-function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
+- const state = ctx.state;
+- if (!state.edgeReachedGate) {
+- return;
+- }
+- const contentSize = getContentSize(ctx);
+- const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
+- const isContentLess = contentSize < state.scrollLength;
+- const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
+- const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
+- const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
+- const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
+- if (isOutsideStart && isOutsideEnd) {
+- state.edgeReachedGate = void 0;
+- }
+-}
+-function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
+- return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
+-}
+-function markReachedEdge(ctx) {
+- ctx.state.edgeReachedGate = "closed";
+-}
+-function prepareReachedEdgeForNextUserScroll(ctx) {
+- if (ctx.state.edgeReachedGate) {
+- ctx.state.edgeReachedGate = "prepared";
+- }
+-}
+-function beginReachedEdgeUserScroll(ctx, scrollDelta) {
+- const state = ctx.state;
+- if (state.edgeReachedGate !== "prepared") {
+- return void 0;
+- }
+- const allowedEdge = scrollDelta < 0 ? "start" : "end";
+- state.edgeReachedGate = "closed";
+- resetEdgeLatch(ctx, allowedEdge);
+- return allowedEdge;
+-}
+-
+-// src/utils/hasActiveInitialScroll.ts
+-function hasActiveInitialScroll(state) {
+- return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
+-}
+-
+-// src/utils/checkAtBottom.ts
+-function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- var _a3;
+- const state = ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- queuedInitialLayout,
+- scrollLength,
+- scroll,
+- maintainingScrollAtEnd,
+- props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
+- } = state;
+- const contentSize = getContentSize(ctx);
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (contentSize > 0 && queuedInitialLayout) {
+- const insetEnd = getContentInsetEnd(ctx);
+- const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
+- const isContentLess = contentSize < scrollLength;
+- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
+- set$(
+- ctx,
+- "isWithinMaintainScrollAtEndThreshold",
+- isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
+- );
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
+- if (!shouldSkipThresholdChecks) {
+- state.isEndReached = checkThreshold(
+- distanceFromEnd,
+- isContentLess,
+- onEndReachedThreshold * scrollLength,
+- state.isEndReached,
+- state.endReachedSnapshot,
+- {
+- contentSize,
+- dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a4, _b;
+- if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
+- }
+- },
+- (snapshot) => {
+- state.endReachedSnapshot = snapshot;
+- }
+- );
+- }
+- }
+-}
+-
+-// src/utils/checkAtTop.ts
+-function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+- const state = ctx == null ? void 0 : ctx.state;
+- if (!state) {
+- return;
+- }
+- const {
+- isStartReached,
+- props: { data, onStartReachedThreshold },
+- scroll,
+- scrollLength,
+- startReachedSnapshot,
+- totalSize
+- } = state;
+- const dataLength = data.length;
+- const threshold = onStartReachedThreshold * scrollLength;
+- resetSharedEdgeGateIfOutsideHysteresis(ctx);
+- if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
+- state.isStartReached = false;
+- state.startReachedSnapshot = void 0;
+- }
+- set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
+- set$(ctx, "isNearStart", scroll <= threshold);
+- const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
+- if (!shouldSkipThresholdChecks) {
+- state.isStartReached = checkThreshold(
+- scroll,
+- false,
+- threshold,
+- state.isStartReached,
+- startReachedSnapshot,
+- {
+- contentSize: totalSize,
+- dataLength,
+- scrollPosition: scroll
+- },
+- (distance) => {
+- var _a3, _b;
+- if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
+- markReachedEdge(ctx);
+- (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
+- }
+- },
+- (snapshot) => {
+- state.startReachedSnapshot = snapshot;
+- }
+- );
+- }
+-}
+-
+-// src/utils/checkThresholds.ts
+-function checkThresholds(ctx, allowedEdge) {
+- const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
+- checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+- checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+-}
+-
+-// src/core/recalculateSettledScroll.ts
+-function recalculateSettledScroll(ctx) {
+- var _a3, _b;
+- const state = ctx.state;
+- if ((_a3 = state.props) == null ? void 0 : _a3.data) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+- }
+- checkThresholds(ctx);
+-}
+-var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
+-var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
+-function clearAdaptiveRenderExitTimeout(ctx) {
+- ctx.state.scheduledWork.cancel("adaptiveRender");
+-}
+-function scheduleAdaptiveRenderExit(ctx, exitDelay) {
+- const state = ctx.state;
+- clearAdaptiveRenderExitTimeout(ctx);
+- if (exitDelay <= 0) {
+- setAdaptiveRender(ctx, "normal", "scroll");
+- } else {
+- state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+- }
+-}
+-function setAdaptiveRender(ctx, mode, reason) {
+- var _a3, _b;
+- const previousMode = peek$(ctx, "adaptiveRender");
+- if (previousMode !== mode) {
+- set$(ctx, "adaptiveRender", mode);
+- (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
+- }
+-}
+-function resetAdaptiveRender(ctx) {
+- var _a3, _b;
+- clearAdaptiveRenderExitTimeout(ctx);
+- const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
+- if (peek$(ctx, "adaptiveRender") !== mode) {
+- setAdaptiveRender(ctx, mode, "initial");
+- }
+-}
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const adaptiveRender = state.props.adaptiveRender;
+- const currentMode = peek$(ctx, "adaptiveRender");
+- if (peek$(ctx, "readyToRender")) {
+- if (adaptiveRender) {
+- const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
+- const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
+- const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
+- const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
+- const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
+- const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
+- if (nextMode !== previousMode) {
+- if (nextMode === "light") {
+- setAdaptiveRender(ctx, "light", "scroll");
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- } else if (currentMode === "light") {
+- scheduleAdaptiveRenderExit(ctx, exitDelay);
+- }
+- }
+- } else {
+- resetAdaptiveRender(ctx);
+- }
+- }
+-}
+-
+-// src/utils/getEffectiveDrawDistance.ts
+-var INITIAL_DRAW_DISTANCE = 50;
+-function getEffectiveDrawDistance(ctx, mode) {
+- var _a3;
+- const drawDistance = ctx.state.props.drawDistance;
+- const initialScroll = ctx.state.initialScroll;
+- const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
+- const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
+- return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
+-}
+-function scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
+- }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
+-}
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
+- }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
+- }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
+- }
+- if (didInitialScroll) {
+- state.didFinishInitialScroll = true;
+- }
+- const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
+- if (isReadyToRender && !peek$(ctx, "readyToRender")) {
+- set$(ctx, "readyToRender", true);
+- setAdaptiveRender(ctx, "normal", "ready");
+- if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
+- scheduleFullDrawDistancePrewarm(ctx);
+- }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
+- }
+- }
+-}
+-
+-// src/core/finishInitialScroll.ts
+-var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
+-function syncInitialScrollOffset(state, offset) {
+- state.scroll = offset;
+- state.scrollPending = offset;
+- state.scrollPrev = offset;
+-}
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
+-}
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
+-}
+-function supersedeInitialScroll(ctx) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
+- if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
+- if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
+- cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
+- }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
+- }
+-}
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
+- const state = ctx.state;
+- if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
+- syncInitialScrollOffset(state, options.resolvedOffset);
+- } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
+- const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
+- if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
+- syncInitialScrollOffset(state, observedOffset);
+- }
+- }
+- const complete = () => {
+- var _a4, _b2, _c2, _d, _e;
+- const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
+- const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
+- initialScrollWatchdog.clear(state);
+- if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- setInitialScrollSession(state);
+- clearPreservedInitialScrollTargetTimeout(state);
+- if (options == null ? void 0 : options.schedulePreservedTargetClear) {
+- state.scheduledWork.timeout(
+- () => {
+- var _a5;
+- if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
+- return;
+- }
+- clearPreservedInitialScrollTarget(state);
+- },
+- PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
+- "preservedInitialScroll"
+- );
+- }
+- } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+- }
+- (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+- };
+- if (options == null ? void 0 : options.waitForCompletionFrame) {
+- requestAnimationFrame(complete);
+- return;
+- }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
+-}
+-
+ // src/core/getStartOffsetAdjustment.ts
+ function getStartOffsetAdjustment(ctx) {
+ const { state } = ctx;
+@@ -1213,58 +614,273 @@ function getItemSizeAtIndex(ctx, index) {
+ if (index === void 0 || index < 0) {
+ return void 0;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ const targetId = getId(ctx.state, index);
++ return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++}
++
++// src/core/calculateOffsetWithOffsetPosition.ts
++function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++ var _a3;
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ let offset = offsetParam;
++ if (viewOffset) {
++ offset -= viewOffset;
++ }
++ if (index !== void 0) {
++ const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
++ if (startOffsetAdjustment) {
++ offset += startOffsetAdjustment;
++ }
++ }
++ if (viewPosition !== void 0 && index !== void 0) {
++ const dataLength = state.props.data.length;
++ if (dataLength === 0) {
++ return offset;
++ }
++ const isOutOfBounds = index < 0 || index >= dataLength;
++ const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
++ const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
++ const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
++ const trailingInset = getContentInsetEnd(ctx);
++ offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
++ if (!isOutOfBounds && index === state.props.data.length - 1) {
++ const footerSize = peek$(ctx, "footerSize") || 0;
++ offset += footerSize;
++ }
++ }
++ return offset;
++}
++
++// src/core/clampScrollOffset.ts
++function clampScrollOffset(ctx, offset, scrollTarget) {
++ const state = ctx.state;
++ const contentSize = getContentSize(ctx);
++ let clampedOffset = offset;
++ if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
++ const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
++ const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
++ const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
++ const maxOffset = baseMaxOffset + extraEndOffset;
++ clampedOffset = Math.min(offset, maxOffset);
++ }
++ clampedOffset = Math.max(0, clampedOffset);
++ return clampedOffset;
++}
++
++// src/utils/checkThreshold.ts
++var HYSTERESIS_MULTIPLIER = 1.3;
++function isOutsideThresholdHysteresis(distance, atThreshold, threshold) {
++ const absDistance = Math.abs(distance);
++ return !atThreshold && threshold > 0 && absDistance >= threshold * HYSTERESIS_MULTIPLIER || !atThreshold && threshold <= 0 && absDistance > 0;
++}
++var checkThreshold = (distance, atThreshold, threshold, wasReached, snapshot, context, onReached, setSnapshot) => {
++ const absDistance = Math.abs(distance);
++ const within = atThreshold || threshold > 0 && absDistance <= threshold;
++ const updateSnapshot = () => {
++ setSnapshot({
++ atThreshold,
++ contentSize: context.contentSize,
++ dataLength: context.dataLength,
++ scrollPosition: context.scrollPosition
++ });
++ };
++ if (!wasReached) {
++ if (!within) {
++ return false;
++ }
++ onReached(distance);
++ updateSnapshot();
++ return true;
++ }
++ const reset = isOutsideThresholdHysteresis(distance, atThreshold, threshold);
++ if (reset) {
++ setSnapshot(void 0);
++ return false;
++ }
++ if (within) {
++ const changed = !snapshot || snapshot.atThreshold !== atThreshold || snapshot.contentSize !== context.contentSize || snapshot.dataLength !== context.dataLength;
++ if (changed) {
++ updateSnapshot();
++ }
++ }
++ return true;
++};
++
++// src/utils/edgeReachedGate.ts
++function resetEdgeLatch(ctx, edge) {
++ const state = ctx.state;
++ if (edge === "start") {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ } else {
++ state.isEndReached = false;
++ state.endReachedSnapshot = void 0;
++ }
++}
++function resetSharedEdgeGateIfOutsideHysteresis(ctx) {
++ const state = ctx.state;
++ if (!state.edgeReachedGate) {
++ return;
++ }
++ const contentSize = getContentSize(ctx);
++ const endDistance = contentSize - state.scroll - state.scrollLength - getContentInsetEnd(ctx);
++ const isContentLess = contentSize < state.scrollLength;
++ const startThreshold = state.props.onStartReachedThreshold * state.scrollLength;
++ const endThreshold = state.props.onEndReachedThreshold * state.scrollLength;
++ const isOutsideStart = isOutsideThresholdHysteresis(state.scroll, false, startThreshold);
++ const isOutsideEnd = isOutsideThresholdHysteresis(endDistance, isContentLess, endThreshold);
++ if (isOutsideStart && isOutsideEnd) {
++ state.edgeReachedGate = void 0;
++ }
++}
++function canDispatchReachedEdge(ctx, edge, allowedEdge, allowGateCreatedInCurrentCheck) {
++ return !ctx.state.edgeReachedGate || allowedEdge === edge || !!allowGateCreatedInCurrentCheck;
++}
++function markReachedEdge(ctx) {
++ ctx.state.edgeReachedGate = "closed";
++}
++function prepareReachedEdgeForNextUserScroll(ctx) {
++ if (ctx.state.edgeReachedGate) {
++ ctx.state.edgeReachedGate = "prepared";
++ }
++}
++function beginReachedEdgeUserScroll(ctx, scrollDelta) {
++ const state = ctx.state;
++ if (state.edgeReachedGate !== "prepared") {
++ return void 0;
++ }
++ const allowedEdge = scrollDelta < 0 ? "start" : "end";
++ state.edgeReachedGate = "closed";
++ resetEdgeLatch(ctx, allowedEdge);
++ return allowedEdge;
++}
++
++// src/utils/hasActiveInitialScroll.ts
++function hasActiveInitialScroll(state) {
++ return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
++}
++
++// src/utils/checkAtBottom.ts
++function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ var _a3;
++ const state = ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ queuedInitialLayout,
++ scrollLength,
++ scroll,
++ maintainingScrollAtEnd,
++ props: { maintainScrollAtEndThreshold, onEndReachedThreshold }
++ } = state;
++ const contentSize = getContentSize(ctx);
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (contentSize > 0 && queuedInitialLayout) {
++ const insetEnd = getContentInsetEnd(ctx);
++ const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
++ const isContentLess = contentSize < scrollLength;
++ set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
++ set$(
++ ctx,
++ "isWithinMaintainScrollAtEndThreshold",
++ isContentLess || distanceFromEnd <= maintainScrollAtEndThreshold * scrollLength
++ );
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || maintainingScrollAtEnd;
++ if (!shouldSkipThresholdChecks) {
++ state.isEndReached = checkThreshold(
++ distanceFromEnd,
++ isContentLess,
++ onEndReachedThreshold * scrollLength,
++ state.isEndReached,
++ state.endReachedSnapshot,
++ {
++ contentSize,
++ dataLength: (_a3 = state.props.data) == null ? void 0 : _a3.length,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a4, _b;
++ if (canDispatchReachedEdge(ctx, "end", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a4 = state.props).onEndReached) == null ? void 0 : _b.call(_a4, { distanceFromEnd: distance });
++ }
++ },
++ (snapshot) => {
++ state.endReachedSnapshot = snapshot;
++ }
++ );
++ }
++ }
++}
++
++// src/utils/checkAtTop.ts
++function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
++ const state = ctx == null ? void 0 : ctx.state;
++ if (!state) {
++ return;
++ }
++ const {
++ isStartReached,
++ props: { data, onStartReachedThreshold },
++ scroll,
++ scrollLength,
++ startReachedSnapshot,
++ totalSize
++ } = state;
++ const dataLength = data.length;
++ const threshold = onStartReachedThreshold * scrollLength;
++ resetSharedEdgeGateIfOutsideHysteresis(ctx);
++ if (isStartReached && threshold > 0 && scroll > threshold && startReachedSnapshot && (startReachedSnapshot.contentSize !== totalSize || startReachedSnapshot.dataLength !== dataLength)) {
++ state.isStartReached = false;
++ state.startReachedSnapshot = void 0;
++ }
++ set$(ctx, "isAtStart", scroll <= EDGE_POSITION_EPSILON);
++ set$(ctx, "isNearStart", scroll <= threshold);
++ const shouldSkipThresholdChecks = hasActiveInitialScroll(state) || !!state.scrollingTo;
++ if (!shouldSkipThresholdChecks) {
++ state.isStartReached = checkThreshold(
++ scroll,
++ false,
++ threshold,
++ state.isStartReached,
++ startReachedSnapshot,
++ {
++ contentSize: totalSize,
++ dataLength,
++ scrollPosition: scroll
++ },
++ (distance) => {
++ var _a3, _b;
++ if (canDispatchReachedEdge(ctx, "start", allowedEdge, allowGateCreatedInCurrentCheck)) {
++ markReachedEdge(ctx);
++ (_b = (_a3 = state.props).onStartReached) == null ? void 0 : _b.call(_a3, { distanceFromStart: distance });
++ }
++ },
++ (snapshot) => {
++ state.startReachedSnapshot = snapshot;
++ }
++ );
++ }
+ }
+
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
+- var _a3;
+- const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
+- }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
+- }
+- if (viewPosition !== void 0 && index !== void 0) {
+- const dataLength = state.props.data.length;
+- if (dataLength === 0) {
+- return offset;
+- }
+- const isOutOfBounds = index < 0 || index >= dataLength;
+- const fallbackEstimatedSize = (_a3 = state.props.estimatedItemSize) != null ? _a3 : 0;
+- const measuredItemSize = isOutOfBounds ? fallbackEstimatedSize : getItemSize(ctx, getId(state, index), index, state.props.data[index]);
+- const itemSize = Math.max(0, measuredItemSize - (isOutOfBounds ? 0 : ctx.scrollAxisGap));
+- const trailingInset = getContentInsetEnd(ctx);
+- offset -= viewPosition * (state.scrollLength - trailingInset - itemSize);
+- if (!isOutOfBounds && index === state.props.data.length - 1) {
+- const footerSize = peek$(ctx, "footerSize") || 0;
+- offset += footerSize;
+- }
+- }
+- return offset;
++// src/utils/checkThresholds.ts
++function checkThresholds(ctx, allowedEdge) {
++ const allowGateCreatedInCurrentCheck = !ctx.state.edgeReachedGate;
++ checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
++ checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck);
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// src/core/recalculateSettledScroll.ts
++function recalculateSettledScroll(ctx) {
++ var _a3, _b;
+ const state = ctx.state;
+- const contentSize = getContentSize(ctx);
+- let clampedOffset = offset;
+- if (Number.isFinite(contentSize) && Number.isFinite(state.scrollLength) && (Platform.OS !== "android")) {
+- const baseMaxOffset = Math.max(0, contentSize - state.scrollLength);
+- const viewOffset = scrollTarget == null ? void 0 : scrollTarget.viewOffset;
+- const extraEndOffset = typeof viewOffset === "number" && viewOffset < 0 ? -viewOffset : 0;
+- const maxOffset = baseMaxOffset + extraEndOffset;
+- clampedOffset = Math.min(offset, maxOffset);
++ if ((_a3 = state.props) == null ? void 0 : _a3.data) {
++ (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state, { forceFullItemPositions: true });
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
++ checkThresholds(ctx);
+ }
+
+ // src/core/finishScrollTo.ts
+@@ -1404,6 +1020,181 @@ function listenForScrollEnd(ctx, params) {
+ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+
++// src/core/initialScrollSession.ts
++var INITIAL_SCROLL_MIN_TARGET_OFFSET = 1;
++function hasInitialScrollSessionCompletion(completion) {
++ return !!((completion == null ? void 0 : completion.didDispatchNativeScroll) || (completion == null ? void 0 : completion.didRetrySilentInitialScroll) || (completion == null ? void 0 : completion.watchdog));
++}
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
++}
++function createInitialScrollSession(options) {
++ const { bootstrap, completion, kind, previousDataLength } = options;
++ return kind === "offset" ? {
++ completion,
++ kind,
++ previousDataLength
++ } : {
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ };
++}
++function ensureInitialScrollSessionCompletion(state, kind = ((_b) => (_b = ((_a3) => (_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind)()) != null ? _b : "bootstrap")()) {
++ var _a4, _b2;
++ if (!state.initialScrollSession) {
++ state.initialScrollSession = createInitialScrollSession({
++ completion: {},
++ kind,
++ previousDataLength: 0
++ });
++ } else if (state.initialScrollSession.kind !== kind) {
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap: state.initialScrollSession.kind === "bootstrap" ? state.initialScrollSession.bootstrap : void 0,
++ completion: state.initialScrollSession.completion,
++ kind,
++ previousDataLength: state.initialScrollSession.previousDataLength
++ });
++ }
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
++}
++var initialScrollCompletion = {
++ didDispatchNativeScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didDispatchNativeScroll);
++ },
++ didRetrySilentInitialScroll(state) {
++ var _a3, _b;
++ return !!((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.didRetrySilentInitialScroll);
++ },
++ markInitialScrollNativeDispatch(state) {
++ ensureInitialScrollSessionCompletion(state).didDispatchNativeScroll = true;
++ },
++ markSilentInitialScrollRetry(state) {
++ ensureInitialScrollSessionCompletion(state).didRetrySilentInitialScroll = true;
++ },
++ resetFlags(state) {
++ if (!state.initialScrollSession) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
++ }
++};
++var initialScrollWatchdog = {
++ clear(state) {
++ initialScrollWatchdog.set(state, void 0);
++ },
++ didReachTarget(newScroll, watchdog) {
++ const nextDistance = Math.abs(newScroll - watchdog.targetOffset);
++ return nextDistance <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ get(state) {
++ var _a3, _b;
++ return (_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog;
++ },
++ hasNonZeroTargetOffset(targetOffset) {
++ return targetOffset !== void 0 && targetOffset > INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ isAtZeroTargetOffset(targetOffset) {
++ return targetOffset <= INITIAL_SCROLL_MIN_TARGET_OFFSET;
++ },
++ set(state, watchdog) {
++ var _a3, _b;
++ if (!watchdog && !((_b = (_a3 = state.initialScrollSession) == null ? void 0 : _a3.completion) == null ? void 0 : _b.watchdog)) {
++ return;
++ }
++ const completion = ensureInitialScrollSessionCompletion(state);
++ completion.watchdog = watchdog ? {
++ startScroll: watchdog.startScroll,
++ targetOffset: watchdog.targetOffset
++ } : void 0;
++ }
++};
++function setInitialScrollSession(state, options = {}) {
++ var _a3, _b, _c, _d;
++ const existingSession = state.initialScrollSession;
++ const kind = (_a3 = options.kind) != null ? _a3 : existingSession == null ? void 0 : existingSession.kind;
++ const completion = existingSession == null ? void 0 : existingSession.completion;
++ const existingBootstrap = (existingSession == null ? void 0 : existingSession.kind) === "bootstrap" ? existingSession.bootstrap : void 0;
++ const bootstrap = kind === "bootstrap" ? options.bootstrap === null ? void 0 : (_b = options.bootstrap) != null ? _b : existingBootstrap : void 0;
++ if (!kind) {
++ return clearInitialScrollSession(state);
++ }
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
++ }
++ const previousDataLength = (_d = (_c = options.previousDataLength) != null ? _c : existingSession == null ? void 0 : existingSession.previousDataLength) != null ? _d : 0;
++ state.initialScrollSession = createInitialScrollSession({
++ bootstrap,
++ completion,
++ kind,
++ previousDataLength
++ });
++ return state.initialScrollSession;
++}
++var DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY = 6;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY = 3;
++var DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY = 250;
++function clearAdaptiveRenderExitTimeout(ctx) {
++ ctx.state.scheduledWork.cancel("adaptiveRender");
++}
++function scheduleAdaptiveRenderExit(ctx, exitDelay) {
++ const state = ctx.state;
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
++ }
++}
++function setAdaptiveRender(ctx, mode, reason) {
++ var _a3, _b;
++ const previousMode = peek$(ctx, "adaptiveRender");
++ if (previousMode !== mode) {
++ set$(ctx, "adaptiveRender", mode);
++ (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.onChange) == null ? void 0 : _b.call(_a3, mode, reason);
++ }
++}
++function resetAdaptiveRender(ctx) {
++ var _a3, _b;
++ clearAdaptiveRenderExitTimeout(ctx);
++ const mode = (_b = (_a3 = ctx.state.props.adaptiveRender) == null ? void 0 : _a3.initialMode) != null ? _b : "normal";
++ if (peek$(ctx, "adaptiveRender") !== mode) {
++ setAdaptiveRender(ctx, mode, "initial");
++ }
++}
++function updateAdaptiveRender(ctx, scrollVelocity, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const adaptiveRender = state.props.adaptiveRender;
++ const currentMode = peek$(ctx, "adaptiveRender");
++ if (peek$(ctx, "readyToRender")) {
++ if (adaptiveRender) {
++ const enterVelocity = (_a3 = adaptiveRender.enterVelocity) != null ? _a3 : DEFAULT_WEB_ADAPTIVE_RENDER_ENTER_VELOCITY ;
++ const exitVelocity = (_b = adaptiveRender.exitVelocity) != null ? _b : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_VELOCITY ;
++ const exitDelay = (_c = adaptiveRender.exitDelay) != null ? _c : DEFAULT_WEB_ADAPTIVE_RENDER_EXIT_DELAY ;
++ const threshold = currentMode === "light" ? exitVelocity : enterVelocity;
++ const nextMode = (options == null ? void 0 : options.forceLight) || Math.abs(scrollVelocity) > threshold ? "light" : "normal";
++ const previousMode = state.scheduledWork.has("adaptiveRender") ? "normal" : currentMode;
++ if (nextMode !== previousMode) {
++ if (nextMode === "light") {
++ setAdaptiveRender(ctx, "light", "scroll");
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ } else if (currentMode === "light") {
++ scheduleAdaptiveRenderExit(ctx, exitDelay);
++ }
++ }
++ } else {
++ resetAdaptiveRender(ctx);
++ }
++ }
++}
++
+ // src/core/doMaintainScrollAtEnd.ts
+ function doMaintainScrollAtEnd(ctx) {
+ const state = ctx.state;
+@@ -1430,9 +1221,12 @@ function doMaintainScrollAtEnd(ctx) {
+ const activeState = maintainScrollAtEnd.animated ? "animated" : "instant";
+ const scrollAtRequest = state.scroll;
+ state.maintainingScrollAtEnd = pendingState;
++ clearScrollTargetSettle(state);
+ requestAnimationFrame(() => {
+ const isStillWithinThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
+- const didScrollSinceRequest = state.scroll !== scrollAtRequest;
++ const lastIssued = state.lastIssuedScrollOffset;
++ const isSettlingOntoIssuedScroll = lastIssued !== void 0 && Math.abs(state.scroll - lastIssued) <= EDGE_POSITION_EPSILON;
++ const didScrollSinceRequest = state.scroll !== scrollAtRequest && !isSettlingOntoIssuedScroll;
+ if (isStillWithinThreshold || !didScrollSinceRequest) {
+ state.maintainingScrollAtEnd = activeState;
+ const scroller = refScroller.current;
+@@ -1788,6 +1582,27 @@ function prepareMVCP(ctx, dataChanged) {
+ }
+ }
+
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
++ var _a3;
++ const drawDistance = ctx.state.props.drawDistance;
++ const initialScroll = ctx.state.initialScroll;
++ const needsFullInitialDrawDistance = initialScroll !== void 0 && ((_a3 = initialScroll.viewPosition) != null ? _a3 : 0) > 0;
++ const shouldCapDrawDistance = mode === "visible-first" || mode !== "full" && !peek$(ctx, "readyToRender") && !needsFullInitialDrawDistance;
++ return shouldCapDrawDistance ? Math.min(drawDistance, INITIAL_DRAW_DISTANCE) : drawDistance;
++}
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
++ }
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
++}
++
+ // src/utils/getScrollVelocity.ts
+ var MAX_SCROLL_VELOCITY_WINDOW_MS = 1e3;
+ var SCROLL_VELOCITY_HALF_LIFE_MS = 200;
+@@ -2048,7 +1863,7 @@ function pinScrollTargetRenderRange(ctx, targetOffset, targetIndex) {
+ }
+ }
+ function scrollTo(ctx, params) {
+- var _a3, _b;
++ var _a3, _b, _c;
+ const state = ctx.state;
+ const { noScrollingTo, forceScroll, ...scrollTarget } = params;
+ const {
+@@ -2070,6 +1885,7 @@ function scrollTo(ctx, params) {
+ if (!noScrollingTo) {
+ if (isInitialScroll) {
+ initialScrollCompletion.resetFlags(state);
++ clearScrollTargetSettle(state);
+ }
+ const averageSizeSnapshot = getAverageSizeSnapshot(state);
+ state.scrollingTo = {
+@@ -2080,6 +1896,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (!animated && scrollTarget.index !== void 0 && scrollTarget.viewPosition !== void 0) {
++ beginScrollTargetSettle(ctx, {
++ index: scrollTarget.index,
++ viewOffset: (_b = scrollTarget.viewOffset) != null ? _b : 0,
++ viewPosition: scrollTarget.viewPosition
++ });
++ } else {
++ clearScrollTargetSettle(state);
++ }
+ }
+ }
+ state.scrollPending = targetOffset;
+@@ -2087,7 +1912,7 @@ function scrollTo(ctx, params) {
+ if (!isInitialScroll && !noScrollingTo && Math.abs(state.scroll - targetOffset) > 1) {
+ if (animated) {
+ if (state.scrollTargetPinnedRange) {
+- (_b = state.triggerCalculateItemsInView) == null ? void 0 : _b.call(state);
++ (_c = state.triggerCalculateItemsInView) == null ? void 0 : _c.call(state);
+ }
+ } else {
+ updateScroll(ctx, targetOffset, true, { markHasScrolled: false });
+@@ -2100,6 +1925,316 @@ function scrollTo(ctx, params) {
+ }
+ }
+
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 1;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_MAX_MS = 1e3;
++var SETTLE_MAX_CORRECTIONS = 8;
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++ state.scheduledWork.cancel("scrollTargetSettle");
++ state.scheduledWork.cancel("scrollTargetSettleDeadline");
++}
++function beginScrollTargetSettle(ctx, params) {
++ const state = ctx.state;
++ const { index, viewOffset, viewPosition } = params;
++ const { alignItemsAtEnd, data, maintainScrollAtEnd } = state.props;
++ const hasEndAnchoring = !!maintainScrollAtEnd || !!alignItemsAtEnd;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem && hasEndAnchoring) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ clearScrollTargetSettle(state);
++ const now = Date.now();
++ state.scrollTargetSettle = {
++ corrections: 0,
++ deadline: now + SETTLE_MAX_MS,
++ id: getId(state, index),
++ measuredIndex: void 0,
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++ state.scheduledWork.timeout(() => clearScrollTargetSettle(state), SETTLE_MAX_MS, "scrollTargetSettleDeadline");
++}
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function applyScrollTargetCorrection(ctx, id) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle || settle.id !== id) {
++ return;
++ }
++ const index = state.indexByKey.get(id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0 || Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ if (settle.corrections >= SETTLE_MAX_CORRECTIONS) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ settle.corrections++;
++ settle.measuredIndex = void 0;
++ scrollTo(ctx, {
++ animated: false,
++ index,
++ itemSize: getItemSizeAtIndex(ctx, index),
++ // Correcting where a scroll already landed is not a new imperative scroll: claiming the
++ // session would suppress the list's own handling of subsequent scroll events and re-arm
++ // this settle from inside its own correction.
++ noScrollingTo: true,
++ offset: position,
++ viewOffset: settle.viewOffset,
++ viewPosition: settle.viewPosition
++ });
++ const scrollingTo = state.scrollingTo;
++ if (scrollingTo) {
++ scrollingTo.targetOffset = state.scrollPending;
++ scrollingTo.offset = position;
++ }
++ (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++}
++function settleScrollTarget(ctx, options) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
++ }
++ const measured = options == null ? void 0 : options.minIndexSizeChanged;
++ if (measured !== void 0) {
++ settle.measuredIndex = Math.min((_a3 = settle.measuredIndex) != null ? _a3 : Number.POSITIVE_INFINITY, measured);
++ }
++ if (options == null ? void 0 : options.isCompensating) {
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ settle.quietPasses = 0;
++ return false;
++ }
++ const index = state.indexByKey.get(settle.id);
++ const position = index === void 0 ? void 0 : state.positions[index];
++ if (index === void 0 || position === void 0) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (Date.now() > settle.deadline) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ if (settle.measuredIndex === void 0 || settle.measuredIndex > index) {
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ if (Math.abs(targetOffset - state.scroll) <= SETTLE_POSITION_EPSILON) {
++ settle.measuredIndex = void 0;
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ state.scheduledWork.frame(() => applyScrollTargetCorrection(ctx, settle.id), "scrollTargetSettle");
++ return true;
++}
++
++// src/core/cancelImperativeScroll.ts
++function cancelScrollCompletionChecks({ scheduledWork }) {
++ scheduledWork.cancel("checkFinishedScrollFrame");
++ scheduledWork.cancel("checkFinishedScrollRetryFrame");
++ scheduledWork.cancel("checkFinishedScrollFallback");
++ scheduledWork.cancel("platformScrollCompletion");
++}
++function settlePendingImperativeScroll(state) {
++ var _a3, _b;
++ const resolvePendingScroll = (_b = state.pendingScrollResolve) != null ? _b : (_a3 = state.pendingScrollToEnd) == null ? void 0 : _a3.resolve;
++ state.pendingScrollResolve = void 0;
++ state.pendingScrollToEnd = void 0;
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++}
++function cancelImperativeScroll(state) {
++ cancelScrollCompletionChecks(state);
++ state.scheduledWork.cancel("imperativeScrollReady");
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ clearScrollTargetSettle(state);
++ settlePendingImperativeScroll(state);
++}
++
++// src/core/deferredPublicOnScroll.ts
++function withResolvedContentOffset(state, event, resolvedOffset) {
++ return {
++ ...event,
++ nativeEvent: {
++ ...event.nativeEvent,
++ contentOffset: state.props.horizontal ? { x: resolvedOffset, y: 0 } : { x: 0, y: resolvedOffset }
++ }
++ };
++}
++function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
++ var _a3, _b, _c, _d;
++ const state = ctx.state;
++ const deferredEvent = state.deferredPublicOnScrollEvent;
++ state.deferredPublicOnScrollEvent = void 0;
++ if (deferredEvent) {
++ (_d = (_c = state.props).onScroll) == null ? void 0 : _d.call(
++ _c,
++ withResolvedContentOffset(
++ state,
++ deferredEvent,
++ (_b = (_a3 = resolvedOffset != null ? resolvedOffset : state.scrollPending) != null ? _a3 : state.scroll) != null ? _b : 0
++ )
++ );
++ }
++}
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
++ }
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
++ }
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
++}
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
++ }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
++ }
++ const isReadyToRender = Boolean(state.didContainersLayout && state.didFinishInitialScroll);
++ if (isReadyToRender && !peek$(ctx, "readyToRender")) {
++ set$(ctx, "readyToRender", true);
++ setAdaptiveRender(ctx, "normal", "ready");
++ if (state.props.drawDistance > INITIAL_DRAW_DISTANCE) {
++ scheduleFullDrawDistancePrewarm(ctx);
++ }
++ if (!state.didLoad) {
++ state.didLoad = true;
++ if (onLoad) {
++ onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
++ }
++ }
++ }
++}
++
++// src/core/finishInitialScroll.ts
++var PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS = 2e3;
++function syncInitialScrollOffset(state, offset) {
++ state.scroll = offset;
++ state.scrollPending = offset;
++ state.scrollPrev = offset;
++}
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
++}
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
++}
++function supersedeInitialScroll(ctx) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ const bootstrapInitialScroll = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap" ? state.initialScrollSession.bootstrap : void 0;
++ if (state.initialScroll || bootstrapInitialScroll || ((_b = state.scrollingTo) == null ? void 0 : _b.isInitialScroll)) {
++ if ((bootstrapInitialScroll == null ? void 0 : bootstrapInitialScroll.frameHandle) !== void 0 && typeof cancelAnimationFrame === "function") {
++ cancelAnimationFrame(bootstrapInitialScroll.frameHandle);
++ }
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ }
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
++ }
++}
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
++ const state = ctx.state;
++ if ((options == null ? void 0 : options.resolvedOffset) !== void 0) {
++ syncInitialScrollOffset(state, options.resolvedOffset);
++ } else if ((options == null ? void 0 : options.syncObservedOffset) && ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset") {
++ const observedOffset = (_c = (_b = state.refScroller.current) == null ? void 0 : _b.getCurrentScrollOffset) == null ? void 0 : _c.call(_b);
++ if (typeof observedOffset === "number" && Number.isFinite(observedOffset)) {
++ syncInitialScrollOffset(state, observedOffset);
++ }
++ }
++ const complete = () => {
++ var _a4, _b2, _c2, _d, _e;
++ const shouldReleaseDeferredPublicOnScroll = ((_a4 = state.initialScrollSession) == null ? void 0 : _a4.kind) === "bootstrap";
++ const finalScrollOffset = (_d = (_c2 = (_b2 = options == null ? void 0 : options.resolvedOffset) != null ? _b2 : state.scrollPending) != null ? _c2 : state.scroll) != null ? _d : 0;
++ initialScrollWatchdog.clear(state);
++ if ((options == null ? void 0 : options.preserveTarget) && state.initialScroll) {
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ setInitialScrollSession(state);
++ clearPreservedInitialScrollTargetTimeout(state);
++ if (options == null ? void 0 : options.schedulePreservedTargetClear) {
++ state.scheduledWork.timeout(
++ () => {
++ var _a5;
++ if (!state.didFinishInitialScroll || ((_a5 = state.scrollingTo) == null ? void 0 : _a5.isInitialScroll) || !state.initialScroll) {
++ return;
++ }
++ clearPreservedInitialScrollTarget(state);
++ },
++ PRESERVED_INITIAL_SCROLL_FALLBACK_CLEAR_DELAY_MS,
++ "preservedInitialScroll"
++ );
++ }
++ } else {
++ clearPreservedInitialScrollTarget(state);
++ }
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
++ }
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ }
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
++ };
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
++ }
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
++}
++
+ // src/core/scrollToIndex.ts
+ function clampScrollIndex(index, dataLength) {
+ if (dataLength <= 0) {
+@@ -4329,6 +4464,7 @@ function calculateItemsInView(ctx, params = {}) {
+ startIndex
+ });
+ totalSize = getContentSize(ctx);
++ const minIndexSizeChangedThisPass = minIndexSizeChanged;
+ if (minIndexSizeChanged !== void 0) {
+ state.minIndexSizeChanged = void 0;
+ }
+@@ -4346,8 +4482,18 @@ function calculateItemsInView(ctx, params = {}) {
+ const scrollBeforeMVCP = state.scroll;
+ const scrollAdjustPendingBeforeMVCP = (_f = peek$(ctx, "scrollAdjustPending")) != null ? _f : 0;
+ checkMVCP == null ? void 0 : checkMVCP();
+- const didMVCPAdjustScroll = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+- if (didMVCPAdjustScroll) {
++ const scrollAdjustPendingAfterMVCP = (_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0;
++ const didMVCPAdjust = !!checkMVCP && (state.scroll !== scrollBeforeMVCP || scrollAdjustPendingAfterMVCP !== scrollAdjustPendingBeforeMVCP);
++ const mvcp = state.props.maintainVisibleContentPosition;
++ const isUnanchoredDataChange = dataChanged && !mvcp.data;
++ const isCompensating = didMVCPAdjust || scrollAdjustPendingAfterMVCP !== 0 || !!state.pendingNativeMVCPAdjust;
++ if (!suppressInitialScrollSideEffects) {
++ settleScrollTarget(ctx, {
++ isCompensating,
++ minIndexSizeChanged: isUnanchoredDataChange ? 0 : minIndexSizeChangedThisPass
++ });
++ }
++ if (didMVCPAdjust) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+ }
+@@ -5985,6 +6131,8 @@ function resolveWindowScrollTarget({ clampedOffset, horizontal, listPos, scroll
+ // src/components/ListComponentScrollView.tsx
+ var SCROLLBAR_HIDDEN_STYLE_ID = "legend-list-scrollbar-axis-hidden-style";
+ var SCROLL_END_FALLBACK_MS = 200;
++var SCROLL_EXTENT_EPSILON = 1;
++var REACHABLE_RETRY_FRAMES = 30;
+ var SCROLLBAR_HIDDEN_STYLE = `.${LEGEND_LIST_SCROLLBAR_Y_HIDDEN_CLASS}::-webkit-scrollbar:vertical{width:0;display:none;}.${LEGEND_LIST_SCROLLBAR_X_HIDDEN_CLASS}::-webkit-scrollbar:horizontal{height:0;display:none;}`;
+ function ensureScrollbarHiddenStyle() {
+ if (typeof document === "undefined" || document.getElementById(SCROLLBAR_HIDDEN_STYLE_ID)) {
+@@ -6073,6 +6221,26 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ }
+ return horizontal ? scrollElement.scrollLeft : scrollElement.scrollTop;
+ }, [getMaxScrollOffset, horizontal, isWindowScroll]);
++ const reissueHandleRef = useRef(0);
++ const scrollUntilReachable = useCallback(
++ (offset, animated, run) => {
++ cancelAnimationFrame(reissueHandleRef.current);
++ let attempts = 0;
++ let previousMaxOffset = Number.NEGATIVE_INFINITY;
++ const attempt = () => {
++ const liveMaxOffset = getMaxScrollOffset();
++ run(clampOffset(offset, liveMaxOffset));
++ const isStillCommitting = liveMaxOffset > previousMaxOffset;
++ previousMaxOffset = liveMaxOffset;
++ if (!animated && isStillCommitting && Number.isFinite(offset) && offset - liveMaxOffset > SCROLL_EXTENT_EPSILON && ++attempts < REACHABLE_RETRY_FRAMES) {
++ reissueHandleRef.current = requestAnimationFrame(attempt);
++ }
++ };
++ attempt();
++ },
++ [getMaxScrollOffset]
++ );
++ useEffect(() => () => cancelAnimationFrame(reissueHandleRef.current), []);
+ const scrollToLocalOffset = useCallback(
+ (offset, animated) => {
+ const scrollElement = scrollRef.current;
+@@ -6080,29 +6248,34 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ if (!target || typeof target.scrollTo !== "function") {
+ return;
+ }
+- const maxOffset = getMaxScrollOffset();
+- const clampedOffset = clampOffset(offset, maxOffset);
+ const behavior = animated ? "smooth" : "auto";
+ const options = { behavior };
+ if (isWindowScroll) {
+ const scroll = getWindowScrollPosition();
+ const listPos = getElementDocumentPosition(scrollElement, scroll);
+ const { left, top } = resolveWindowScrollTarget({
+- clampedOffset,
++ clampedOffset: clampOffset(offset, getMaxScrollOffset()),
+ horizontal,
+ listPos,
+ scroll
+ });
+ options.left = left;
+ options.top = top;
+- } else if (horizontal) {
+- options.left = clampedOffset;
+- } else {
+- options.top = clampedOffset;
++ ctx.state.lastIssuedScrollOffset = clampOffset(offset, getMaxScrollOffset());
++ target.scrollTo(options);
++ return;
+ }
+- target.scrollTo(options);
++ scrollUntilReachable(offset, animated, (reachableOffset) => {
++ if (horizontal) {
++ options.left = reachableOffset;
++ } else {
++ options.top = reachableOffset;
++ }
++ ctx.state.lastIssuedScrollOffset = reachableOffset;
++ target.scrollTo(options);
++ });
+ },
+- [getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll]
++ [ctx, getMaxScrollOffset, getScrollTarget, horizontal, isWindowScroll, scrollUntilReachable]
+ );
+ useImperativeHandle(ref, () => {
+ const api = {
+@@ -6128,8 +6301,7 @@ var ListComponentScrollView = forwardRef(function ListComponentScrollView2({
+ },
+ scrollToEnd: (options = {}) => {
+ const { animated = true } = options;
+- const endOffset = getMaxScrollOffset();
+- scrollToLocalOffset(endOffset, animated);
++ scrollToLocalOffset(getMaxScrollOffset(), animated);
+ },
+ scrollToOffset: (params) => {
+ const { offset, animated = true } = params;
+@@ -6478,6 +6650,7 @@ function shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, nextHeaderSize
+ return props.maintainVisibleContentPosition.size && didContainersLayout && didFinishInitialScroll && !scrollingTo && scroll >= previousHeaderEnd - SCROLL_ADJUST_EPSILON && Math.abs(sizeDiff) > SCROLL_ADJUST_EPSILON;
+ }
+ function setHeaderSize(ctx, size) {
++ var _a3;
+ const { state } = ctx;
+ const previousHeaderSize = peek$(ctx, "headerSize") || 0;
+ const didChange = previousHeaderSize !== size;
+@@ -6487,6 +6660,8 @@ function setHeaderSize(ctx, size) {
+ updateContentMetricsState(ctx);
+ if (hasMeasuredOrEstimatedHeaderBaseline && shouldAdjustForHeaderSizeChange(ctx, previousHeaderSize, size)) {
+ requestAdjust(ctx, size - previousHeaderSize);
++ } else if ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.onHeaderLayout) {
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7549,13 +7724,14 @@ function getRenderedItem(ctx, key) {
+
+ // src/utils/normalizeMaintainScrollAtEnd.ts
+ function normalizeMaintainScrollAtEndOn(on, hasExplicitOn) {
+- var _a3, _b, _c, _d;
++ var _a3, _b, _c, _d, _e;
+ return {
+ animated: false,
+ onDataChange: hasExplicitOn ? (_a3 = on == null ? void 0 : on.dataChange) != null ? _a3 : false : true,
+ onFooterLayout: hasExplicitOn ? (_b = on == null ? void 0 : on.footerLayout) != null ? _b : false : true,
+- onItemLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.itemLayout) != null ? _c : false : true,
+- onLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.layout) != null ? _d : false : true
++ onHeaderLayout: hasExplicitOn ? (_c = on == null ? void 0 : on.headerLayout) != null ? _c : false : true,
++ onItemLayout: hasExplicitOn ? (_d = on == null ? void 0 : on.itemLayout) != null ? _d : false : true,
++ onLayout: hasExplicitOn ? (_e = on == null ? void 0 : on.layout) != null ? _e : false : true
+ };
+ }
+ function normalizeMaintainScrollAtEnd(value) {
+@@ -8267,6 +8443,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
+ onScrollBeginDrag: (event) => {
+ var _a4, _b2;
+ prepareReachedEdgeForNextUserScroll(ctx);
++ clearScrollTargetSettle(state);
+ (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
+ },
+ onScrollEnd: () => prepareReachedEdgeForNextUserScroll(ctx)
+diff --git a/node_modules/@legendapp/list/reanimated.d.ts b/node_modules/@legendapp/list/reanimated.d.ts
+index e504332..99337c1 100644
+--- a/node_modules/@legendapp/list/reanimated.d.ts
++++ b/node_modules/@legendapp/list/reanimated.d.ts
+@@ -489,6 +489,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
+diff --git a/node_modules/@legendapp/list/section-list.d.ts b/node_modules/@legendapp/list/section-list.d.ts
+index a790f7f..9a3c2ef 100644
+--- a/node_modules/@legendapp/list/section-list.d.ts
++++ b/node_modules/@legendapp/list/section-list.d.ts
+@@ -545,6 +545,12 @@ interface AlwaysRenderConfig {
+ interface MaintainScrollAtEndOnOptions {
+ dataChange?: boolean;
+ footerLayout?: boolean;
++ /**
++ * Keep the list pinned to the end when the header changes size. A header that measures larger
++ * than its estimate pushes every item down, which leaves a list that had already scrolled to
++ * its end short of it by the difference.
++ */
++ headerLayout?: boolean;
+ itemLayout?: boolean;
+ layout?: boolean;
+ }
diff --git a/shared/tests/e2e/electron/flows/chat-search-hit.test.ts b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts
new file mode 100644
index 000000000000..f44c27d4846a
--- /dev/null
+++ b/shared/tests/e2e/electron/flows/chat-search-hit.test.ts
@@ -0,0 +1,111 @@
+import {test, expect} from '@/tests/e2e/electron/helpers/fixtures'
+import {navigateToChat} from '@/tests/e2e/electron/helpers/navigate'
+import * as T from '@/tests/e2e/shared/test-ids'
+
+// Searching a thread has to land on the hit, and then leave the thread alone. Both were manual
+// checks until now: the list was landing with the hit far below the viewport, and after that was
+// fixed a thread the reader had scrolled away from could still pull itself back.
+//
+// One test rather than two, because the second half depends on the state the first leaves: the
+// search bar is open and a hit is selected, and re-entering that from scratch would just be the
+// first half again.
+// A row has to be visible by more than a hairline to count as landed on, matching the iOS flow.
+const MIN_VISIBLE_HEIGHT = 24
+
+test('lands on every search hit, then stays where the reader scrolls it', async ({page}) => {
+ test.setTimeout(120_000)
+ // Named, not "whichever row is first". The inbox is ordered by recency and the suites send
+ // messages of their own, so the first row is a different conversation from one run to the next —
+ // and with it the hit count and which words match at all.
+ const smokeUser = process.env['KB_SMOKE_USER']
+ expect(smokeUser, 'KB_SMOKE_USER is not set').toBeTruthy()
+ await navigateToChat(page)
+ const row = page
+ .getByTestId(T.CHAT_INBOX_ROW)
+ .filter({hasText: smokeUser!})
+ .first()
+ await row.click({timeout: 10_000})
+ await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 5_000})
+
+ // force: the conversation header sits in the window's WebkitAppRegion drag region, which makes
+ // playwright's actionability check wait forever on a control that is perfectly clickable.
+ await page.getByTestId(T.CHAT_HEADER_SEARCH_BUTTON).first().click({force: true})
+ await page.waitForTimeout(1_000)
+ // A word common enough to hit repeatedly in any conversation with history.
+ await page.keyboard.type('the')
+ await page.waitForTimeout(4_000)
+
+ // Enter steps to the next hit, wrapping around at the end — which is the case that used to land
+ // off screen, since wrapping jumps the furthest.
+ let checked = 0
+ for (let i = 0; i < 12; i++) {
+ await page.keyboard.press('Enter')
+ await page.waitForTimeout(900)
+ const hit = await page
+ .getByTestId(T.CHAT_SEARCH_HIT)
+ .first()
+ .boundingBox({timeout: 3_000})
+ .catch(() => null)
+ const list = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox()
+ if (!hit || !list) continue
+ checked++
+ // By more than a hairline: a row overlapping the viewport by a pixel has not "landed on the
+ // hit", and the iOS flow holds the same line.
+ const visibleHeight = Math.min(hit.y + hit.height, list.y + list.height) - Math.max(hit.y, list.y)
+ const onScreen = visibleHeight >= Math.min(hit.height, MIN_VISIBLE_HEIGHT)
+ expect(
+ onScreen,
+ `step ${i}: hit at y=${Math.round(hit.y)} h=${Math.round(hit.height)} is outside the list (y=${Math.round(list.y)} h=${Math.round(list.height)})`
+ ).toBe(true)
+ }
+ // Without this the loop above passes by never measuring anything.
+ expect(checked, 'no hit was ever measurable, so nothing was checked').toBeGreaterThan(0)
+
+ const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox()
+ expect(listBox).not.toBeNull()
+ await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2)
+ for (let i = 0; i < 10; i++) {
+ await page.mouse.wheel(0, -600)
+ await page.waitForTimeout(150)
+ }
+ await page.waitForTimeout(1_500)
+
+ const readHit = async () =>
+ page
+ .getByTestId(T.CHAT_SEARCH_HIT)
+ .first()
+ .boundingBox({timeout: 3_000})
+ .catch(() => null)
+
+ // Watch rather than look once: a re-centre lands whenever the rows scrolled past finish
+ // measuring, which is after the gesture rather than during it.
+ // The row is often scrolled clean out of the render window, so its position is not always
+ // readable. The top of the thread is, in every state — without a second reading like it, both
+ // branches below can be skipped and this half of the test asserts nothing at all.
+ const readThreadTop = async () =>
+ page
+ .getByTestId(T.CHAT_THREAD_TOP)
+ .first()
+ .boundingBox({timeout: 3_000})
+ .then(b => (b ? b.y : null))
+ .catch(() => null)
+
+ const settled = await readHit()
+ const settledTop = await readThreadTop()
+ await page.waitForTimeout(3_000)
+ const after = await readHit()
+ const afterTop = await readThreadTop()
+
+ if (settled && after) {
+ const moved = Math.abs(after.y - settled.y)
+ expect(moved, `the thread scrolled itself ${Math.round(moved)}px back toward the hit`).toBeLessThanOrEqual(8)
+ } else if (!settled && after) {
+ // Scrolled far enough to unmount the row, and then it came back — which only a scroll does.
+ throw new Error('the hit came back into the render window after being scrolled away from')
+ } else if (settledTop !== null && afterTop !== null) {
+ const moved = Math.abs(afterTop - settledTop)
+ expect(moved, `the thread moved ${Math.round(moved)}px on its own after the drag`).toBeLessThanOrEqual(8)
+ } else {
+ throw new Error('neither the hit nor the top of the thread could be measured, so nothing was checked')
+ }
+})
diff --git a/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts
new file mode 100644
index 000000000000..667eab4b2d25
--- /dev/null
+++ b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts
@@ -0,0 +1,95 @@
+import type {Page} from '@playwright/test'
+import {test, expect} from '@/tests/e2e/electron/helpers/fixtures'
+import {navigateToChat} from '@/tests/e2e/electron/helpers/navigate'
+import * as T from '@/tests/e2e/shared/test-ids'
+
+// Opening a conversation has to leave the reader on its newest message. It stopped doing that for
+// threads whose rows grow a frame or two after the list has already landed - a link preview
+// committing, an image measuring, the full thread response replacing the cached one. The list
+// anchored to the end it could see, the content then grew past it, and the thread sat hundreds of
+// pixels above the newest message. Measured in the app: it landed on an extent of 6891, the content
+// committed 7224, and the thread stayed 432px short of the end.
+//
+// Image responses are held back so that growth lands after the initial scroll rather than before it,
+// which is what makes this able to fail rather than passing on whatever the disk cache happened to
+// have warm.
+const IMAGE_DELAY_MS = 700
+// A couple of pixels of sub-pixel residue is fine; anything more is a reader looking at old
+// messages with the newest one off screen.
+const MAX_DISTANCE_FROM_END = 8
+// Threads shorter than their viewport cannot be short of their end, so they prove nothing.
+const MIN_SCROLLABLE_OVERFLOW = 200
+
+type ListMetrics = {clientHeight: number; distanceFromEnd: number; scrollHeight: number} | null
+
+// The scroller is the list element LegendList renders inside the wrapper that carries the testID.
+// Reached through globalThis because this suite's tsconfig has no DOM lib, the same way the app's
+// desktop-only code does it.
+const readListMetrics = async (page: Page): Promise =>
+ page.evaluate((testID: string) => {
+ type Scroller = {clientHeight: number; scrollHeight: number; scrollTop: number}
+ const doc = (
+ globalThis as unknown as {
+ document?: {querySelector: (selector: string) => {firstElementChild?: Scroller | null} | null}
+ }
+ ).document
+ const scroller = doc?.querySelector(`[data-testid="${testID}"]`)?.firstElementChild
+ if (!scroller) return null
+ return {
+ clientHeight: Math.round(scroller.clientHeight),
+ distanceFromEnd: Math.round(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop),
+ scrollHeight: Math.round(scroller.scrollHeight),
+ }
+ }, T.CHAT_MESSAGE_LIST)
+
+// No retries. The growth this depends on lands a frame or two after the list does, so it is timing
+// dependent by nature - and a retry that passes hides the regression the flow exists for. Verified:
+// with the library fix disabled the first attempt failed and the retry passed.
+test.describe.configure({retries: 0})
+
+test('opens every conversation on its newest message', async ({page}) => {
+ test.setTimeout(120_000)
+ await navigateToChat(page)
+
+ // Routing every request so the handler can pick the images out; everything else continues
+ // untouched. Removed at the end because the page is shared with the rest of the suite.
+ await page.route('**/*', async route => {
+ if (route.request().resourceType() === 'image') {
+ await new Promise(resolve => setTimeout(resolve, IMAGE_DELAY_MS))
+ }
+ await route.continue()
+ })
+
+ const checked: string[] = []
+ try {
+ // Team channels as well as one-to-ones: the threads with enough history to grow after landing
+ // are mostly team channels, and the flow that found this bug was clicking through them.
+ const rows = page.locator(
+ `[data-testid="${T.CHAT_INBOX_CHANNEL_ROW}"], [data-testid="${T.CHAT_INBOX_ROW}"]`
+ )
+ const rowCount = Math.min(await rows.count(), 25)
+ for (let i = 0; i < rowCount && checked.length < 5; i++) {
+ const row = rows.nth(i)
+ const name = (await row.innerText().catch(() => '')).split('\n')[0] ?? `row ${i}`
+ await row.click({force: true, timeout: 10_000}).catch(() => {})
+ await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000})
+ // Long enough for the delayed images to commit and for anything following the end to react.
+ await page.waitForTimeout(IMAGE_DELAY_MS + 2_500)
+
+ const metrics = await readListMetrics(page)
+ if (!metrics || metrics.scrollHeight - metrics.clientHeight < MIN_SCROLLABLE_OVERFLOW) continue
+ checked.push(name)
+ expect(
+ metrics.distanceFromEnd,
+ `${name}: thread settled ${metrics.distanceFromEnd}px above its newest message (content ${metrics.scrollHeight}, viewport ${metrics.clientHeight})`
+ ).toBeLessThanOrEqual(MAX_DISTANCE_FROM_END)
+ }
+ } finally {
+ await page.unroute('**/*')
+ }
+
+ // Without this the loop above passes by never measuring a thread long enough to fail.
+ expect(checked.length, 'no conversation had more content than its viewport, so nothing was checked').toBeGreaterThan(
+ 0
+ )
+})
diff --git a/shared/tests/e2e/ios-appium/all.test.ts b/shared/tests/e2e/ios-appium/all.test.ts
index d5895c7e9be7..a6af9d957d56 100644
--- a/shared/tests/e2e/ios-appium/all.test.ts
+++ b/shared/tests/e2e/ios-appium/all.test.ts
@@ -4,6 +4,7 @@
// each) and the app stays warm between flows.
import './flows/android-activity-restart.test'
import './flows/chat-conversation.test'
+import './flows/chat-search-hit.test'
import './flows/chat-send-message.test'
import './flows/crypto-outputs.test'
import './flows/crypto-subtabs.test'
diff --git a/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts
new file mode 100644
index 000000000000..4bd01e683f19
--- /dev/null
+++ b/shared/tests/e2e/ios-appium/flows/chat-search-hit.test.ts
@@ -0,0 +1,385 @@
+import type {ChainablePromiseElement} from 'webdriverio'
+import {expect} from '@wdio/globals'
+import {requireSmokeUser} from '../helpers/app'
+import {anyExist, byTextWithin, el, tab, waitForTestID, enterText} from '../helpers/elements'
+import {dismissKeyboard, escapeToTabs} from '../helpers/navigate'
+import * as T from '../../shared/test-ids'
+
+// A word common enough to match throughout the thread, so hits span messages of different heights.
+const QUERY = 'one'
+// A word whose hit sits among the messages already on screen: jumping a few rows is the case where
+// the list has nothing to load and the scroll lands against a content size that has not caught up.
+// Taken from the messages this suite itself sends ("e2e-test-"), so it is both present
+// and recent in whatever conversation the run is pointed at.
+const SAME_SCREEN_QUERY = 'test'
+// Flings back through the thread until a page of older messages arrives.
+const MAX_FLINGS = 20
+// Drags needed to move a hit clear of the viewport. One drag moves about a third of a screen, and
+// the hit starts centred, so this is "enough to be sure" rather than a tuned number.
+const MAX_DRAGS_AWAY = 6
+// A row has to be visible by more than a hairline to count as landed on.
+const MIN_VISIBLE_HEIGHT = 24
+// How far the top of the thread has to jump away from the viewport to be a page of older messages
+// arriving rather than the fling that provoked it.
+const PREPEND_MIN_SHIFT = 600
+// The thread is watched for this long after the drag, in samples, rather than looked at once at the
+// end: a jump back to the hit can be brief.
+const SNAP_BACK_SAMPLES = 8
+const SNAP_BACK_SAMPLE_MS = 400
+// How far the hit row may travel back toward the viewport after the drag before that is the thread
+// scrolling itself rather than settling. maintainVisibleContentPosition holds the visible content
+// in place across the page-in, so a row that marches back by this much moved because something
+// scrolled the list.
+const SNAP_BACK_TOLERANCE = 150
+
+// iOS puts the conversation's header actions in a native overflow menu, so there is no React view
+// to carry a testID - the bar button and its menu item are addressed by the accessibility labels
+// the platform exposes, scoped to the navigation bar and the menu so they cannot match the "More"
+// tab or the inbox's own search field. Other platforms render the search control directly.
+const openThreadSearch = async () => {
+ if (browser.isIOS) {
+ const moreButton = browser.$('-ios class chain:**/XCUIElementTypeNavigationBar/**/XCUIElementTypeButton[`name == "More"`]')
+ await moreButton.waitForExist({timeout: 5000, timeoutMsg: 'header overflow menu never appeared'})
+ await moreButton.click()
+ const searchItem = browser.$(
+ '-ios predicate string:(type == "XCUIElementTypeMenuItem" OR type == "XCUIElementTypeButton") AND name == "Search"'
+ )
+ await searchItem.waitForExist({timeout: 5000, timeoutMsg: 'search menu item never appeared'})
+ await searchItem.click()
+ return
+ }
+ await waitForTestID(T.CHAT_HEADER_SEARCH_BUTTON, 5000)
+ await el(T.CHAT_HEADER_SEARCH_BUTTON).click()
+}
+
+type Bounds = {height: number; width: number; x: number; y: number}
+
+const boundsOfElement = async (element: ChainablePromiseElement): Promise => {
+ const [location, size] = await Promise.all([element.getLocation(), element.getSize()])
+ return {height: size.height, width: size.width, x: location.x, y: location.y}
+}
+
+const boundsOf = async (id: string): Promise => boundsOfElement(el(id))
+
+// A row outside the render window is not in the accessibility tree at all, and that is an answer
+// ("not on screen") rather than an error. Anything else - a dead session, a driver fault - is not,
+// so only a missing element is swallowed here.
+const maybeBoundsOf = async (element: ChainablePromiseElement): Promise => {
+ if (!(await element.isExisting())) return undefined
+ try {
+ return await boundsOfElement(element)
+ } catch (error) {
+ if (/no such element|stale element|not found/i.test(String(error))) return undefined
+ throw error
+ }
+}
+
+// What the reader can actually see of the thread: the list's frame less the search bar, which
+// overlays the bottom of it rather than shrinking it.
+const visibleThreadBounds = async (): Promise => {
+ const list = await boundsOf(T.CHAT_MESSAGE_LIST)
+ const bar = await maybeBoundsOf(el(T.CHAT_THREAD_SEARCH_CANCEL))
+ if (!bar) return list
+ return {...list, height: Math.max(0, bar.y - list.y)}
+}
+
+const visibleHeight = (thing: Bounds, viewport: Bounds): number =>
+ Math.min(thing.y + thing.height, viewport.y + viewport.height) - Math.max(thing.y, viewport.y)
+
+// The row keeps its marker while it is the selected hit, but a virtualised list renders rows
+// outside the viewport too - so the marker existing says nothing about whether it can be seen.
+// Compare where the row is against where the thread is, and require more than a sliver.
+const hitOnScreen = async (): Promise => {
+ const hit = await maybeBoundsOf(el(T.CHAT_SEARCH_HIT))
+ if (!hit) return undefined
+ const viewport = await visibleThreadBounds()
+ return visibleHeight(hit, viewport) >= Math.min(hit.height, MIN_VISIBLE_HEIGHT) ? hit : undefined
+}
+
+const describeHit = async (): Promise => {
+ const hit = await maybeBoundsOf(el(T.CHAT_SEARCH_HIT))
+ const viewport = await visibleThreadBounds()
+ if (!hit) return `no highlighted row is rendered; thread ${viewport.y}..${viewport.y + viewport.height}`
+ return `row ${hit.y}..${hit.y + hit.height}, thread ${viewport.y}..${viewport.y + viewport.height}`
+}
+
+// The header above the oldest loaded message. It stays mounted while off screen, so its position is
+// readable throughout - and a page-in is visible in that position directly: a fling moves it toward
+// the viewport, while a prepend pushes it a page's worth further away.
+const topOfThreadPosition = async (): Promise => (await maybeBoundsOf(el(T.CHAT_THREAD_TOP)))?.y
+
+// "3 of 18" in the search bar. The count is what makes the wrap-around case honest: stepping a
+// fixed number of times proves nothing if the thread happens to have more hits than that. Read
+// through the bar's own testID — matching " of " across the screen finds message text first, and
+// the thread behind the bar is full of it.
+//
+// undefined means the counter could not be read, which is a different thing from a search with no
+// results, and the caller says so differently.
+const readHitCount = async (): Promise => {
+ const wrapper = el(T.CHAT_THREAD_SEARCH_COUNT)
+ const inner = browser.isAndroid
+ ? wrapper.$('.//android.widget.TextView')
+ : wrapper.$('-ios class chain:**/XCUIElementTypeStaticText')
+ const label = (await inner.getText().catch(() => '')) || (await wrapper.getText().catch(() => ''))
+ if (/no results/i.test(label)) return 0
+ const parsed = /of (\d+)/.exec(label)
+ if (!parsed) {
+ console.log(`readHitCount: could not read the counter, saw "${label}" at ${new Date().toISOString()}`)
+ return undefined
+ }
+ return Number(parsed[1])
+}
+
+// Results stream in, and the counter renders as soon as the first ones land - so a count read too
+// early is a partial count, and stepping by it would quietly stop exercising the wrap-around.
+// Settled means two reads in a row agree.
+const readSettledHitCount = async (): Promise => {
+ let previous = await readHitCount()
+ for (let attempt = 0; attempt < 10; attempt++) {
+ await browser.pause(500)
+ const next = await readHitCount()
+ if (next !== undefined && next === previous) return next
+ previous = next
+ }
+ return previous
+}
+
+const startSearch = async (query: string): Promise => {
+ await openThreadSearch()
+ await waitForTestID(T.CHAT_THREAD_SEARCH_INPUT, 5000)
+ // The field focuses itself a beat after mounting, so type into it rather than sending keys at
+ // whatever happens to be focused. enterText also pastes where per-key injection is unsafe.
+ await enterText(T.CHAT_THREAD_SEARCH_INPUT, query)
+ await browser.keys(['\n'])
+
+ // Results stream in from the server; the first hit is selected once they arrive.
+ await waitForTestID(T.CHAT_SEARCH_HIT, 15000)
+ const landed = await browser
+ .waitUntil(async () => (await hitOnScreen()) !== undefined, {timeout: 5000})
+ .then(() => true)
+ .catch(() => false)
+ if (!landed) throw new Error(`the first hit never came on screen: ${await describeHit()}`)
+ const hits = await readSettledHitCount()
+ if (hits === undefined) throw new Error(`could not read the hit counter while searching "${query}"`)
+ if (hits === 0) throw new Error(`"${query}" found no hits in this conversation`)
+ return hits
+}
+
+// Walking the whole ring is what this used to do, and its cost grows with the conversation: once
+// the thread has enough hits the walk runs past the suite's per-test budget, and the timeout is
+// reported as "the hit drifted off screen" rather than as this having run out of time. The wrap is
+// forced directly now, and the walk after it is bounded.
+const MAX_HIT_STEPS = 12
+
+const stepThroughHits = async (steps: number, control: string = T.CHAT_THREAD_SEARCH_PREV) => {
+ for (let step = 0; step < steps; step++) {
+ await el(control).click()
+
+ // Give the jump, and the measurements that follow it, time to land.
+ const landed = await browser
+ .waitUntil(async () => (await hitOnScreen()) !== undefined, {timeout: 5000})
+ .then(() => true)
+ .catch(() => false)
+ if (!landed) throw new Error(`hit ${step + 1} never came on screen: ${await describeHit()}`)
+ // A hit that lands and then drifts off screen is the other half of what this watches for.
+ await browser.pause(700)
+ if (!(await hitOnScreen())) {
+ throw new Error(`hit ${step + 1} landed and then drifted off screen: ${await describeHit()}`)
+ }
+ }
+}
+
+const closeThreadSearch = async () => {
+ await el(T.CHAT_THREAD_SEARCH_CANCEL).click()
+ await browser.pause(500)
+}
+
+// Gestures are measured from the thread itself: on a tablet the inbox sits beside it, so a fixed x
+// would scroll the wrong list, and the search bar and keyboard cover part of the thread's frame.
+const gesturePoints = async (distance: number) => {
+ const viewport = await visibleThreadBounds()
+ const x = Math.round(viewport.x + viewport.width / 2)
+ const middle = viewport.y + viewport.height / 2
+ const half = Math.min(distance, viewport.height - 80) / 2
+ return {from: Math.round(middle - half), to: Math.round(middle + half), x}
+}
+
+// A fast flick that coasts - used only to travel back through the thread, never to establish the
+// position an assertion depends on.
+const flingThread = async () => {
+ const {from, to, x} = await gesturePoints(420)
+ await browser.action('pointer').move({x, y: from}).down().move({duration: 120, x, y: to}).up().perform()
+}
+
+// A drag that ends where it is left rather than coasting.
+const dragThread = async () => {
+ const {from, to, x} = await gesturePoints(200)
+ await browser
+ .action('pointer')
+ .move({x, y: from})
+ .down()
+ .pause(100)
+ .move({duration: 400, x, y: to})
+ .pause(100)
+ .up()
+ .perform()
+}
+
+// Drag until the hit is off screen, which is the state the rest of the case depends on. Failing here
+// is a real failure: with the thread re-centring itself on the hit, this is where that shows up.
+const dragUntilHitLeaves = async () => {
+ for (let attempt = 0; attempt < MAX_DRAGS_AWAY; attempt++) {
+ await dragThread()
+ await browser.pause(400)
+ if (!(await hitOnScreen())) return
+ }
+ throw new Error(
+ `the hit never left the viewport after ${MAX_DRAGS_AWAY} drags: ${await describeHit()}`
+ )
+}
+
+// Each test starts from the tab root: the suite returns there between tests, so a flow cannot
+// assume the conversation another one left open. The conversation needs enough history to page in
+// and enough matches for both queries, which is the smoke account's own chat with itself.
+// Named, not "whichever row is first". The inbox is ordered by recency and this suite sends
+// messages of its own, so the first row is a different conversation from one run to the next — and
+// then so are the hit count and which words match at all. That drift reads as this flow failing.
+//
+// The smoke user's own chat is the stable choice: it always exists, its name is the username the
+// run was given, and it has the history these cases need.
+const openSearchConversation = async (): Promise => {
+ const smokeUser = requireSmokeUser()
+ await escapeToTabs()
+ await tab('Teams').click()
+ await tab('Chat').click()
+ await waitForTestID(T.CHAT_INBOX_LIST, 5000)
+
+ if (!(await anyExist(T.CHAT_INBOX_ROW))) return false
+ // Scoped to the inbox: the signed-in username is also on the header avatar above it, and tapping
+ // that opens the account switcher.
+ const row = byTextWithin(el(T.CHAT_INBOX_LIST), smokeUser)
+ if (!(await row.isExisting())) {
+ throw new Error(`no conversation named "${smokeUser}" in the inbox`)
+ }
+ await row.click()
+ await waitForTestID(T.CHAT_MESSAGE_LIST, 5000)
+ await dismissKeyboard()
+ return true
+}
+
+describe('chat thread search', function () {
+ // Stated rather than inherited: a re-run cannot tell a slow sim from a thread that scrolled
+ // itself, so the failures this flow exists to catch are exactly the ones a retry would paper
+ // over. If the suite default ever goes back to retrying, this flow still must not.
+ this.retries(0)
+
+ it('keeps every hit it lands on visible, including wrapping around', async () => {
+ requireSmokeUser()
+ if (!(await openSearchConversation())) throw new Error('no conversations in the inbox')
+
+ const hits = await startSearch(QUERY)
+ // Next from the hit the search lands on wraps straight to the far end of the thread, which is
+ // the longest jump the list is ever asked to make - and the case that used to leave the hit off
+ // screen. Forced first rather than reached by walking the whole ring to it.
+ await stepThroughHits(1, T.CHAT_THREAD_SEARCH_NEXT)
+ const walk = Math.min(hits + 2, MAX_HIT_STEPS)
+ if (walk < hits + 2) {
+ // Said out loud: a bound nobody can see reads as "every hit was checked" when it was not.
+ console.log(`stepping ${walk} of ${hits + 2} hits — the rest costs more than the test budget`)
+ }
+ await stepThroughHits(walk)
+ await closeThreadSearch()
+ })
+
+ it('lands on a hit that is already on screen', async () => {
+ requireSmokeUser()
+ if (!(await openSearchConversation())) throw new Error('no conversations in the inbox')
+
+ // No native mutation has been found that makes this case fail on its own - it is here because
+ // it is the case people report on desktop, where it does fail. Treat a green here as coverage
+ // of the flow, not proof of the fix.
+ await startSearch(SAME_SCREEN_QUERY)
+ await stepThroughHits(1)
+ await closeThreadSearch()
+ })
+
+ it('leaves the thread where the user drags it after a hit', async () => {
+ requireSmokeUser()
+ if (!(await openSearchConversation())) throw new Error('no conversations in the inbox')
+ await startSearch(SAME_SCREEN_QUERY)
+
+ // A moment after landing is when the list is still measuring, and where anything holding the
+ // scroll target used to pull the thread back out from under the user.
+ await browser.pause(1000)
+ expect(await hitOnScreen()).toBeDefined()
+
+ // The reader moves away from the hit. Dragged until the row is genuinely gone rather than a
+ // fixed number of times: how far one drag carries a hit depends on the row's height and the
+ // screen's, and a hit still clinging to the bottom edge is not the state this test is about.
+ await dragUntilHitLeaves()
+
+ // ...and keeps reading back through older messages, which is what asks the thread for another
+ // page. The order matters: the prepend has to arrive *after* the reader has moved, because the
+ // reported failure is "search, wait, scroll, and it pops back". Every index shifts when the page
+ // lands, and an index-keyed re-centre reads that as a new target.
+ let pagedIn = false
+ let previousTop = await topOfThreadPosition()
+ for (let fling = 0; fling < MAX_FLINGS && !pagedIn; fling++) {
+ await flingThread()
+ await browser.pause(200)
+ // At no point during this should the thread take itself back to the hit.
+ const returned = await hitOnScreen()
+ if (returned) throw new Error(`the thread scrolled back to the hit while reading: ${await describeHit()}`)
+
+ const top = await topOfThreadPosition()
+ // A fling moves the top of the thread toward the viewport; only a prepend moves it away, and
+ // by a page's worth rather than a gesture's.
+ if (top !== undefined && previousTop !== undefined && top < previousTop - PREPEND_MIN_SHIFT) {
+ console.log(`page-in: top of thread moved ${previousTop} -> ${top}`)
+ pagedIn = true
+ }
+ previousTop = top ?? previousTop
+ }
+ // Not provoking a page-in proves nothing about snapping back, so fail instead of passing.
+ if (!pagedIn) throw new Error('flinging never loaded another page of older messages')
+
+ // Settle where the reader left it, and watch rather than look once: a re-centre lands whenever
+ // the page finishes measuring, and it does not necessarily stay.
+ await dragUntilHitLeaves()
+ // Undefined means the drag pushed the row clean out of the render window, which is the usual
+ // outcome — the drags keep going until it is off screen, and off screen far enough is unmounted.
+ // The samples below handle both cases rather than skipping the check in one of them.
+ const restingPosition = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y
+
+ let snappedBack: string | undefined
+ for (let sample = 0; sample < SNAP_BACK_SAMPLES && !snappedBack; sample++) {
+ await browser.pause(SNAP_BACK_SAMPLE_MS)
+ const visible = await hitOnScreen()
+ if (visible) {
+ snappedBack = `the hit is back on screen at ${visible.y}`
+ break
+ }
+ // Whether the hit is *visible* depends on the search bar and the keyboard; whether the thread
+ // travelled back toward it does not. maintainVisibleContentPosition holds the visible content
+ // in place across a page-in, so a row that marches back moved because something scrolled.
+ const position = (await maybeBoundsOf(el(T.CHAT_SEARCH_HIT)))?.y
+ if (restingPosition === undefined) {
+ // It was outside the render window when the reader stopped dragging. Coming back into it is
+ // itself the movement this is watching for: a prepend does not carry a row toward the
+ // viewport, so something scrolled to bring it back within drawDistance of it.
+ if (position !== undefined) {
+ snappedBack = `the hit came back into the render window at ${position} after being dragged out of it`
+ }
+ } else if (position !== undefined) {
+ const travelled = Math.abs(position - restingPosition)
+ if (travelled > SNAP_BACK_TOLERANCE) {
+ snappedBack = `the hit moved ${Math.round(travelled)} back toward the viewport (${restingPosition} -> ${position})`
+ }
+ }
+ }
+ if (snappedBack) console.log(`thread scrolled itself after the drag: ${snappedBack}`)
+ expect(snappedBack).toBeUndefined()
+
+ await closeThreadSearch()
+ })
+})
diff --git a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts
index 59d0b9d1efc1..dc317b4a2673 100644
--- a/shared/tests/e2e/ios-appium/flows/people-profile.test.ts
+++ b/shared/tests/e2e/ios-appium/flows/people-profile.test.ts
@@ -1,7 +1,7 @@
import {expect} from '@wdio/globals'
import {requireSmokeUser} from '../helpers/app'
import {escapeToTabs, navigateToPeople} from '../helpers/navigate'
-import {byText, el, waitForTestID} from '../helpers/elements'
+import {byTextWithin, el, waitForTestID} from '../helpers/elements'
import * as T from '../../shared/test-ids'
describe('people profile', () => {
@@ -13,8 +13,26 @@ describe('people profile', () => {
// Your own username appearing in your own feed is genuinely conditional
// (the feed surfaces others' activity), so guard rather than hard-wait.
- const userEl = byText(smokeUser)
- if (!(await userEl.isExisting())) return
+ //
+ // Scoped to the feed, not matched across the screen: the People header's avatar carries the
+ // username too, and tapping that opens the account switcher rather than a profile - which then
+ // fails here on a missing profile page, and leaves a modal up for whatever runs next.
+ // The feed container mounts empty and immediately, so waiting on it says nothing about whether
+ // the feed has arrived. Wait for the row itself instead - a bounded wait rather than the retries
+ // this flow used to carry, which re-ran the whole test to buy the same time.
+ //
+ // Rebuilt on every poll: a scoped element caches its parent's id, so a feed that re-renders
+ // makes the scoped lookup throw stale forever, and the swallowed error reads as "not there".
+ const findUser = () => byTextWithin(el(T.PEOPLE_FEED), smokeUser)
+ const present = await browser
+ .waitUntil(async () => findUser().isExisting().catch(() => false), {interval: 250, timeout: 10000})
+ .then(() => true)
+ .catch(() => false)
+ if (!present) {
+ console.log(`people profile: ${smokeUser} is not in its own feed, skipping the profile open`)
+ return
+ }
+ const userEl = findUser()
await userEl.click()
await waitForTestID(T.PROFILE_PAGE, 10000)
await expect(el(T.PROFILE_PAGE)).toExist()
diff --git a/shared/tests/e2e/ios-appium/helpers/elements.ts b/shared/tests/e2e/ios-appium/helpers/elements.ts
index 311ab79b2399..372c87406062 100644
--- a/shared/tests/e2e/ios-appium/helpers/elements.ts
+++ b/shared/tests/e2e/ios-appium/helpers/elements.ts
@@ -146,6 +146,13 @@ export const anyExist = async (id: string, timeout = 3000): Promise =>
// Backslashes and double quotes would otherwise terminate/alter the quoted
// predicate literal and make the selector invalid.
+// An XPath string literal for arbitrary text. XPath 1.0 cannot escape a quote inside a literal, so
+// text containing one has to be assembled with concat().
+const xpathLiteral = (s: string): string => {
+ if (!s.includes('"')) return `"${s}"`
+ return `concat(${s.split('"').map(part => `"${part}"`).join(`, '"', `)})`
+}
+
const escapePredicate = (s: string) => s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
// CONTAINS, not ==, on purpose: many tappable rows (More menu items, team tabs)
@@ -164,6 +171,22 @@ export const byText = (text: string): ChainablePromiseElement => {
return browser.$(`-ios predicate string:label CONTAINS "${t}" OR name CONTAINS "${t}"`)
}
+// byText, scoped to a subtree, with the same platform split. Use it when the text you mean also
+// appears in the chrome around the content: the People header's avatar carries the signed-in
+// username, and tapping that opens the account switcher rather than a profile.
+export const byTextWithin = (root: ChainablePromiseElement, text: string): ChainablePromiseElement => {
+ if (browser.isAndroid) {
+ // XPath 1.0 has no escape for a quote, so a literal containing one has to be built with
+ // concat(). escapePredicate is for ObjC predicates and would emit a backslash XPath cannot read.
+ const literal = xpathLiteral(text)
+ return root.$(
+ `descendant-or-self::*[contains(@text, ${literal}) or contains(@content-desc, ${literal})]`
+ )
+ }
+ const t = escapePredicate(text)
+ return root.$(`-ios predicate string:label CONTAINS "${t}" OR name CONTAINS "${t}"`)
+}
+
// Tab-bar buttons. iOS exposes the native UITabBarItem by its title as the
// accessibility id. Android's tab bar is a native Material BottomNavigationView:
// target the ITEM view via its content-desc — the label, plus an optional badge
diff --git a/shared/tests/e2e/ios-appium/helpers/navigate.ts b/shared/tests/e2e/ios-appium/helpers/navigate.ts
index 2733c1b725e2..f1a267c7e368 100644
--- a/shared/tests/e2e/ios-appium/helpers/navigate.ts
+++ b/shared/tests/e2e/ios-appium/helpers/navigate.ts
@@ -85,10 +85,88 @@ async function atTabs(): Promise {
export async function dismissKeyboard(): Promise {
// isKeyboardShown is a direct Appium endpoint (fast) — avoid an
// //XCUIElementTypeKeyboard xpath, which is a slow full-tree search per call.
- if (await browser.isKeyboardShown().catch(() => false)) {
- if (browser.isAndroid) await browser.hideKeyboard().catch(() => {})
- else await browser.execute('mobile: hideKeyboard').catch(() => {})
+ if (!(await browser.isKeyboardShown().catch(() => false))) return
+ if (browser.isAndroid) {
+ await browser.hideKeyboard().catch(() => {})
+ return
+ }
+ await browser.execute('mobile: hideKeyboard').catch(() => {})
+ if (!(await browser.isKeyboardShown().catch(() => false))) return
+
+ // Naming the keys to press gets WDA past "Did not know how to dismiss the keyboard" on the
+ // screens whose keyboard carries one of them (search fields show Search, the team wizard's
+ // shows Done).
+ await browser.execute('mobile: hideKeyboard', {keys: ['Done', 'Search', 'Go', 'Return']}).catch(() => {})
+ if (!(await browser.isKeyboardShown().catch(() => false))) return
+
+ // WDA answers "Did not know how to dismiss the keyboard" for the chat composer — it has no Done
+ // key and no accessory to press. This is not cosmetic: while the keyboard is up the screen's own
+ // controls stop reporting as hittable, so the back chevron is invisible to tapNavBack and the
+ // left-edge pop does not take, and escapeToTabs burns its whole budget on a conversation it
+ // cannot leave. Every flow after it then starts from the wrong screen.
+ //
+ // Dismiss it with a drag rather than a tap. The chat list sets keyboardDismissMode="on-drag", so
+ // a drag is the gesture it listens for; it also sets keyboardShouldPersistTaps="handled", which
+ // means a tap landing on a row is handled BY that row and does not dismiss the keyboard — while
+ // still doing whatever the row does (opening an attachment, following a link). A drag cannot
+ // activate a touchable, so it has no such side effect on any screen this runs from.
+ const {height, width} = await browser.getWindowRect()
+ const x = Math.round(width / 2)
+ const keyboardGone = async () => !(await browser.isKeyboardShown().catch(() => false))
+ // Start well above the keyboard: on an iPad in landscape its top edge sits around 55% of the
+ // screen, and an accessory or autocorrect bar raises it further.
+ await browser
+ .action('pointer')
+ .move({x, y: Math.round(height * 0.35)})
+ .down()
+ .pause(60)
+ .move({duration: 250, x, y: Math.round(height * 0.15)})
+ .up()
+ .perform()
+ .catch(() => {})
+ if (
+ await browser
+ .waitUntil(keyboardGone, {interval: 100, timeout: 2000})
+ .then(() => true)
+ .catch(() => false)
+ ) {
+ return
}
+
+ // The drag is what the CHAT list listens for - it is the one screen that sets
+ // keyboardDismissMode="on-drag", and the one where a tap is unsafe because
+ // keyboardShouldPersistTaps="handled" lets a row swallow it and act on it. Everywhere else
+ // (feedback, crypto) there is no dismiss-on-drag and the default persist-taps is "never", so a
+ // tap is both safe and the only thing that works. Try it when this is not the chat list.
+ const onChatList = await el(T.CHAT_MESSAGE_LIST)
+ .isExisting()
+ .catch(() => false)
+ if (!onChatList) {
+ // 30%, the same place the tap used to land before this became a drag: it is above the keyboard
+ // on every screen this runs from and below their headers and inputs.
+ await browser
+ .action('pointer')
+ .move({x, y: Math.round(height * 0.3)})
+ .down()
+ .pause(60)
+ .up()
+ .perform()
+ .catch(() => {})
+ if (
+ await browser
+ .waitUntil(keyboardGone, {interval: 100, timeout: 2000})
+ .then(() => true)
+ .catch(() => false)
+ ) {
+ return
+ }
+ }
+
+ // Say so rather than leaving escapeToTabs to spend its whole budget on a screen whose controls
+ // are not hittable - a 50s stall with nothing in the log to explain it.
+ console.log(
+ `dismissKeyboard: keyboard still up after drag${onChatList ? '' : ' and tap'} at ${new Date().toISOString()}`
+ )
}
// Tap the leading (leftmost) button of a native NavigationBar — the back
@@ -130,6 +208,16 @@ async function tapNavBack(requireLeftEdge = false): Promise {
// element type — cheaper than three separate searches.
// visible == 1: hidden nav-stack screens and keyboard toolbars can carry their
// own Done/Close/Cancel — clicking one is a silent no-op that loops forever.
+// The pre-loop's own predicate: an EXACT name, unlike DISMISS_PRED's substring match. This one
+// clicks unattended before every test, so it must never match a control that merely contains the
+// word — a "Close team" or "Cancel invite" shipped later would otherwise become a destructive click
+// in the reset. Buttons and menu items only: a StaticText matching by name would also match a chat
+// message whose whole body is "Cancel", and the reset runs with the suite parked in a thread. The
+// one StaticText that did need dismissing — the thread search bar's Cancel, a Kb.Text with an
+// onClick — is closed by its own testID above instead.
+const MODAL_DISMISS_PRED =
+ '-ios predicate string:(type == "XCUIElementTypeButton" OR type == "XCUIElementTypeMenuItem") AND (name == "Done" OR name == "Close" OR name == "Cancel" OR label == "Done" OR label == "Close" OR label == "Cancel") AND visible == 1'
+
const DISMISS_PRED =
'-ios predicate string:(label CONTAINS "Done" OR name CONTAINS "Done" OR label CONTAINS "Close" OR name CONTAINS "Close" OR label CONTAINS "Cancel" OR name CONTAINS "Cancel") AND visible == 1'
@@ -177,6 +265,43 @@ export async function escapeToTabs(): Promise {
}
throw new Error('escapeToTabs(android): root tab bar not reached after 12 attempts')
}
+ // Dismiss anything presented over the tabs BEFORE asking whether we are at the root. A modal
+ // leaves the tab bar — and the whole screen behind it — in the accessibility tree, so atTabs reads
+ // "already home" while a New chat or account-switcher modal is still up; the reset then returns
+ // with it still there, the next flow taps rows belonging to the modal, and every test after it
+ // fails somewhere unrelated. It outlives the run too: the app restores its last screen, so a
+ // leaked modal wedges the NEXT run from its first test. Bounded, and only ever clicks a control
+ // that is on screen — at a real root there is nothing to click and this costs one query.
+ // The thread search bar first, by its own testID. It is a Kb.Text with an onClick rather than a
+ // button, so nothing in MODAL_DISMISS_PRED reaches it, and on iPad it is the only thing the reset
+ // has to close — atTabs is already true inside the Chat tab, so the loop below never runs.
+ const searchCancel = els(T.CHAT_THREAD_SEARCH_CANCEL)
+ if ((await searchCancel.length) > 0) {
+ const ctrl = searchCancel[0]!
+ await ctrl.click().catch(() => {})
+ await settleAfter(ctrl)
+ }
+ for (let i = 0; i < 3; i++) {
+ const controls = await browser.$$(MODAL_DISMISS_PRED).getElements()
+ if (controls.length === 0) break
+ // The LAST match, not the first: a modal that only partly covers the screen leaves the
+ // background's controls in the tree, and those come first — its view controller is appended
+ // after. Clicking the first can click straight through the sheet.
+ const ctrl = controls[controls.length - 1]!
+ await ctrl.click().catch(() => {})
+ // Waiting on atTabs here would be circular: that is the predicate this loop exists because it
+ // lies while a modal is up. Wait for THIS control to go - isDisplayed goes through the element
+ // id, where isExisting re-runs the selector and would answer "still there" for any other
+ // Done/Close/Cancel on screen (the layer behind, a second sheet, the keyboard's own toolbar).
+ // A stale element throws, which is the clearest "it is gone" there is.
+ const gone = await browser
+ .waitUntil(async () => !(await ctrl.isDisplayed().catch(() => false)), {interval: 100, timeout: 3000})
+ .then(() => true)
+ .catch(() => false)
+ // A control that survives its own click is not a modal dismiss — leave it to the loop below
+ // rather than clicking it forever.
+ if (!gone) break
+ }
for (let i = 0; i < 10; i++) {
if (await atTabs()) return
// Past the first few hops something is off — narrate each step so a stall
@@ -187,7 +312,6 @@ export async function escapeToTabs(): Promise {
// so its Done/Close/Cancel must win.
if ((await browser.$$(DISMISS_PRED).length) > 0) {
const ctrl = browser.$$(DISMISS_PRED)[0]!
- // eslint-disable-next-line no-console
if (debug) console.log(` escapeToTabs[${i}]: dismissing "${await ctrl.getAttribute('label').catch(() => '?')}"`)
await ctrl.click().catch(() => {})
await settleAfter(ctrl)
@@ -195,10 +319,8 @@ export async function escapeToTabs(): Promise {
// whose click no-ops (still present, still not at tabs) must fall through
// to the back/pop path or we'd click it forever.
if ((await atTabs()) || !(await ctrl.isExisting().catch(() => false))) continue
- // eslint-disable-next-line no-console
if (debug) console.log(` escapeToTabs[${i}]: dismiss no-oped, falling through to back/pop`)
} else if (debug) {
- // eslint-disable-next-line no-console
console.log(` escapeToTabs[${i}]: no dismiss control, trying back/pop`)
}
if ((await els(T.COMMON_BACK_BUTTON).length) > 0) {
diff --git a/shared/tests/e2e/ios-appium/wdio.android.conf.ts b/shared/tests/e2e/ios-appium/wdio.android.conf.ts
index ae9980a1862e..c4ea79562031 100644
--- a/shared/tests/e2e/ios-appium/wdio.android.conf.ts
+++ b/shared/tests/e2e/ios-appium/wdio.android.conf.ts
@@ -27,10 +27,10 @@ export const config: WebdriverIO.Config = {
capabilities: [androidCapabilities(serial)],
logLevel: 'warn',
framework: 'mocha',
- // Mirrors the iOS config: the one-session suite accumulates load over many
- // flows; retries: 2 (each with a fresh escapeToTabs reset) absorbs transient
- // nav/list flake without masking real failures. Retries run ONLY on failure.
- mochaOpts: {ui: 'bdd', timeout: 120000, retries: 2},
+ // retries: 0, matching the iOS config - see wdio.conf.ts for why. A flow that
+ // genuinely needs a retry asks for one in its own describe, where the reason is
+ // visible to whoever reads the failure.
+ mochaOpts: {ui: 'bdd', timeout: 120000, retries: 0},
reporters: ['spec'],
// relaxedSecurity lets Appium run privileged commands such as `mobile: shell`.
// android-activity-restart.test.ts needs it (pidof) to prove the app process
diff --git a/shared/tests/e2e/ios-appium/wdio.conf.ts b/shared/tests/e2e/ios-appium/wdio.conf.ts
index f2b3b85b48a1..ecac835b2606 100644
--- a/shared/tests/e2e/ios-appium/wdio.conf.ts
+++ b/shared/tests/e2e/ios-appium/wdio.conf.ts
@@ -46,13 +46,15 @@ export const config: WebdriverIO.Config = {
logLevel: 'warn',
framework: 'mocha',
// 120s: the tablet settings-subpages flow can run long; phone tests finish well
- // under this. retries: 2 — the one-session suite accumulates load over 16 flows
- // (KBFS/list loads, transient nav), and the old iOS-16.4 sims are slower/flakier
- // still (paste-menu summon, list timing), so a flow can intermittently fail; up
- // to two retries (each with a fresh escapeToTabs reset) absorbs that without
- // masking real failures (a real break fails all attempts). Retries run ONLY on
- // failure, so passing tests cost nothing.
- mochaOpts: {ui: 'bdd', timeout: 120000, retries: 2},
+ // under this.
+ //
+ // retries: 0. The claim this used to carry — "a real break fails all attempts" —
+ // is not true of every flow: a chat-search regression that failed three runs in
+ // four was reported green here, because a retry cannot tell a slow simulator
+ // from a thread that scrolled itself. Retries are opt-in per flow now: a flow
+ // that is genuinely load-sensitive calls this.retries(n) in its describe and
+ // says why, which keeps the cost visible where someone can judge it.
+ mochaOpts: {ui: 'bdd', timeout: 120000, retries: 0},
reporters: ['spec'],
services: [['appium', {args: {basePath: '/', port}}]],
// Set device orientation once at session start (e.g. iPad in landscape).
diff --git a/shared/tests/e2e/shared/test-ids.ts b/shared/tests/e2e/shared/test-ids.ts
index bafe3dbeb78f..c01658038632 100644
--- a/shared/tests/e2e/shared/test-ids.ts
+++ b/shared/tests/e2e/shared/test-ids.ts
@@ -14,6 +14,7 @@ export const NAV_TAB_SETTINGS = 'nav-tab-settings'
// Chat
export const CHAT_INBOX_LIST = 'chat-inbox-list'
export const CHAT_INBOX_ROW = 'chat-inbox-row'
+export const CHAT_INBOX_CHANNEL_ROW = 'chat-inbox-channel-row'
export const CHAT_MESSAGE_LIST = 'chat-message-list'
export const CHAT_INPUT = 'chat-input'
export const CHAT_SEND_BUTTON = 'chat-send-button'
@@ -31,6 +32,24 @@ export const CHAT_INFO_PANEL_SETTINGS_TAB = 'chat-info-panel-settings-tab'
// Android only: iOS 26 folds Search/Info into one native "More" header menu,
// but the Android header keeps the plain info icon — icons have no tappable text
export const CHAT_HEADER_INFO_BUTTON = 'chat-header-info-button'
+export const CHAT_HEADER_SEARCH_BUTTON = 'chat-header-search-button'
+export const CHAT_THREAD_SEARCH_CANCEL = 'chat-thread-search-cancel'
+// The thread search query field. It focuses itself a beat after mounting, so a test types into it
+// rather than sending keys and hoping the focus landed.
+export const CHAT_THREAD_SEARCH_INPUT = 'chat-thread-search-input'
+// "3 of 18" in the thread search bar. Read through this rather than matching " of " on screen: the
+// thread behind the bar is full of message text and a body containing " of " matches first.
+export const CHAT_THREAD_SEARCH_COUNT = 'chat-thread-search-count'
+export const CHAT_THREAD_SEARCH_PREV = 'chat-thread-search-prev'
+export const CHAT_THREAD_SEARCH_NEXT = 'chat-thread-search-next'
+// The row a thread search is currently sitting on. Really "the centre-highlighted row": pinned
+// messages, reply jumps and permalinks highlight one too, so this only means "search hit" inside a
+// search flow. Present only while the row is highlighted.
+export const CHAT_SEARCH_HIT = 'chat-search-hit'
+// The header above the oldest loaded message. Mounted in every state - loading, more to load, start
+// of the conversation - so its position is readable throughout, which is how a test sees a page of
+// older messages arrive.
+export const CHAT_THREAD_TOP = 'chat-thread-top'
// Files
export const FILES_BROWSER = 'files-browser'
diff --git a/shared/yarn.lock b/shared/yarn.lock
index 1a2bb23dcf1d..cc6c3427188d 100644
--- a/shared/yarn.lock
+++ b/shared/yarn.lock
@@ -2642,10 +2642,10 @@
dependencies:
"@khanacademy/perseus-utils" "2.1.5"
-"@legendapp/list@3.3.4":
- version "3.3.4"
- resolved "https://registry.yarnpkg.com/@legendapp/list/-/list-3.3.4.tgz#6d71d46d1026d92015116f00492839a629cc50b8"
- integrity sha512-XsKCM7F8Fmy1jE7dZ9UEBCihouVOJ6B5pbAI1ssooadBu+YQHGoSQdxHs5dPXHb4qkGOIfUIAljIXnDzMYUCCQ==
+"@legendapp/list@3.3.5":
+ version "3.3.5"
+ resolved "https://registry.yarnpkg.com/@legendapp/list/-/list-3.3.5.tgz#a2a7b8e0c3897fc5bd159d7b87e0b62ef45a4f74"
+ integrity sha512-XTsLYtpg41SVb5uLBYA+YcDSA3w0tgoPq/W8ZggQ2tx+3lrC/rf+ehTP9KYHea9oFaZIuePAgzACs5/auVMJlQ==
dependencies:
use-sync-external-store "^1.5.0"