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 dda21755fdf5..765cfa4f9678 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,23 @@ 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 = {}
+
const keyExtractor = (ordinal: ItemType) => String(ordinal)
// Item type for list recycling pool separation. A message that leads its author group renders an
@@ -138,7 +140,6 @@ const usePagination = (p: {
return {onEndReached, onStartReached}
}
-const centerTolerancePx = 8
// A scroller within this many pixels of its end counts as at the end.
const endTolerancePx = 2
@@ -212,6 +213,52 @@ 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])
+
+ // Unconditional on purpose. A guard that stood this call down while the list's own
+ // initialScrollIndex bootstrap looked like it owned the target was tried and measured against the
+ // app, and there is no version of it that is safe: on the permalink path the thread mounts with no
+ // centred target, so the list is built with initialScrollAtEnd, and the bootstrap it re-arms when
+ // the centred dataset lands does not move it - the thread settles at its end with the target never
+ // shown. That path is indistinguishable from a warm in-thread jump by anything visible here (both
+ // arrive as "dataset with a resolvable initialScrollIndex"), so this call has to be the one
+ // authority that always fires.
+ 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)
@@ -221,7 +268,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.
+ // 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)
@@ -319,111 +368,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)
@@ -527,12 +472,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 (
void}
onStartReached={onStartReached}
@@ -621,174 +562,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
@@ -804,119 +647,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
@@ -937,99 +679,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 && (
@@ -1054,42 +765,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..e6b237cb58dc 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
@@ -905,7 +906,11 @@ function RightSide(p: RProps) {
)}
>
-
+
)
@@ -1018,7 +1023,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/conversation/thread-context.tsx b/shared/chat/conversation/thread-context.tsx
index 598b2c74fa11..9a30b23a5214 100644
--- a/shared/chat/conversation/thread-context.tsx
+++ b/shared/chat/conversation/thread-context.tsx
@@ -96,9 +96,11 @@ ConversationThreadIDContext.displayName = 'ConversationThreadIDContext'
export type ConversationThreadState = {
accountsInfoMap: Map
- // Bumped on every messagesClear. The desktop list remounts on it: LegendList cannot recover
- // from a non-empty -> empty -> non-empty data transition (it resets its layout state and waits
- // for a container layout event that never comes), so the thread renders blank forever.
+ // Bumped on every messagesClear, and fed to the list as its dataKey (not as a React key - the
+ // list is not remounted). LegendList cannot recover from a non-empty -> empty -> non-empty data
+ // transition on its own (it resets its layout state and waits for a container layout event that
+ // never comes), so the thread renders blank forever; the dataKey change is what tells it this is
+ // a new dataset and makes it reset rather than wait.
clearVersion: number
explodingMode: number
flipStatusMap: Map
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 (
-
+
0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
++ }
++ }
+ }
+
+-// 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 }
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
++ const state = ctx.state;
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
+ }
+- };
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
++ state.pendingTotalSize = totalSize;
++ } else {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
+ }
++ sizes.set(itemKey, size);
+ }
+
+-// 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));
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
++ }
+ }
+-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
+- });
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
+ }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
++ }
++ }
++ return -1;
+ }
+-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;
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- 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;
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -824,748 +932,760 @@ function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+ },
+ (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) {
++
++// 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;
+- 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");
++ 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);
+ }
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(ctx) {
++ var _a3, _b;
+ 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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ if (PlatformAdjustBreaksScroll) {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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;
++// 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 scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
++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
++ });
+ }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
+ }
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
++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;
+ }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
++};
++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;
+ }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
++};
++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 (didInitialScroll) {
+- state.didFinishInitialScroll = true;
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- 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 });
+- }
++ 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;
++var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1;
++var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16;
++var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1;
++function checkFinishedScroll(ctx, options) {
++ const scrollingTo = ctx.state.scrollingTo;
++ if (options == null ? void 0 : options.onlyIfAligned) {
++ if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) {
++ return;
++ }
++ if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) {
++ return;
+ }
+ }
++ ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame");
+ }
+-
+-// 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 hasScrollCompletionOwnership(state, options) {
++ const { clampedTargetOffset, scrollingTo } = options;
++ return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON;
+ }
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
++function isSilentInitialDispatch(state, scrollingTo) {
++ return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled;
+ }
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
++function getInitialScrollWatchdogTargetOffset(state) {
++ var _a3;
++ return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset;
+ }
+-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 isNativeInitialNonZeroTarget(state) {
++ const targetOffset = getInitialScrollWatchdogTargetOffset(state);
++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset);
+ }
+-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);
+- }
++function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) {
++ var _a3, _b;
++ if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) {
++ return false;
+ }
+- 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;
++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") {
++ return false;
+ }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset;
++ if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) {
++ return false;
++ }
++ if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) {
++ return false;
++ }
++ return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state);
+ }
+-
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
++function shouldFinishInitialZeroTargetScroll(ctx) {
++ var _a3;
+ const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++ return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON;
+ }
+-
+-// src/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
+- }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
++function isEndAlignedLastItemTarget(ctx, scrollingTo) {
++ return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1;
+ }
+-
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++function getCurrentTargetOffset(ctx, scrollingTo) {
++ var _a3;
++ const index = scrollingTo.index;
++ const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo);
++ const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo);
++ return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo);
+ }
+-function getAlignItemsAtEndPadding(ctx) {
++function getResolvedScrollCompletionState(ctx, scrollingTo) {
+ const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++ const scroll = state.scrollPending;
++ const adjust = state.scrollAdjustHandler.getAdjust();
++ const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo);
++ const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo);
++ const diff1 = Math.abs(scroll - clampedTargetOffset);
++ const adjustedTargetOffset = clampedTargetOffset + adjust;
++ const diff2 = Math.abs(scroll - adjustedTargetOffset);
++ const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios";
++ return {
++ clampedTargetOffset,
++ isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1)
++ };
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
++function checkFinishedScrollFrame(ctx) {
++ const scrollingTo = ctx.state.scrollingTo;
++ if (!scrollingTo) {
++ return;
++ }
+ const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++ const completionState = getResolvedScrollCompletionState(ctx, scrollingTo);
++ if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, {
++ clampedTargetOffset: completionState.clampedTargetOffset,
++ scrollingTo
++ })) {
++ finishScrollTo(ctx);
+ }
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
++function scrollToFallbackOffset(ctx, offset) {
++ var _a3;
++ (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({
++ animated: false,
++ x: ctx.state.props.horizontal ? offset : 0,
++ y: ctx.state.props.horizontal ? 0 : offset
++ });
++}
++function checkFinishedScrollFallback(ctx) {
+ const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
+- }
+- } else {
+- totalSize += add;
++ if (state.scheduledWork.has("checkFinishedScrollFallback")) {
++ return;
+ }
+- if (prevTotalSize !== totalSize) {
+- if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
+- state.pendingTotalSize = totalSize;
+- } else {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
++ const scrollingTo = state.scrollingTo;
++ const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
++ const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo);
++ const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false;
++ const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout;
++ const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100;
++ let numChecks = 0;
++ const scheduleFallbackCheck = (delay) => {
++ state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback");
++ };
++ const checkHasScrolled = () => {
++ var _a3, _b, _c, _d;
++ const isStillScrollingTo = state.scrollingTo;
++ if (isStillScrollingTo) {
++ numChecks++;
++ const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled;
++ const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5;
++ const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
++ const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress(
++ state,
++ isStillScrollingTo
++ );
++ const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo);
++ const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1;
++ const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state);
++ const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget);
++ const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
++ const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
++ if (shouldRetrySilentInitialNativeScroll) {
++ const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0;
++ const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON;
++ initialScrollCompletion.markSilentInitialScrollRetry(state);
++ scrollToFallbackOffset(ctx, jiggleOffset);
++ state.scheduledWork.frame(
++ () => scrollToFallbackOffset(ctx, targetOffset),
++ "checkFinishedScrollRetryFrame"
++ );
++ scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
++ } else if (shouldRetryUnalignedEndScroll) {
++ scrollToFallbackOffset(ctx, completionState.clampedTargetOffset);
++ scheduleFallbackCheck(100);
++ } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
++ finishScrollTo(ctx);
++ } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) {
++ const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending;
++ scrollToFallbackOffset(ctx, targetOffset);
++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
++ } else {
++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+ }
+- updateContentMetricsState(ctx);
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
++ };
++ scheduleFallbackCheck(initialDelay);
+ }
+
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++// src/core/doScrollTo.native.ts
++function doScrollTo(ctx, params) {
+ const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ const { animated, horizontal, isInitialScroll, offset } = params;
++ const isAnimated = !!animated;
++ const { refScroller } = state;
++ const scroller = refScroller.current;
++ if (!scroller) {
++ return;
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
+-}
+-function isArray(obj) {
+- return Array.isArray(obj);
+-}
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize);
++ scroller.scrollTo({
++ animated: isAnimated,
++ x: isHorizontal ? nativeOffset : 0,
++ y: isHorizontal ? 0 : offset
++ });
++ if (isInitialScroll) {
++ initialScrollCompletion.markInitialScrollNativeDispatch(state);
+ }
+-}
+-function roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
+- var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
++ if (isAnimated && Math.abs(state.scroll - offset) <= 1) {
++ checkFinishedScroll(ctx);
+ }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ if (!isAnimated) {
++ state.scroll = offset;
++ checkFinishedScrollFallback(ctx);
+ }
+- return -1;
+ }
+
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
+- var _a3, _b;
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ const state = ctx.state;
+- const { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
+- }
+- }
+- return size;
+-}
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
+-}
+-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
++ if (Math.abs(positionDiff) > 0.1) {
++ const dataChanged = source === "data";
++ const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
++ const doit = () => {
++ if (needsScrollWorkaround) {
++ doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll });
++ } else {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
++ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
++ if (Platform.OS !== "web" && source !== "item-size") {
++ const threshold = state.scroll - positionDiff / 2;
++ if (!state.ignoreScrollFromMVCP) {
++ state.ignoreScrollFromMVCP = {};
++ }
++ if (positionDiff > 0) {
++ state.ignoreScrollFromMVCP.lt = threshold;
++ } else {
++ state.ignoreScrollFromMVCP.gt = threshold;
++ }
++ const delay = needsScrollWorkaround ? 250 : 100;
++ state.scheduledWork.timeout(
++ () => {
++ var _a3;
++ state.ignoreScrollFromMVCP = void 0;
++ const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false;
++ if (shouldForceUpdate) {
++ state.ignoreScrollFromMVCPIgnored = false;
++ state.scrollPending = state.scroll;
++ (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state);
++ }
++ },
++ delay,
++ "ignoreScrollFromMVCP"
++ );
++ }
++ } else {
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+ }
+- return true;
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
++
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
+- }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
+ }
+-
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++function settleScrollTarget(ctx) {
+ var _a3;
+ const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ 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 (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;
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
+ }
++ return false;
+ }
+- return offset;
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// 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 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);
++ 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
++ )
++ );
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
+ }
+
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++// 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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- if (PlatformAdjustBreaksScroll) {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
+- }
+- if (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
+- }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+ }
+ }
+-
+-// src/core/checkFinishedScroll.ts
+-var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20;
+-var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1;
+-var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1;
+-var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16;
+-var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1;
+-function checkFinishedScroll(ctx, options) {
+- const scrollingTo = ctx.state.scrollingTo;
+- if (options == null ? void 0 : options.onlyIfAligned) {
+- if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) {
+- return;
+- }
+- if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) {
+- return;
+- }
++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);
+ }
+- ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame");
+ }
+-function hasScrollCompletionOwnership(state, options) {
+- const { clampedTargetOffset, scrollingTo } = options;
+- return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON;
++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 isSilentInitialDispatch(state, scrollingTo) {
+- return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled;
++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);
++ }
++ }
+ }
+-function getInitialScrollWatchdogTargetOffset(state) {
++
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
+ var _a3;
+- return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset;
+-}
+-function isNativeInitialNonZeroTarget(state) {
+- const targetOffset = getInitialScrollWatchdogTargetOffset(state);
+- return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset);
++ 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 shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) {
+- var _a3, _b;
+- if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) {
+- return false;
+- }
+- if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") {
+- return false;
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset;
+- if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) {
+- return false;
++ 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 (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) {
+- return false;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-function shouldFinishInitialZeroTargetScroll(ctx) {
+- var _a3;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
+ const { state } = ctx;
+- return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON;
+-}
+-function isEndAlignedLastItemTarget(ctx, scrollingTo) {
+- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1;
++ 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 });
++ }
++ }
++ }
+ }
+-function getCurrentTargetOffset(ctx, scrollingTo) {
+- var _a3;
+- const index = scrollingTo.index;
+- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo);
+- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo);
+- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo);
++
++// 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 getResolvedScrollCompletionState(ctx, scrollingTo) {
+- const { state } = ctx;
+- const scroll = state.scrollPending;
+- const adjust = state.scrollAdjustHandler.getAdjust();
+- const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo);
+- const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo);
+- const diff1 = Math.abs(scroll - clampedTargetOffset);
+- const adjustedTargetOffset = clampedTargetOffset + adjust;
+- const diff2 = Math.abs(scroll - adjustedTargetOffset);
+- const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios";
+- return {
+- clampedTargetOffset,
+- isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1)
+- };
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
+ }
+-function checkFinishedScrollFrame(ctx) {
+- const scrollingTo = ctx.state.scrollingTo;
+- if (!scrollingTo) {
+- return;
+- }
+- const { state } = ctx;
+- const completionState = getResolvedScrollCompletionState(ctx, scrollingTo);
+- if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, {
+- clampedTargetOffset: completionState.clampedTargetOffset,
+- scrollingTo
+- })) {
+- finishScrollTo(ctx);
+- }
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
+ }
+-function scrollToFallbackOffset(ctx, offset) {
+- var _a3;
+- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({
+- animated: false,
+- x: ctx.state.props.horizontal ? offset : 0,
+- y: ctx.state.props.horizontal ? 0 : offset
+- });
++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 checkFinishedScrollFallback(ctx) {
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- if (state.scheduledWork.has("checkFinishedScrollFallback")) {
+- return;
++ 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 scrollingTo = state.scrollingTo;
+- const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
+- const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo);
+- const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false;
+- const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout;
+- const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100;
+- let numChecks = 0;
+- const scheduleFallbackCheck = (delay) => {
+- state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback");
+- };
+- const checkHasScrolled = () => {
+- var _a3, _b, _c, _d;
+- const isStillScrollingTo = state.scrollingTo;
+- if (isStillScrollingTo) {
+- numChecks++;
+- const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled;
+- const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5;
+- const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
+- const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress(
+- state,
+- isStillScrollingTo
+- );
+- const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo);
+- const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1;
+- const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state);
+- const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget);
+- const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
+- const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
+- if (shouldRetrySilentInitialNativeScroll) {
+- const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0;
+- const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON;
+- initialScrollCompletion.markSilentInitialScrollRetry(state);
+- scrollToFallbackOffset(ctx, jiggleOffset);
+- state.scheduledWork.frame(
+- () => scrollToFallbackOffset(ctx, targetOffset),
+- "checkFinishedScrollRetryFrame"
++ 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"
+ );
+- scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
+- } else if (shouldRetryUnalignedEndScroll) {
+- scrollToFallbackOffset(ctx, completionState.clampedTargetOffset);
+- scheduleFallbackCheck(100);
+- } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
+- finishScrollTo(ctx);
+- } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) {
+- const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending;
+- scrollToFallbackOffset(ctx, targetOffset);
+- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+- } else {
+- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+ }
++ } 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);
+ };
+- scheduleFallbackCheck(initialDelay);
+-}
+-
+-// src/core/doScrollTo.native.ts
+-function doScrollTo(ctx, params) {
+- const state = ctx.state;
+- const { animated, horizontal, isInitialScroll, offset } = params;
+- const isAnimated = !!animated;
+- const { refScroller } = state;
+- const scroller = refScroller.current;
+- if (!scroller) {
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
+ return;
+ }
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize);
+- scroller.scrollTo({
+- animated: isAnimated,
+- x: isHorizontal ? nativeOffset : 0,
+- y: isHorizontal ? 0 : offset
+- });
+- if (isInitialScroll) {
+- initialScrollCompletion.markInitialScrollNativeDispatch(state);
+- }
+- if (isAnimated && Math.abs(state.scroll - offset) <= 1) {
+- checkFinishedScroll(ctx);
+- }
+- if (!isAnimated) {
+- state.scroll = offset;
+- checkFinishedScrollFallback(ctx);
+- }
++ complete();
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1612,60 +1732,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const dataChanged = source === "data";
+- const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
+- const doit = () => {
+- if (needsScrollWorkaround) {
+- doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll });
+- } else {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- if (Platform.OS !== "web" && source !== "item-size") {
+- const threshold = state.scroll - positionDiff / 2;
+- if (!state.ignoreScrollFromMVCP) {
+- state.ignoreScrollFromMVCP = {};
+- }
+- if (positionDiff > 0) {
+- state.ignoreScrollFromMVCP.lt = threshold;
+- } else {
+- state.ignoreScrollFromMVCP.gt = threshold;
+- }
+- const delay = needsScrollWorkaround ? 250 : 100;
+- state.scheduledWork.timeout(
+- () => {
+- var _a3;
+- state.ignoreScrollFromMVCP = void 0;
+- const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false;
+- if (shouldForceUpdate) {
+- state.ignoreScrollFromMVCPIgnored = false;
+- state.scrollPending = state.scroll;
+- (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state);
+- }
+- },
+- delay,
+- "ignoreScrollFromMVCP"
+- );
+- }
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2302,7 +2368,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 {
+@@ -2334,6 +2400,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2341,7 +2416,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 });
+@@ -4415,7 +4490,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -5940,6 +6016,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;
+@@ -5949,6 +6026,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7036,13 +7116,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+diff --git a/node_modules/@legendapp/list/react-native.mjs b/node_modules/@legendapp/list/react-native.mjs
+index 4ad913c..8c313ee 100644
+--- a/node_modules/@legendapp/list/react-native.mjs
++++ b/node_modules/@legendapp/list/react-native.mjs
+@@ -437,171 +437,279 @@ 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");
++// src/core/getStartOffsetAdjustment.ts
++function getStartOffsetAdjustment(ctx) {
++ const { state } = ctx;
++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
+ }
+-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();
++
++// src/utils/getId.ts
++function getId(state, index) {
++ const { data, keyExtractor } = state.props;
++ if (!data) {
++ return "";
++ }
++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
++ const id = ret;
++ state.idCache[index] = id;
++ return id;
+ }
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
++
++// src/core/updateContentMetricsState.ts
++function getRawContentLength(ctx) {
++ var _a3, _b, _c;
++ const { state, values } = ctx;
++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++}
++function getAlignItemsAtEndPadding(ctx) {
++ const { state } = ctx;
++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
++ }
++ }
+ }
+
+-// 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 }
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
++ const state = ctx.state;
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
+ }
+- };
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
++ state.pendingTotalSize = totalSize;
++ } else {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
+ }
++ sizes.set(itemKey, size);
+ }
+
+-// 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));
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
++ }
+ }
+-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
+- });
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
+ }
+- (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
+- return state.initialScrollSession.completion;
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
++ }
++ }
++ return -1;
+ }
+-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;
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- 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;
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -803,748 +911,760 @@ function checkAtTop(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
+ },
+ (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) {
++
++// 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;
+- 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");
++ 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);
+ }
+-function updateAdaptiveRender(ctx, scrollVelocity, options) {
+- var _a3, _b, _c;
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(ctx) {
++ var _a3, _b;
+ 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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ if (PlatformAdjustBreaksScroll) {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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;
++// 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 scheduleFullDrawDistancePrewarm(ctx) {
+- const { state } = ctx;
+- if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
+- return;
++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
++ });
+ }
+- state.scheduledWork.frame(() => {
+- var _a3;
+- return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
+- }, "fullDrawDistancePrewarm");
++ (_b2 = (_a4 = state.initialScrollSession).completion) != null ? _b2 : _a4.completion = {};
++ return state.initialScrollSession.completion;
+ }
+-
+-// src/utils/setInitialRenderState.ts
+-function resetInitialRenderState(ctx, {
+- resetLayout,
+- resetInitialScroll
+-}) {
+- const { state } = ctx;
+- if (resetLayout) {
+- state.didContainersLayout = false;
+- state.queuedInitialLayout = false;
++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;
+ }
+- if (resetInitialScroll) {
+- state.didFinishInitialScroll = false;
++};
++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;
+ }
+- set$(ctx, "readyToRender", false);
+- resetAdaptiveRender(ctx);
+-}
+-function setInitialRenderState(ctx, {
+- didLayout,
+- didInitialScroll
+-}) {
+- const { state } = ctx;
+- const {
+- loadStartTime,
+- props: { onLoad }
+- } = state;
+- if (didLayout) {
+- state.didContainersLayout = true;
++};
++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 (didInitialScroll) {
+- state.didFinishInitialScroll = true;
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- 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 });
+- }
++ 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;
++var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1;
++var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16;
++var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1;
++function checkFinishedScroll(ctx, options) {
++ const scrollingTo = ctx.state.scrollingTo;
++ if (options == null ? void 0 : options.onlyIfAligned) {
++ if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) {
++ return;
++ }
++ if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) {
++ return;
+ }
+ }
++ ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame");
+ }
+-
+-// 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 hasScrollCompletionOwnership(state, options) {
++ const { clampedTargetOffset, scrollingTo } = options;
++ return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON;
+ }
+-function clearPreservedInitialScrollTargetTimeout(state) {
+- state.scheduledWork.cancel("preservedInitialScroll");
++function isSilentInitialDispatch(state, scrollingTo) {
++ return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled;
+ }
+-function clearPreservedInitialScrollTarget(state) {
+- clearPreservedInitialScrollTargetTimeout(state);
+- state.clearPreservedInitialScrollOnNextFinish = void 0;
+- state.initialScroll = void 0;
+- setInitialScrollSession(state);
++function getInitialScrollWatchdogTargetOffset(state) {
++ var _a3;
++ return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset;
+ }
+-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 isNativeInitialNonZeroTarget(state) {
++ const targetOffset = getInitialScrollWatchdogTargetOffset(state);
++ return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset);
+ }
+-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);
+- }
++function shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) {
++ var _a3, _b;
++ if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) {
++ return false;
+ }
+- 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;
++ if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") {
++ return false;
+ }
+- complete();
+-}
+-
+-// src/core/calculateOffsetForIndex.ts
+-function calculateOffsetForIndex(ctx, index) {
+- const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset;
++ if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) {
++ return false;
++ }
++ if (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) {
++ return false;
++ }
++ return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state);
+ }
+-
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
++function shouldFinishInitialZeroTargetScroll(ctx) {
++ var _a3;
+ const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++ return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON;
+ }
+-
+-// src/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
+- }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
++function isEndAlignedLastItemTarget(ctx, scrollingTo) {
++ return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1;
+ }
+-
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++function getCurrentTargetOffset(ctx, scrollingTo) {
++ var _a3;
++ const index = scrollingTo.index;
++ const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo);
++ const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo);
++ return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo);
+ }
+-function getAlignItemsAtEndPadding(ctx) {
++function getResolvedScrollCompletionState(ctx, scrollingTo) {
+ const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++ const scroll = state.scrollPending;
++ const adjust = state.scrollAdjustHandler.getAdjust();
++ const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo);
++ const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo);
++ const diff1 = Math.abs(scroll - clampedTargetOffset);
++ const adjustedTargetOffset = clampedTargetOffset + adjust;
++ const diff2 = Math.abs(scroll - adjustedTargetOffset);
++ const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios";
++ return {
++ clampedTargetOffset,
++ isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1)
++ };
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
++function checkFinishedScrollFrame(ctx) {
++ const scrollingTo = ctx.state.scrollingTo;
++ if (!scrollingTo) {
++ return;
++ }
+ const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++ const completionState = getResolvedScrollCompletionState(ctx, scrollingTo);
++ if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, {
++ clampedTargetOffset: completionState.clampedTargetOffset,
++ scrollingTo
++ })) {
++ finishScrollTo(ctx);
+ }
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
++function scrollToFallbackOffset(ctx, offset) {
++ var _a3;
++ (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({
++ animated: false,
++ x: ctx.state.props.horizontal ? offset : 0,
++ y: ctx.state.props.horizontal ? 0 : offset
++ });
++}
++function checkFinishedScrollFallback(ctx) {
+ const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
+- }
+- } else {
+- totalSize += add;
++ if (state.scheduledWork.has("checkFinishedScrollFallback")) {
++ return;
+ }
+- if (prevTotalSize !== totalSize) {
+- if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
+- state.pendingTotalSize = totalSize;
+- } else {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
++ const scrollingTo = state.scrollingTo;
++ const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
++ const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo);
++ const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false;
++ const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout;
++ const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100;
++ let numChecks = 0;
++ const scheduleFallbackCheck = (delay) => {
++ state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback");
++ };
++ const checkHasScrolled = () => {
++ var _a3, _b, _c, _d;
++ const isStillScrollingTo = state.scrollingTo;
++ if (isStillScrollingTo) {
++ numChecks++;
++ const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled;
++ const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5;
++ const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
++ const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress(
++ state,
++ isStillScrollingTo
++ );
++ const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo);
++ const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1;
++ const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state);
++ const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget);
++ const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
++ const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
++ if (shouldRetrySilentInitialNativeScroll) {
++ const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0;
++ const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON;
++ initialScrollCompletion.markSilentInitialScrollRetry(state);
++ scrollToFallbackOffset(ctx, jiggleOffset);
++ state.scheduledWork.frame(
++ () => scrollToFallbackOffset(ctx, targetOffset),
++ "checkFinishedScrollRetryFrame"
++ );
++ scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
++ } else if (shouldRetryUnalignedEndScroll) {
++ scrollToFallbackOffset(ctx, completionState.clampedTargetOffset);
++ scheduleFallbackCheck(100);
++ } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
++ finishScrollTo(ctx);
++ } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) {
++ const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending;
++ scrollToFallbackOffset(ctx, targetOffset);
++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
++ } else {
++ scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+ }
+- updateContentMetricsState(ctx);
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
++ };
++ scheduleFallbackCheck(initialDelay);
+ }
+
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++// src/core/doScrollTo.native.ts
++function doScrollTo(ctx, params) {
+ const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ const { animated, horizontal, isInitialScroll, offset } = params;
++ const isAnimated = !!animated;
++ const { refScroller } = state;
++ const scroller = refScroller.current;
++ if (!scroller) {
++ return;
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
+-}
+-function isArray(obj) {
+- return Array.isArray(obj);
+-}
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize);
++ scroller.scrollTo({
++ animated: isAnimated,
++ x: isHorizontal ? nativeOffset : 0,
++ y: isHorizontal ? 0 : offset
++ });
++ if (isInitialScroll) {
++ initialScrollCompletion.markInitialScrollNativeDispatch(state);
+ }
+-}
+-function roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
+- var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
++ if (isAnimated && Math.abs(state.scroll - offset) <= 1) {
++ checkFinishedScroll(ctx);
+ }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ if (!isAnimated) {
++ state.scroll = offset;
++ checkFinishedScrollFallback(ctx);
+ }
+- return -1;
+ }
+
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
+- var _a3, _b;
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ const state = ctx.state;
+- const { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
+- }
+- }
+- return size;
+-}
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
+-}
+-function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
++ if (Math.abs(positionDiff) > 0.1) {
++ const dataChanged = source === "data";
++ const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
++ const doit = () => {
++ if (needsScrollWorkaround) {
++ doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll });
++ } else {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
++ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
++ if (Platform.OS !== "web" && source !== "item-size") {
++ const threshold = state.scroll - positionDiff / 2;
++ if (!state.ignoreScrollFromMVCP) {
++ state.ignoreScrollFromMVCP = {};
++ }
++ if (positionDiff > 0) {
++ state.ignoreScrollFromMVCP.lt = threshold;
++ } else {
++ state.ignoreScrollFromMVCP.gt = threshold;
++ }
++ const delay = needsScrollWorkaround ? 250 : 100;
++ state.scheduledWork.timeout(
++ () => {
++ var _a3;
++ state.ignoreScrollFromMVCP = void 0;
++ const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false;
++ if (shouldForceUpdate) {
++ state.ignoreScrollFromMVCPIgnored = false;
++ state.scrollPending = state.scroll;
++ (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state);
++ }
++ },
++ delay,
++ "ignoreScrollFromMVCP"
++ );
++ }
++ } else {
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+ }
+- return true;
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
++
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
+- }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
+ }
+-
+-// src/core/calculateOffsetWithOffsetPosition.ts
+-function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
++function settleScrollTarget(ctx) {
+ var _a3;
+ const state = ctx.state;
+- const { index, viewOffset, viewPosition } = params;
+- let offset = offsetParam;
+- if (viewOffset) {
+- offset -= viewOffset;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ return false;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ 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 (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;
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
+ }
++ return false;
+ }
+- return offset;
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/clampScrollOffset.ts
+-function clampScrollOffset(ctx, offset, scrollTarget) {
++// 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 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);
++ 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
++ )
++ );
+ }
+- clampedOffset = Math.max(0, clampedOffset);
+- return clampedOffset;
+ }
+
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++// 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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- if (PlatformAdjustBreaksScroll) {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
+- }
+- if (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
+- }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ clearAdaptiveRenderExitTimeout(ctx);
++ if (exitDelay <= 0) {
++ setAdaptiveRender(ctx, "normal", "scroll");
++ } else {
++ state.scheduledWork.timeout(() => setAdaptiveRender(ctx, "normal", "scroll"), exitDelay, "adaptiveRender");
+ }
+ }
+-
+-// src/core/checkFinishedScroll.ts
+-var INITIAL_SCROLL_MAX_FALLBACK_CHECKS = 20;
+-var INITIAL_SCROLL_COMPLETION_TARGET_EPSILON = 1;
+-var INITIAL_SCROLL_ZERO_TARGET_EPSILON = 1;
+-var SILENT_INITIAL_SCROLL_RETRY_DELAY_MS = 16;
+-var SILENT_INITIAL_SCROLL_TARGET_EPSILON = 1;
+-function checkFinishedScroll(ctx, options) {
+- const scrollingTo = ctx.state.scrollingTo;
+- if (options == null ? void 0 : options.onlyIfAligned) {
+- if (!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) || scrollingTo.animated) {
+- return;
+- }
+- if (!getResolvedScrollCompletionState(ctx, scrollingTo).isAtResolvedTarget) {
+- return;
+- }
++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);
+ }
+- ctx.state.scheduledWork.frame(() => checkFinishedScrollFrame(ctx), "checkFinishedScrollFrame");
+ }
+-function hasScrollCompletionOwnership(state, options) {
+- const { clampedTargetOffset, scrollingTo } = options;
+- return !scrollingTo.isInitialScroll || state.hasScrolled || clampedTargetOffset <= INITIAL_SCROLL_COMPLETION_TARGET_EPSILON;
++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 isSilentInitialDispatch(state, scrollingTo) {
+- return !!(scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled;
++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);
++ }
++ }
+ }
+-function getInitialScrollWatchdogTargetOffset(state) {
++
++// src/utils/getEffectiveDrawDistance.ts
++var INITIAL_DRAW_DISTANCE = 50;
++function getEffectiveDrawDistance(ctx, mode) {
+ var _a3;
+- return (_a3 = initialScrollWatchdog.get(state)) == null ? void 0 : _a3.targetOffset;
+-}
+-function isNativeInitialNonZeroTarget(state) {
+- const targetOffset = getInitialScrollWatchdogTargetOffset(state);
+- return !state.didFinishInitialScroll && initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset);
++ 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 shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) {
+- var _a3, _b;
+- if (!scrollingTo.isInitialScroll || scrollingTo.animated || !state.didContainersLayout) {
+- return false;
+- }
+- if (((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "bootstrap") {
+- return false;
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- const targetOffset = (_b = scrollingTo.targetOffset) != null ? _b : scrollingTo.offset;
+- if (initialScrollWatchdog.hasNonZeroTargetOffset(targetOffset) && initialScrollCompletion.didDispatchNativeScroll(state) && !state.hasScrolled) {
+- return false;
++ 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 (initialScrollWatchdog.isAtZeroTargetOffset(targetOffset) || Math.abs(state.scroll - targetOffset) > 1 || Math.abs(state.scrollPending - targetOffset) > 1) {
+- return false;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- return !!scrollingTo.waitForInitialScrollCompletionFrame || isNativeInitialNonZeroTarget(state);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-function shouldFinishInitialZeroTargetScroll(ctx) {
+- var _a3;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
+ const { state } = ctx;
+- return !!((_a3 = state.scrollingTo) == null ? void 0 : _a3.isInitialScroll) && state.props.data.length > 0 && getContentSize(ctx) <= state.scrollLength && state.scrollPending <= INITIAL_SCROLL_ZERO_TARGET_EPSILON;
+-}
+-function isEndAlignedLastItemTarget(ctx, scrollingTo) {
+- return scrollingTo.index === ctx.state.props.data.length - 1 && scrollingTo.viewPosition === 1;
++ 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 });
++ }
++ }
++ }
+ }
+-function getCurrentTargetOffset(ctx, scrollingTo) {
+- var _a3;
+- const index = scrollingTo.index;
+- const shouldRecomputeEndTarget = isEndAlignedLastItemTarget(ctx, scrollingTo);
+- const requestedTargetOffset = shouldRecomputeEndTarget && index !== void 0 ? calculateOffsetWithOffsetPosition(ctx, calculateOffsetForIndex(ctx, index), scrollingTo) : (_a3 = scrollingTo.targetOffset) != null ? _a3 : clampScrollOffset(ctx, scrollingTo.offset - (scrollingTo.viewOffset || 0), scrollingTo);
+- return clampScrollOffset(ctx, requestedTargetOffset, scrollingTo);
++
++// 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 getResolvedScrollCompletionState(ctx, scrollingTo) {
+- const { state } = ctx;
+- const scroll = state.scrollPending;
+- const adjust = state.scrollAdjustHandler.getAdjust();
+- const clampedTargetOffset = getCurrentTargetOffset(ctx, scrollingTo);
+- const maxOffset = clampScrollOffset(ctx, scroll, scrollingTo);
+- const diff1 = Math.abs(scroll - clampedTargetOffset);
+- const adjustedTargetOffset = clampedTargetOffset + adjust;
+- const diff2 = Math.abs(scroll - adjustedTargetOffset);
+- const canUseAdjustedCompletion = !scrollingTo.animated || Platform.OS === "ios";
+- return {
+- clampedTargetOffset,
+- isAtResolvedTarget: Math.abs(scroll - maxOffset) < 1 && (diff1 < 1 || canUseAdjustedCompletion && diff2 < 1)
+- };
++function clearPreservedInitialScrollTargetTimeout(state) {
++ state.scheduledWork.cancel("preservedInitialScroll");
+ }
+-function checkFinishedScrollFrame(ctx) {
+- const scrollingTo = ctx.state.scrollingTo;
+- if (!scrollingTo) {
+- return;
+- }
+- const { state } = ctx;
+- const completionState = getResolvedScrollCompletionState(ctx, scrollingTo);
+- if (completionState.isAtResolvedTarget && hasScrollCompletionOwnership(state, {
+- clampedTargetOffset: completionState.clampedTargetOffset,
+- scrollingTo
+- })) {
+- finishScrollTo(ctx);
+- }
++function clearPreservedInitialScrollTarget(state) {
++ clearPreservedInitialScrollTargetTimeout(state);
++ state.clearPreservedInitialScrollOnNextFinish = void 0;
++ state.initialScroll = void 0;
++ setInitialScrollSession(state);
+ }
+-function scrollToFallbackOffset(ctx, offset) {
+- var _a3;
+- (_a3 = ctx.state.refScroller.current) == null ? void 0 : _a3.scrollTo({
+- animated: false,
+- x: ctx.state.props.horizontal ? offset : 0,
+- y: ctx.state.props.horizontal ? 0 : offset
+- });
++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 checkFinishedScrollFallback(ctx) {
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- if (state.scheduledWork.has("checkFinishedScrollFallback")) {
+- return;
++ 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 scrollingTo = state.scrollingTo;
+- const shouldFinishInitialZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
+- const silentInitialDispatch = isSilentInitialDispatch(state, scrollingTo);
+- const canFinishInitialWithoutNativeProgress = scrollingTo !== void 0 ? shouldFinishInitialScrollWithoutNativeProgress(state, scrollingTo) : false;
+- const slowTimeout = (scrollingTo == null ? void 0 : scrollingTo.isInitialScroll) && !shouldFinishInitialZeroTarget && !canFinishInitialWithoutNativeProgress || !state.didContainersLayout;
+- const initialDelay = shouldFinishInitialZeroTarget || canFinishInitialWithoutNativeProgress ? 0 : silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : slowTimeout ? 500 : 100;
+- let numChecks = 0;
+- const scheduleFallbackCheck = (delay) => {
+- state.scheduledWork.timeout(checkHasScrolled, delay, "checkFinishedScrollFallback");
+- };
+- const checkHasScrolled = () => {
+- var _a3, _b, _c, _d;
+- const isStillScrollingTo = state.scrollingTo;
+- if (isStillScrollingTo) {
+- numChecks++;
+- const isNativeInitialPending = isNativeInitialNonZeroTarget(state) && !state.hasScrolled;
+- const maxChecks = silentInitialDispatch ? 5 : isNativeInitialPending ? INITIAL_SCROLL_MAX_FALLBACK_CHECKS : 5;
+- const shouldFinishZeroTarget = shouldFinishInitialZeroTargetScroll(ctx);
+- const canFinishInitialScrollWithoutNativeProgress = shouldFinishInitialScrollWithoutNativeProgress(
+- state,
+- isStillScrollingTo
+- );
+- const completionState = getResolvedScrollCompletionState(ctx, isStillScrollingTo);
+- const canFinishAfterSilentNativeDispatch = Platform.OS === "android" && silentInitialDispatch && completionState.isAtResolvedTarget && numChecks >= 1;
+- const shouldRetrySilentInitialNativeScroll = Platform.OS === "android" && canFinishAfterSilentNativeDispatch && !initialScrollCompletion.didRetrySilentInitialScroll(state);
+- const shouldFinishAfterObservedScroll = state.hasScrolled && (!isStillScrollingTo.isInitialScroll || completionState.isAtResolvedTarget);
+- const shouldRetryUnalignedInitialScroll = isStillScrollingTo.isInitialScroll && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
+- const shouldRetryUnalignedEndScroll = Platform.OS === "ios" && !isStillScrollingTo.isInitialScroll && isEndAlignedLastItemTarget(ctx, isStillScrollingTo) && !completionState.isAtResolvedTarget && numChecks <= maxChecks;
+- if (shouldRetrySilentInitialNativeScroll) {
+- const targetOffset = (_b = (_a3 = getInitialScrollWatchdogTargetOffset(state)) != null ? _a3 : isStillScrollingTo.targetOffset) != null ? _b : 0;
+- const jiggleOffset = targetOffset >= SILENT_INITIAL_SCROLL_TARGET_EPSILON ? targetOffset - SILENT_INITIAL_SCROLL_TARGET_EPSILON : targetOffset + SILENT_INITIAL_SCROLL_TARGET_EPSILON;
+- initialScrollCompletion.markSilentInitialScrollRetry(state);
+- scrollToFallbackOffset(ctx, jiggleOffset);
+- state.scheduledWork.frame(
+- () => scrollToFallbackOffset(ctx, targetOffset),
+- "checkFinishedScrollRetryFrame"
++ 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"
+ );
+- scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
+- } else if (shouldRetryUnalignedEndScroll) {
+- scrollToFallbackOffset(ctx, completionState.clampedTargetOffset);
+- scheduleFallbackCheck(100);
+- } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
+- finishScrollTo(ctx);
+- } else if ((isNativeInitialPending || shouldRetryUnalignedInitialScroll) && numChecks <= maxChecks) {
+- const targetOffset = (_d = (_c = getInitialScrollWatchdogTargetOffset(state)) != null ? _c : isStillScrollingTo.targetOffset) != null ? _d : state.scrollPending;
+- scrollToFallbackOffset(ctx, targetOffset);
+- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+- } else {
+- scheduleFallbackCheck(silentInitialDispatch ? SILENT_INITIAL_SCROLL_RETRY_DELAY_MS : 100);
+ }
++ } 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);
+ };
+- scheduleFallbackCheck(initialDelay);
+-}
+-
+-// src/core/doScrollTo.native.ts
+-function doScrollTo(ctx, params) {
+- const state = ctx.state;
+- const { animated, horizontal, isInitialScroll, offset } = params;
+- const isAnimated = !!animated;
+- const { refScroller } = state;
+- const scroller = refScroller.current;
+- if (!scroller) {
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
+ return;
+ }
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const nativeOffset = toNativeHorizontalOffset(state, offset, contentSize);
+- scroller.scrollTo({
+- animated: isAnimated,
+- x: isHorizontal ? nativeOffset : 0,
+- y: isHorizontal ? 0 : offset
+- });
+- if (isInitialScroll) {
+- initialScrollCompletion.markInitialScrollNativeDispatch(state);
+- }
+- if (isAnimated && Math.abs(state.scroll - offset) <= 1) {
+- checkFinishedScroll(ctx);
+- }
+- if (!isAnimated) {
+- state.scroll = offset;
+- checkFinishedScrollFallback(ctx);
+- }
++ complete();
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1591,60 +1711,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const dataChanged = source === "data";
+- const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
+- const doit = () => {
+- if (needsScrollWorkaround) {
+- doScrollTo(ctx, { horizontal: state.props.horizontal, offset: state.scroll });
+- } else {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- if (Platform.OS !== "web" && source !== "item-size") {
+- const threshold = state.scroll - positionDiff / 2;
+- if (!state.ignoreScrollFromMVCP) {
+- state.ignoreScrollFromMVCP = {};
+- }
+- if (positionDiff > 0) {
+- state.ignoreScrollFromMVCP.lt = threshold;
+- } else {
+- state.ignoreScrollFromMVCP.gt = threshold;
+- }
+- const delay = needsScrollWorkaround ? 250 : 100;
+- state.scheduledWork.timeout(
+- () => {
+- var _a3;
+- state.ignoreScrollFromMVCP = void 0;
+- const shouldForceUpdate = state.ignoreScrollFromMVCPIgnored && state.scrollProcessingEnabled !== false;
+- if (shouldForceUpdate) {
+- state.ignoreScrollFromMVCPIgnored = false;
+- state.scrollPending = state.scroll;
+- (_a3 = state.reprocessCurrentScroll) == null ? void 0 : _a3.call(state);
+- }
+- },
+- delay,
+- "ignoreScrollFromMVCP"
+- );
+- }
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2281,7 +2347,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 {
+@@ -2313,6 +2379,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2320,7 +2395,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 });
+@@ -4394,7 +4469,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -5919,6 +5995,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;
+@@ -5928,6 +6005,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7015,13 +7095,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+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 b2240b5..cbff6eb 100644
+--- a/node_modules/@legendapp/list/react-native.web.js
++++ b/node_modules/@legendapp/list/react-native.web.js
+@@ -430,171 +430,271 @@ 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();
++// src/core/getStartOffsetAdjustment.ts
++function getStartOffsetAdjustment(ctx) {
++ const { state } = ctx;
++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
+ }
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
++
++// src/utils/getId.ts
++function getId(state, index) {
++ const { data, keyExtractor } = state.props;
++ if (!data) {
++ return "";
++ }
++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
++ const id = ret;
++ state.idCache[index] = id;
++ return id;
+ }
+
+-// 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 }
++// src/core/updateContentMetricsState.ts
++function getRawContentLength(ctx) {
++ var _a3, _b, _c;
++ const { state, values } = ctx;
++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++}
++function getAlignItemsAtEndPadding(ctx) {
++ const { state } = ctx;
++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
+ }
+- };
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
++ }
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
+ }
+ }
+
+-// 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));
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++ const state = ctx.state;
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ }
++ sizes.set(itemKey, size);
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-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
+- });
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
+ }
+- (_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;
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
++ }
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
+ }
+- 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;
++ return -1;
++}
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
++ }
++ }
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -817,626 +917,616 @@ function recalculateSettledScroll(ctx) {
+ }
+ 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) {
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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")) {
++// src/core/doScrollTo.ts
++var SCROLL_END_IDLE_MS = 80;
++var SCROLL_END_MAX_MS = 1500;
++var SMOOTH_SCROLL_DURATION_MS = 320;
++var SCROLL_END_TARGET_EPSILON = 1;
++function doScrollTo(ctx, params) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { animated, horizontal, offset } = params;
++ state.scheduledWork.cancel("platformScrollCompletion");
++ const scroller = state.refScroller.current;
++ const node = scroller == null ? void 0 : scroller.getScrollableNode();
++ if (!scroller || !node) {
+ 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;
++ const isAnimated = !!animated;
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
++ const top = isHorizontal ? 0 : offset;
++ scroller.scrollTo({ animated: isAnimated, x: left, y: top });
++ if (isAnimated) {
++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
++ listenForScrollEnd(ctx, {
++ readOffset: () => scroller.getCurrentScrollOffset(),
++ target,
++ targetOffset: offset
++ });
++ } else {
++ state.scroll = offset;
++ const targetToken = state.scrollingTo;
++ state.scheduledWork.timeout(
++ () => {
++ if (targetToken === state.scrollingTo) {
++ finishScrollTo(ctx);
++ }
++ },
++ 100,
++ "platformScrollCompletion"
++ );
+ }
+- 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;
++function listenForScrollEnd(ctx, params) {
++ const { readOffset, target, targetOffset } = params;
++ if (!target) {
++ finishScrollTo(ctx);
++ return;
+ }
+- 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);
++ const supportsScrollEnd = "onscrollend" in target;
++ let idleTimeout;
++ let settled = false;
++ const { scheduledWork, scrollingTo: targetToken } = ctx.state;
++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
++ const cleanup = () => {
++ target.removeEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.removeEventListener("scrollend", onScrollEnd);
+ }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
+ }
+- }
+-}
+-
+-// 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);
++ clearTimeout(maxTimeout);
++ };
++ const cancel = () => {
++ if (!settled) {
++ settled = true;
++ cleanup();
+ }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
++ };
++ const finish = (reason) => {
++ if (settled) return;
++ if (targetToken !== ctx.state.scrollingTo) {
++ scheduledWork.cancel("platformScrollCompletion");
++ return;
+ }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
++ const currentOffset = readOffset();
++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
++ if (reason === "scrollend" && !isNearTarget) {
++ return;
++ }
++ scheduledWork.cancel("platformScrollCompletion");
++ finishScrollTo(ctx);
++ };
++ const onScroll2 = () => {
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
++ }
++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ };
++ const onScrollEnd = () => finish("scrollend");
++ target.addEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.addEventListener("scrollend", onScrollEnd);
++ } else {
++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
+ }
++ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
++
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ 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"
+- );
++ if (Math.abs(positionDiff) > 0.1) {
++ const doit = () => {
++ {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
+ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
+ } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+- (_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) {
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++}
++function settleScrollTarget(ctx) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ 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;
++ }
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
+- const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++// 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/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
++// 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
++ )
++ );
+ }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
+ }
+
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 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 getAlignItemsAtEndPadding(ctx) {
+- const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
+- const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++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;
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+- const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
++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;
+ }
+- } else {
+- totalSize += add;
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
+ }
+- if (prevTotalSize !== totalSize) {
+- {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
+- updateContentMetricsState(ctx);
++};
++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;
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
++ 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);
+ }
+-}
+-
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+- const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
++ 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;
+ }
+-function isArray(obj) {
+- return Array.isArray(obj);
++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");
+ }
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++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 roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
++function setAdaptiveRender(ctx, mode, reason) {
+ var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
+- }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ 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);
+ }
+- return -1;
+ }
+-
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++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 { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
++ 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);
+ }
+ }
+- return size;
+ }
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++
++// 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 areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
+- }
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- return true;
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
+ }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-
+-// 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;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
+ }
+- 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;
++ 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 });
++ }
+ }
+ }
+- 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/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;
+ }
+-
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ 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 (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
+ }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
+ }
+ }
+-
+-// src/core/doScrollTo.ts
+-var SCROLL_END_IDLE_MS = 80;
+-var SCROLL_END_MAX_MS = 1500;
+-var SMOOTH_SCROLL_DURATION_MS = 320;
+-var SCROLL_END_TARGET_EPSILON = 1;
+-function doScrollTo(ctx, params) {
+- var _a3, _b;
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- const { animated, horizontal, offset } = params;
+- state.scheduledWork.cancel("platformScrollCompletion");
+- const scroller = state.refScroller.current;
+- const node = scroller == null ? void 0 : scroller.getScrollableNode();
+- if (!scroller || !node) {
+- return;
+- }
+- const isAnimated = !!animated;
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
+- const top = isHorizontal ? 0 : offset;
+- scroller.scrollTo({ animated: isAnimated, x: left, y: top });
+- if (isAnimated) {
+- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
+- listenForScrollEnd(ctx, {
+- readOffset: () => scroller.getCurrentScrollOffset(),
+- target,
+- targetOffset: offset
+- });
+- } else {
+- state.scroll = offset;
+- const targetToken = state.scrollingTo;
+- state.scheduledWork.timeout(
+- () => {
+- if (targetToken === state.scrollingTo) {
+- finishScrollTo(ctx);
+- }
+- },
+- 100,
+- "platformScrollCompletion"
+- );
+- }
+-}
+-function listenForScrollEnd(ctx, params) {
+- const { readOffset, target, targetOffset } = params;
+- if (!target) {
+- finishScrollTo(ctx);
+- return;
+- }
+- const supportsScrollEnd = "onscrollend" in target;
+- let idleTimeout;
+- let settled = false;
+- const { scheduledWork, scrollingTo: targetToken } = ctx.state;
+- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
+- const cleanup = () => {
+- target.removeEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.removeEventListener("scrollend", onScrollEnd);
+- }
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
+- }
+- clearTimeout(maxTimeout);
+- };
+- const cancel = () => {
+- if (!settled) {
+- settled = true;
+- cleanup();
++ 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 finish = (reason) => {
+- if (settled) return;
+- if (targetToken !== ctx.state.scrollingTo) {
+- scheduledWork.cancel("platformScrollCompletion");
+- return;
++ }
++ 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);
+ }
+- const currentOffset = readOffset();
+- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
+- if (reason === "scrollend" && !isNearTarget) {
+- return;
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
+ }
+- scheduledWork.cancel("platformScrollCompletion");
+- finishScrollTo(ctx);
+- };
+- const onScroll2 = () => {
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+ }
+- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+ };
+- const onScrollEnd = () => finish("scrollend");
+- target.addEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.addEventListener("scrollend", onScrollEnd);
+- } else {
+- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
+ }
+- scheduledWork.register("platformScrollCompletion", cancel);
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1483,30 +1573,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const doit = () => {
+- {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2141,7 +2207,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 {
+@@ -2173,6 +2239,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2180,7 +2255,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 });
+@@ -4444,7 +4519,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -6607,6 +6683,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;
+@@ -6616,6 +6693,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7679,13 +7759,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+diff --git a/node_modules/@legendapp/list/react-native.web.mjs b/node_modules/@legendapp/list/react-native.web.mjs
+index a97be05..83225ee 100644
+--- a/node_modules/@legendapp/list/react-native.web.mjs
++++ b/node_modules/@legendapp/list/react-native.web.mjs
+@@ -409,171 +409,271 @@ 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();
++// src/core/getStartOffsetAdjustment.ts
++function getStartOffsetAdjustment(ctx) {
++ const { state } = ctx;
++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
+ }
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
++
++// src/utils/getId.ts
++function getId(state, index) {
++ const { data, keyExtractor } = state.props;
++ if (!data) {
++ return "";
++ }
++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
++ const id = ret;
++ state.idCache[index] = id;
++ return id;
+ }
+
+-// 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 }
++// src/core/updateContentMetricsState.ts
++function getRawContentLength(ctx) {
++ var _a3, _b, _c;
++ const { state, values } = ctx;
++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++}
++function getAlignItemsAtEndPadding(ctx) {
++ const { state } = ctx;
++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
+ }
+- };
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
++ }
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
+ }
+ }
+
+-// 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));
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++ const state = ctx.state;
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ }
++ sizes.set(itemKey, size);
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-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
+- });
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
+ }
+- (_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;
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
++ }
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
+ }
+- 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;
++ return -1;
++}
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
++ }
++ }
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -796,626 +896,616 @@ function recalculateSettledScroll(ctx) {
+ }
+ 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) {
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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")) {
++// src/core/doScrollTo.ts
++var SCROLL_END_IDLE_MS = 80;
++var SCROLL_END_MAX_MS = 1500;
++var SMOOTH_SCROLL_DURATION_MS = 320;
++var SCROLL_END_TARGET_EPSILON = 1;
++function doScrollTo(ctx, params) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { animated, horizontal, offset } = params;
++ state.scheduledWork.cancel("platformScrollCompletion");
++ const scroller = state.refScroller.current;
++ const node = scroller == null ? void 0 : scroller.getScrollableNode();
++ if (!scroller || !node) {
+ 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;
++ const isAnimated = !!animated;
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
++ const top = isHorizontal ? 0 : offset;
++ scroller.scrollTo({ animated: isAnimated, x: left, y: top });
++ if (isAnimated) {
++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
++ listenForScrollEnd(ctx, {
++ readOffset: () => scroller.getCurrentScrollOffset(),
++ target,
++ targetOffset: offset
++ });
++ } else {
++ state.scroll = offset;
++ const targetToken = state.scrollingTo;
++ state.scheduledWork.timeout(
++ () => {
++ if (targetToken === state.scrollingTo) {
++ finishScrollTo(ctx);
++ }
++ },
++ 100,
++ "platformScrollCompletion"
++ );
+ }
+- 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;
++function listenForScrollEnd(ctx, params) {
++ const { readOffset, target, targetOffset } = params;
++ if (!target) {
++ finishScrollTo(ctx);
++ return;
+ }
+- 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);
++ const supportsScrollEnd = "onscrollend" in target;
++ let idleTimeout;
++ let settled = false;
++ const { scheduledWork, scrollingTo: targetToken } = ctx.state;
++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
++ const cleanup = () => {
++ target.removeEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.removeEventListener("scrollend", onScrollEnd);
+ }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
+ }
+- }
+-}
+-
+-// 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);
++ clearTimeout(maxTimeout);
++ };
++ const cancel = () => {
++ if (!settled) {
++ settled = true;
++ cleanup();
+ }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
++ };
++ const finish = (reason) => {
++ if (settled) return;
++ if (targetToken !== ctx.state.scrollingTo) {
++ scheduledWork.cancel("platformScrollCompletion");
++ return;
+ }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
++ const currentOffset = readOffset();
++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
++ if (reason === "scrollend" && !isNearTarget) {
++ return;
++ }
++ scheduledWork.cancel("platformScrollCompletion");
++ finishScrollTo(ctx);
++ };
++ const onScroll2 = () => {
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
++ }
++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ };
++ const onScrollEnd = () => finish("scrollend");
++ target.addEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.addEventListener("scrollend", onScrollEnd);
++ } else {
++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
+ }
++ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
++
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ 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"
+- );
++ if (Math.abs(positionDiff) > 0.1) {
++ const doit = () => {
++ {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
+ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
+ } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+- (_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) {
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++}
++function settleScrollTarget(ctx) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ 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;
++ }
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
+- const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++// 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/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
++// 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
++ )
++ );
+ }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
+ }
+
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 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 getAlignItemsAtEndPadding(ctx) {
+- const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
+- const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++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;
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+- const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
++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;
+ }
+- } else {
+- totalSize += add;
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
+ }
+- if (prevTotalSize !== totalSize) {
+- {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
+- updateContentMetricsState(ctx);
++};
++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;
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
++ 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);
+ }
+-}
+-
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+- const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
++ 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;
+ }
+-function isArray(obj) {
+- return Array.isArray(obj);
++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");
+ }
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++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 roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
++function setAdaptiveRender(ctx, mode, reason) {
+ var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
+- }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ 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);
+ }
+- return -1;
+ }
+-
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++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 { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
++ 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);
+ }
+ }
+- return size;
+ }
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++
++// 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 areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
+- }
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- return true;
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
+ }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-
+-// 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;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
+ }
+- 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;
++ 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 });
++ }
+ }
+ }
+- 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/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;
+ }
+-
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ 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 (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
+ }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
+ }
+ }
+-
+-// src/core/doScrollTo.ts
+-var SCROLL_END_IDLE_MS = 80;
+-var SCROLL_END_MAX_MS = 1500;
+-var SMOOTH_SCROLL_DURATION_MS = 320;
+-var SCROLL_END_TARGET_EPSILON = 1;
+-function doScrollTo(ctx, params) {
+- var _a3, _b;
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- const { animated, horizontal, offset } = params;
+- state.scheduledWork.cancel("platformScrollCompletion");
+- const scroller = state.refScroller.current;
+- const node = scroller == null ? void 0 : scroller.getScrollableNode();
+- if (!scroller || !node) {
+- return;
+- }
+- const isAnimated = !!animated;
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
+- const top = isHorizontal ? 0 : offset;
+- scroller.scrollTo({ animated: isAnimated, x: left, y: top });
+- if (isAnimated) {
+- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
+- listenForScrollEnd(ctx, {
+- readOffset: () => scroller.getCurrentScrollOffset(),
+- target,
+- targetOffset: offset
+- });
+- } else {
+- state.scroll = offset;
+- const targetToken = state.scrollingTo;
+- state.scheduledWork.timeout(
+- () => {
+- if (targetToken === state.scrollingTo) {
+- finishScrollTo(ctx);
+- }
+- },
+- 100,
+- "platformScrollCompletion"
+- );
+- }
+-}
+-function listenForScrollEnd(ctx, params) {
+- const { readOffset, target, targetOffset } = params;
+- if (!target) {
+- finishScrollTo(ctx);
+- return;
+- }
+- const supportsScrollEnd = "onscrollend" in target;
+- let idleTimeout;
+- let settled = false;
+- const { scheduledWork, scrollingTo: targetToken } = ctx.state;
+- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
+- const cleanup = () => {
+- target.removeEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.removeEventListener("scrollend", onScrollEnd);
+- }
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
+- }
+- clearTimeout(maxTimeout);
+- };
+- const cancel = () => {
+- if (!settled) {
+- settled = true;
+- cleanup();
++ 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 finish = (reason) => {
+- if (settled) return;
+- if (targetToken !== ctx.state.scrollingTo) {
+- scheduledWork.cancel("platformScrollCompletion");
+- return;
++ }
++ 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);
+ }
+- const currentOffset = readOffset();
+- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
+- if (reason === "scrollend" && !isNearTarget) {
+- return;
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
+ }
+- scheduledWork.cancel("platformScrollCompletion");
+- finishScrollTo(ctx);
+- };
+- const onScroll2 = () => {
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+ }
+- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+ };
+- const onScrollEnd = () => finish("scrollend");
+- target.addEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.addEventListener("scrollend", onScrollEnd);
+- } else {
+- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
+ }
+- scheduledWork.register("platformScrollCompletion", cancel);
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1462,30 +1552,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const doit = () => {
+- {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2120,7 +2186,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 {
+@@ -2152,6 +2218,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2159,7 +2234,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 });
+@@ -4423,7 +4498,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -6586,6 +6662,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;
+@@ -6595,6 +6672,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7658,13 +7738,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+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 b2240b5..cbff6eb 100644
+--- a/node_modules/@legendapp/list/react.js
++++ b/node_modules/@legendapp/list/react.js
+@@ -430,171 +430,271 @@ 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();
++// src/core/getStartOffsetAdjustment.ts
++function getStartOffsetAdjustment(ctx) {
++ const { state } = ctx;
++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
+ }
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
++
++// src/utils/getId.ts
++function getId(state, index) {
++ const { data, keyExtractor } = state.props;
++ if (!data) {
++ return "";
++ }
++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
++ const id = ret;
++ state.idCache[index] = id;
++ return id;
+ }
+
+-// 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 }
++// src/core/updateContentMetricsState.ts
++function getRawContentLength(ctx) {
++ var _a3, _b, _c;
++ const { state, values } = ctx;
++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++}
++function getAlignItemsAtEndPadding(ctx) {
++ const { state } = ctx;
++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
+ }
+- };
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
++ }
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
+ }
+ }
+
+-// 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));
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++ const state = ctx.state;
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ }
++ sizes.set(itemKey, size);
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-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
+- });
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
+ }
+- (_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;
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
++ }
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
+ }
+- 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;
++ return -1;
++}
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
++ }
++ }
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -817,626 +917,616 @@ function recalculateSettledScroll(ctx) {
+ }
+ 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) {
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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")) {
++// src/core/doScrollTo.ts
++var SCROLL_END_IDLE_MS = 80;
++var SCROLL_END_MAX_MS = 1500;
++var SMOOTH_SCROLL_DURATION_MS = 320;
++var SCROLL_END_TARGET_EPSILON = 1;
++function doScrollTo(ctx, params) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { animated, horizontal, offset } = params;
++ state.scheduledWork.cancel("platformScrollCompletion");
++ const scroller = state.refScroller.current;
++ const node = scroller == null ? void 0 : scroller.getScrollableNode();
++ if (!scroller || !node) {
+ 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;
++ const isAnimated = !!animated;
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
++ const top = isHorizontal ? 0 : offset;
++ scroller.scrollTo({ animated: isAnimated, x: left, y: top });
++ if (isAnimated) {
++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
++ listenForScrollEnd(ctx, {
++ readOffset: () => scroller.getCurrentScrollOffset(),
++ target,
++ targetOffset: offset
++ });
++ } else {
++ state.scroll = offset;
++ const targetToken = state.scrollingTo;
++ state.scheduledWork.timeout(
++ () => {
++ if (targetToken === state.scrollingTo) {
++ finishScrollTo(ctx);
++ }
++ },
++ 100,
++ "platformScrollCompletion"
++ );
+ }
+- 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;
++function listenForScrollEnd(ctx, params) {
++ const { readOffset, target, targetOffset } = params;
++ if (!target) {
++ finishScrollTo(ctx);
++ return;
+ }
+- 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);
++ const supportsScrollEnd = "onscrollend" in target;
++ let idleTimeout;
++ let settled = false;
++ const { scheduledWork, scrollingTo: targetToken } = ctx.state;
++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
++ const cleanup = () => {
++ target.removeEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.removeEventListener("scrollend", onScrollEnd);
+ }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
+ }
+- }
+-}
+-
+-// 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);
++ clearTimeout(maxTimeout);
++ };
++ const cancel = () => {
++ if (!settled) {
++ settled = true;
++ cleanup();
+ }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
++ };
++ const finish = (reason) => {
++ if (settled) return;
++ if (targetToken !== ctx.state.scrollingTo) {
++ scheduledWork.cancel("platformScrollCompletion");
++ return;
+ }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
++ const currentOffset = readOffset();
++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
++ if (reason === "scrollend" && !isNearTarget) {
++ return;
++ }
++ scheduledWork.cancel("platformScrollCompletion");
++ finishScrollTo(ctx);
++ };
++ const onScroll2 = () => {
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
++ }
++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ };
++ const onScrollEnd = () => finish("scrollend");
++ target.addEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.addEventListener("scrollend", onScrollEnd);
++ } else {
++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
+ }
++ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
++
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ 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"
+- );
++ if (Math.abs(positionDiff) > 0.1) {
++ const doit = () => {
++ {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
+ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
+ } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+- (_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) {
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++}
++function settleScrollTarget(ctx) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ 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;
++ }
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
+- const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++// 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/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
++// 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
++ )
++ );
+ }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
+ }
+
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 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 getAlignItemsAtEndPadding(ctx) {
+- const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
+- const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++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;
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+- const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
++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;
+ }
+- } else {
+- totalSize += add;
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
+ }
+- if (prevTotalSize !== totalSize) {
+- {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
+- updateContentMetricsState(ctx);
++};
++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;
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
++ 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);
+ }
+-}
+-
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+- const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
++ 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;
+ }
+-function isArray(obj) {
+- return Array.isArray(obj);
++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");
+ }
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++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 roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
++function setAdaptiveRender(ctx, mode, reason) {
+ var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
+- }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ 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);
+ }
+- return -1;
+ }
+-
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++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 { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
++ 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);
+ }
+ }
+- return size;
+ }
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++
++// 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 areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
+- }
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- return true;
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
+ }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-
+-// 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;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
+ }
+- 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;
++ 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 });
++ }
+ }
+ }
+- 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/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;
+ }
+-
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ 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 (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
+ }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
+ }
+ }
+-
+-// src/core/doScrollTo.ts
+-var SCROLL_END_IDLE_MS = 80;
+-var SCROLL_END_MAX_MS = 1500;
+-var SMOOTH_SCROLL_DURATION_MS = 320;
+-var SCROLL_END_TARGET_EPSILON = 1;
+-function doScrollTo(ctx, params) {
+- var _a3, _b;
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- const { animated, horizontal, offset } = params;
+- state.scheduledWork.cancel("platformScrollCompletion");
+- const scroller = state.refScroller.current;
+- const node = scroller == null ? void 0 : scroller.getScrollableNode();
+- if (!scroller || !node) {
+- return;
+- }
+- const isAnimated = !!animated;
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
+- const top = isHorizontal ? 0 : offset;
+- scroller.scrollTo({ animated: isAnimated, x: left, y: top });
+- if (isAnimated) {
+- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
+- listenForScrollEnd(ctx, {
+- readOffset: () => scroller.getCurrentScrollOffset(),
+- target,
+- targetOffset: offset
+- });
+- } else {
+- state.scroll = offset;
+- const targetToken = state.scrollingTo;
+- state.scheduledWork.timeout(
+- () => {
+- if (targetToken === state.scrollingTo) {
+- finishScrollTo(ctx);
+- }
+- },
+- 100,
+- "platformScrollCompletion"
+- );
+- }
+-}
+-function listenForScrollEnd(ctx, params) {
+- const { readOffset, target, targetOffset } = params;
+- if (!target) {
+- finishScrollTo(ctx);
+- return;
+- }
+- const supportsScrollEnd = "onscrollend" in target;
+- let idleTimeout;
+- let settled = false;
+- const { scheduledWork, scrollingTo: targetToken } = ctx.state;
+- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
+- const cleanup = () => {
+- target.removeEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.removeEventListener("scrollend", onScrollEnd);
+- }
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
+- }
+- clearTimeout(maxTimeout);
+- };
+- const cancel = () => {
+- if (!settled) {
+- settled = true;
+- cleanup();
++ 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 finish = (reason) => {
+- if (settled) return;
+- if (targetToken !== ctx.state.scrollingTo) {
+- scheduledWork.cancel("platformScrollCompletion");
+- return;
++ }
++ 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);
+ }
+- const currentOffset = readOffset();
+- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
+- if (reason === "scrollend" && !isNearTarget) {
+- return;
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
+ }
+- scheduledWork.cancel("platformScrollCompletion");
+- finishScrollTo(ctx);
+- };
+- const onScroll2 = () => {
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+ }
+- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+ };
+- const onScrollEnd = () => finish("scrollend");
+- target.addEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.addEventListener("scrollend", onScrollEnd);
+- } else {
+- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
+ }
+- scheduledWork.register("platformScrollCompletion", cancel);
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1483,30 +1573,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const doit = () => {
+- {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2141,7 +2207,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 {
+@@ -2173,6 +2239,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2180,7 +2255,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 });
+@@ -4444,7 +4519,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -6607,6 +6683,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;
+@@ -6616,6 +6693,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7679,13 +7759,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+diff --git a/node_modules/@legendapp/list/react.mjs b/node_modules/@legendapp/list/react.mjs
+index a97be05..83225ee 100644
+--- a/node_modules/@legendapp/list/react.mjs
++++ b/node_modules/@legendapp/list/react.mjs
+@@ -409,171 +409,271 @@ 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();
++// src/core/getStartOffsetAdjustment.ts
++function getStartOffsetAdjustment(ctx) {
++ const { state } = ctx;
++ const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
++ return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
+ }
+-function cancelImperativeScroll(state) {
+- cancelScrollCompletionChecks(state);
+- state.scheduledWork.cancel("imperativeScrollReady");
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- settlePendingImperativeScroll(state);
++
++// src/utils/getId.ts
++function getId(state, index) {
++ const { data, keyExtractor } = state.props;
++ if (!data) {
++ return "";
++ }
++ const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
++ const id = ret;
++ state.idCache[index] = id;
++ return id;
+ }
+
+-// 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 }
++// src/core/updateContentMetricsState.ts
++function getRawContentLength(ctx) {
++ var _a3, _b, _c;
++ const { state, values } = ctx;
++ return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 0);
++}
++function getAlignItemsAtEndPadding(ctx) {
++ const { state } = ctx;
++ const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
++ return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++}
++function updateContentMetricsState(ctx) {
++ var _a3;
++ const { state } = ctx;
++ const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
++ const nextPadding = getAlignItemsAtEndPadding(ctx);
++ if (previousPadding !== nextPadding) {
++ const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
++ const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
++ if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
++ state.scheduledWork.microtask(() => {
++ if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
++ set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
++ }
++ }, "alignItemsAtEndPaddingFallback");
++ } else if (!isMaintainExpected) {
++ set$(ctx, "alignItemsAtEndPadding", nextPadding);
+ }
+- };
++ }
+ }
+-function releaseDeferredPublicOnScroll(ctx, resolvedOffset) {
+- var _a3, _b, _c, _d;
++
++// src/core/addTotalSize.ts
++function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+ 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
+- )
+- );
++ const prevTotalSize = state.totalSize;
++ let totalSize = state.totalSize;
++ if (key === null) {
++ totalSize = add;
++ if (state.timeoutSetPaddingTop) {
++ clearTimeout(state.timeoutSetPaddingTop);
++ state.timeoutSetPaddingTop = void 0;
++ }
++ } else {
++ totalSize += add;
++ }
++ if (prevTotalSize !== totalSize) {
++ {
++ state.pendingTotalSize = void 0;
++ state.totalSize = totalSize;
++ if (notifyTotalSize) {
++ set$(ctx, "totalSize", totalSize);
++ }
++ updateContentMetricsState(ctx);
++ }
++ } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
++ set$(ctx, "totalSize", totalSize);
+ }
+ }
+
+-// 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));
++// src/core/setSize.ts
++function setSize(ctx, itemKey, size, notifyTotalSize = true) {
++ const state = ctx.state;
++ const { sizes } = state;
++ const previousSize = sizes.get(itemKey);
++ const diff = previousSize !== void 0 ? size - previousSize : size;
++ if (diff !== 0) {
++ addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ }
++ sizes.set(itemKey, size);
+ }
+-function clearInitialScrollSession(state) {
+- state.initialScrollSession = void 0;
+- return void 0;
++
++// src/utils/helpers.ts
++function isFunction(obj) {
++ return typeof obj === "function";
+ }
+-function createInitialScrollSession(options) {
+- const { bootstrap, completion, kind, previousDataLength } = options;
+- return kind === "offset" ? {
+- completion,
+- kind,
+- previousDataLength
+- } : {
+- bootstrap,
+- completion,
+- kind,
+- previousDataLength
+- };
++function isArray(obj) {
++ return Array.isArray(obj);
+ }
+-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
+- });
++var warned = /* @__PURE__ */ new Set();
++function warnDevOnce(id, text) {
++ if (IS_DEV && !warned.has(id)) {
++ warned.add(id);
++ console.warn(`[legend-list] ${text}`);
+ }
+- (_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;
++function roundSize(size) {
++ return Math.floor(size * 8) / 8;
++}
++function isNullOrUndefined(value) {
++ return value === null || value === void 0;
++}
++function getPadding(s, type) {
++ var _a3, _b, _c;
++ const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
++ return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
++}
++function extractPadding(style, contentContainerStyle, type) {
++ return getPadding(style, type) + getPadding(contentContainerStyle, type);
++}
++function findContainerId(ctx, key) {
++ var _a3, _b;
++ const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
++ if (directMatch !== void 0) {
++ return directMatch;
++ }
++ const numContainers = peek$(ctx, "numContainers");
++ for (let i = 0; i < numContainers; i++) {
++ const itemKey = peek$(ctx, `containerItemKey${i}`);
++ if (itemKey === key) {
++ return i;
+ }
+- 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;
++ return -1;
++}
++
++// src/utils/getItemSize.ts
++function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { getFixedItemSize, getItemType } = state.props;
++ let size = key ? state.sizesKnown.get(key) : void 0;
++ if (size === void 0 && key && getFixedItemSize) {
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
++ if (fixedSize !== void 0) {
++ size = fixedSize + ctx.scrollAxisGap;
++ state.sizesKnown.set(key, size);
+ }
+- const completion = ensureInitialScrollSessionCompletion(state);
+- completion.watchdog = watchdog ? {
+- startScroll: watchdog.startScroll,
+- targetOffset: watchdog.targetOffset
+- } : void 0;
+ }
+-};
+-function setInitialScrollSession(state, options = {}) {
++ return size;
++}
++function getKnownOrFixedItemSize(ctx, index) {
++ const key = getId(ctx.state, index);
++ return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++}
++function areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
++ for (let index = startIndex; index <= endIndex; index++) {
++ if (getKnownOrFixedItemSize(ctx, index) === void 0) {
++ return false;
++ }
++ }
++ return true;
++}
++function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+ 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);
++ const state = ctx.state;
++ const {
++ sizes,
++ averageSizes,
++ props: { estimatedItemSize, getItemType },
++ scrollingTo
++ } = state;
++ const sizeKnown = state.sizesKnown.get(key);
++ if (sizeKnown !== void 0) {
++ return sizeKnown;
+ }
+- if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
+- return clearInitialScrollSession(state);
++ let size;
++ const renderedSize = sizes.get(key);
++ if (preferCachedSize) {
++ if (renderedSize !== void 0) {
++ return renderedSize;
++ }
+ }
+- 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;
++ size = getKnownOrFixedSize(ctx, key, index, data, resolved);
++ if (size !== void 0) {
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++ }
++ const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
++ if (useAverageSize && !scrollingTo) {
++ const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0 && renderedSize !== void 0) {
++ return renderedSize;
++ }
++ if (size === void 0 && useAverageSize && scrollingTo) {
++ const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
++ if (averageSizeForType !== void 0) {
++ size = roundSize(averageSizeForType);
++ }
++ }
++ if (size === void 0) {
++ size = estimatedItemSize + ctx.scrollAxisGap;
++ }
++ setSize(ctx, key, size, notifyTotalSize);
++ return size;
++}
++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]);
++}
++
++// 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
+@@ -796,626 +896,616 @@ function recalculateSettledScroll(ctx) {
+ }
+ 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) {
++
++// src/core/finishScrollTo.ts
++function finishScrollTo(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);
++ if (state == null ? void 0 : state.scrollingTo) {
++ cancelScrollCompletionChecks(state);
++ const resolvePendingScroll = state.pendingScrollResolve;
++ state.pendingScrollResolve = void 0;
++ const scrollingTo = state.scrollingTo;
++ state.scrollHistory.length = 0;
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
++ if (state.pendingTotalSize !== void 0) {
++ addTotalSize(ctx, null, state.pendingTotalSize);
++ }
++ {
++ state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ }
++ if (scrollingTo.isInitialScroll || state.initialScroll) {
++ const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
++ const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
++ finishInitialScroll(ctx, {
++ onFinished: () => {
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ },
++ preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
++ recalculateItems: true,
++ schedulePreservedTargetClear: shouldPreserveResizeTarget,
++ syncObservedOffset: isOffsetSession,
++ waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
++ });
++ return;
+ }
++ recalculateSettledScroll(ctx);
++ resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+ }
+ }
+
+-// 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")) {
++// src/core/doScrollTo.ts
++var SCROLL_END_IDLE_MS = 80;
++var SCROLL_END_MAX_MS = 1500;
++var SMOOTH_SCROLL_DURATION_MS = 320;
++var SCROLL_END_TARGET_EPSILON = 1;
++function doScrollTo(ctx, params) {
++ var _a3, _b;
++ const state = ctx.state;
++ const { animated, horizontal, offset } = params;
++ state.scheduledWork.cancel("platformScrollCompletion");
++ const scroller = state.refScroller.current;
++ const node = scroller == null ? void 0 : scroller.getScrollableNode();
++ if (!scroller || !node) {
+ 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;
++ const isAnimated = !!animated;
++ const isHorizontal = !!horizontal;
++ const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
++ const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
++ const top = isHorizontal ? 0 : offset;
++ scroller.scrollTo({ animated: isAnimated, x: left, y: top });
++ if (isAnimated) {
++ const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
++ listenForScrollEnd(ctx, {
++ readOffset: () => scroller.getCurrentScrollOffset(),
++ target,
++ targetOffset: offset
++ });
++ } else {
++ state.scroll = offset;
++ const targetToken = state.scrollingTo;
++ state.scheduledWork.timeout(
++ () => {
++ if (targetToken === state.scrollingTo) {
++ finishScrollTo(ctx);
++ }
++ },
++ 100,
++ "platformScrollCompletion"
++ );
+ }
+- 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;
++function listenForScrollEnd(ctx, params) {
++ const { readOffset, target, targetOffset } = params;
++ if (!target) {
++ finishScrollTo(ctx);
++ return;
+ }
+- 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);
++ const supportsScrollEnd = "onscrollend" in target;
++ let idleTimeout;
++ let settled = false;
++ const { scheduledWork, scrollingTo: targetToken } = ctx.state;
++ const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
++ const cleanup = () => {
++ target.removeEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.removeEventListener("scrollend", onScrollEnd);
+ }
+- if (!state.didLoad) {
+- state.didLoad = true;
+- if (onLoad) {
+- onLoad({ elapsedTimeInMs: Date.now() - loadStartTime });
+- }
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
+ }
+- }
+-}
+-
+-// 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);
++ clearTimeout(maxTimeout);
++ };
++ const cancel = () => {
++ if (!settled) {
++ settled = true;
++ cleanup();
+ }
+- if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
+- cancelScrollCompletionChecks(state);
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
++ };
++ const finish = (reason) => {
++ if (settled) return;
++ if (targetToken !== ctx.state.scrollingTo) {
++ scheduledWork.cancel("platformScrollCompletion");
++ return;
+ }
+- initialScrollCompletion.resetFlags(state);
+- setInitialScrollSession(state, { bootstrap: null });
+- finishInitialScroll(ctx);
++ const currentOffset = readOffset();
++ const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
++ if (reason === "scrollend" && !isNearTarget) {
++ return;
++ }
++ scheduledWork.cancel("platformScrollCompletion");
++ finishScrollTo(ctx);
++ };
++ const onScroll2 = () => {
++ if (idleTimeout !== void 0) {
++ clearTimeout(idleTimeout);
++ }
++ idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ };
++ const onScrollEnd = () => finish("scrollend");
++ target.addEventListener("scroll", onScroll2);
++ if (supportsScrollEnd) {
++ target.addEventListener("scrollend", onScrollEnd);
++ } else {
++ idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
+ }
++ scheduledWork.register("platformScrollCompletion", cancel);
+ }
+-function finishInitialScroll(ctx, options) {
+- var _a3, _b, _c;
++
++// src/utils/requestAdjust.ts
++function requestAdjust(ctx, positionDiff, source) {
+ 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"
+- );
++ if (Math.abs(positionDiff) > 0.1) {
++ const doit = () => {
++ {
++ state.scrollAdjustHandler.requestAdjust(positionDiff);
++ if (state.adjustingFromInitialMount) {
++ state.adjustingFromInitialMount--;
++ }
+ }
++ };
++ state.scroll += positionDiff;
++ state.scrollForNextCalculateItemsInView = void 0;
++ const readyToRender = peek$(ctx, "readyToRender");
++ if (readyToRender) {
++ doit();
+ } else {
+- clearPreservedInitialScrollTarget(state);
+- }
+- if (options == null ? void 0 : options.recalculateItems) {
+- recalculateSettledScroll(ctx);
+- }
+- setInitialRenderState(ctx, { didInitialScroll: true });
+- if (shouldReleaseDeferredPublicOnScroll) {
+- releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
++ state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
++ requestAnimationFrame(doit);
+ }
+- (_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) {
++// src/core/scrollTargetSettle.ts
++var SETTLE_POSITION_EPSILON = 0.5;
++var SETTLE_QUIET_PASSES_TO_RELEASE = 2;
++var SETTLE_TTL_MS = 500;
++function getSettleTargetOffset(ctx, settle, index, position) {
++ const params = { index, viewOffset: settle.viewOffset, viewPosition: settle.viewPosition };
++ return clampScrollOffset(ctx, calculateOffsetWithOffsetPosition(ctx, position, params), params);
++}
++function clearScrollTargetSettle(state) {
++ state.scrollTargetSettle = void 0;
++}
++function beginScrollTargetSettle(ctx, params) {
+ const state = ctx.state;
+- return index !== void 0 ? state.positions[index] || 0 : 0;
++ const { index, viewOffset, viewPosition } = params;
++ const { data } = state.props;
++ const isEndAlignedLastItem = index === data.length - 1 && viewPosition === 1;
++ if (index < 0 || index >= data.length || isEndAlignedLastItem) {
++ clearScrollTargetSettle(state);
++ return;
++ }
++ state.scrollTargetSettle = {
++ expiresAt: Date.now() + SETTLE_TTL_MS,
++ id: getId(state, index),
++ quietPasses: 0,
++ viewOffset,
++ viewPosition
++ };
++}
++function settleScrollTarget(ctx) {
++ var _a3;
++ const state = ctx.state;
++ const settle = state.scrollTargetSettle;
++ if (!settle) {
++ 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;
++ }
++ const now = Date.now();
++ if (now > settle.expiresAt) {
++ clearScrollTargetSettle(state);
++ return false;
++ }
++ const targetOffset = getSettleTargetOffset(ctx, settle, index, position);
++ const diff = targetOffset - state.scroll;
++ if (Math.abs(diff) <= SETTLE_POSITION_EPSILON) {
++ settle.quietPasses++;
++ if (settle.quietPasses >= SETTLE_QUIET_PASSES_TO_RELEASE) {
++ clearScrollTargetSettle(state);
++ }
++ return false;
++ }
++ settle.quietPasses = 0;
++ settle.expiresAt = now + SETTLE_TTL_MS;
++ if (((_a3 = state.scrollingTo) == null ? void 0 : _a3.index) === index) {
++ state.scrollingTo.offset = position;
++ state.scrollingTo.targetOffset = targetOffset;
++ }
++ requestAdjust(ctx, diff);
++ return true;
+ }
+
+-// src/core/getStartOffsetAdjustment.ts
+-function getStartOffsetAdjustment(ctx) {
+- const { state } = ctx;
+- const stylePaddingStart = state.props.horizontal ? (isHorizontalRTL(state) ? state.props.stylePaddingRight : state.props.stylePaddingLeft) || 0 : peek$(ctx, "stylePaddingTop") || 0;
+- return stylePaddingStart + (peek$(ctx, "alignItemsAtEndPadding") || 0) + (peek$(ctx, "headerSize") || 0);
++// 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/utils/getId.ts
+-function getId(state, index) {
+- const { data, keyExtractor } = state.props;
+- if (!data) {
+- return "";
++// 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
++ )
++ );
+ }
+- const ret = index < data.length ? keyExtractor ? keyExtractor(data[index], index) : index : null;
+- const id = ret;
+- state.idCache[index] = id;
+- return id;
+ }
+
+-// src/core/updateContentMetricsState.ts
+-function getRawContentLength(ctx) {
+- var _a3, _b, _c;
+- const { state, values } = ctx;
+- return (values.get("headerSize") || 0) + (values.get("footerSize") || 0) + ((_c = (_b = (_a3 = state.pendingTotalSize) != null ? _a3 : state.totalSize) != null ? _b : values.get("totalSize")) != null ? _c : 0) + (state.props.stylePaddingTop || 0) + (state.props.stylePaddingBottom || 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 getAlignItemsAtEndPadding(ctx) {
+- const { state } = ctx;
+- const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
+- return shouldPad ? Math.max(0, state.scrollLength - getRawContentLength(ctx) - getContentInsetEnd(ctx)) : 0;
++function clearInitialScrollSession(state) {
++ state.initialScrollSession = void 0;
++ return void 0;
+ }
+-function updateContentMetricsState(ctx) {
+- var _a3;
+- const { state } = ctx;
+- const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
+- const nextPadding = getAlignItemsAtEndPadding(ctx);
+- if (previousPadding !== nextPadding) {
+- const isAnimatedMaintainActive = state.maintainingScrollAtEnd === "pending-animated" || state.maintainingScrollAtEnd === "animated";
+- const isMaintainExpected = state.didContainersLayout && ((_a3 = state.props.maintainScrollAtEnd) == null ? void 0 : _a3.animated) && (isAnimatedMaintainActive || previousPadding > nextPadding && peek$(ctx, "isWithinMaintainScrollAtEndThreshold"));
+- if (isMaintainExpected && !isAnimatedMaintainActive && !state.pendingMaintainScrollAtEnd) {
+- state.scheduledWork.microtask(() => {
+- if (!state.maintainingScrollAtEnd && !state.pendingMaintainScrollAtEnd) {
+- set$(ctx, "alignItemsAtEndPadding", getAlignItemsAtEndPadding(ctx));
+- }
+- }, "alignItemsAtEndPaddingFallback");
+- } else if (!isMaintainExpected) {
+- set$(ctx, "alignItemsAtEndPadding", nextPadding);
+- }
++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;
+ }
+-
+-// src/core/addTotalSize.ts
+-function addTotalSize(ctx, key, add, notifyTotalSize = true) {
+- const state = ctx.state;
+- const prevTotalSize = state.totalSize;
+- let totalSize = state.totalSize;
+- if (key === null) {
+- totalSize = add;
+- if (state.timeoutSetPaddingTop) {
+- clearTimeout(state.timeoutSetPaddingTop);
+- state.timeoutSetPaddingTop = void 0;
++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;
+ }
+- } else {
+- totalSize += add;
++ const completion = ensureInitialScrollSessionCompletion(state, state.initialScrollSession.kind);
++ completion.didDispatchNativeScroll = void 0;
++ completion.didRetrySilentInitialScroll = void 0;
+ }
+- if (prevTotalSize !== totalSize) {
+- {
+- state.pendingTotalSize = void 0;
+- state.totalSize = totalSize;
+- if (notifyTotalSize) {
+- set$(ctx, "totalSize", totalSize);
+- }
+- updateContentMetricsState(ctx);
++};
++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;
+ }
+- } else if (notifyTotalSize && ctx.values.get("totalSize") !== totalSize) {
+- set$(ctx, "totalSize", totalSize);
++ 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);
+ }
+-}
+-
+-// src/core/setSize.ts
+-function setSize(ctx, itemKey, size, notifyTotalSize = true) {
+- const state = ctx.state;
+- const { sizes } = state;
+- const previousSize = sizes.get(itemKey);
+- const diff = previousSize !== void 0 ? size - previousSize : size;
+- if (diff !== 0) {
+- addTotalSize(ctx, itemKey, diff, notifyTotalSize);
++ if (!state.initialScroll && !bootstrap && !hasInitialScrollSessionCompletion(completion)) {
++ return clearInitialScrollSession(state);
+ }
+- sizes.set(itemKey, size);
+-}
+-
+-// src/utils/helpers.ts
+-function isFunction(obj) {
+- return typeof obj === "function";
++ 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;
+ }
+-function isArray(obj) {
+- return Array.isArray(obj);
++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");
+ }
+-var warned = /* @__PURE__ */ new Set();
+-function warnDevOnce(id, text) {
+- if (IS_DEV && !warned.has(id)) {
+- warned.add(id);
+- console.warn(`[legend-list] ${text}`);
++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 roundSize(size) {
+- return Math.floor(size * 8) / 8;
+-}
+-function isNullOrUndefined(value) {
+- return value === null || value === void 0;
+-}
+-function getPadding(s, type) {
+- var _a3, _b, _c;
+- const axisPadding = type === "Left" || type === "Right" ? s.paddingHorizontal : s.paddingVertical;
+- return (_c = (_b = (_a3 = s[`padding${type}`]) != null ? _a3 : axisPadding) != null ? _b : s.padding) != null ? _c : 0;
+-}
+-function extractPadding(style, contentContainerStyle, type) {
+- return getPadding(style, type) + getPadding(contentContainerStyle, type);
+-}
+-function findContainerId(ctx, key) {
++function setAdaptiveRender(ctx, mode, reason) {
+ var _a3, _b;
+- const directMatch = (_b = (_a3 = ctx.state) == null ? void 0 : _a3.containerItemKeys) == null ? void 0 : _b.get(key);
+- if (directMatch !== void 0) {
+- return directMatch;
+- }
+- const numContainers = peek$(ctx, "numContainers");
+- for (let i = 0; i < numContainers; i++) {
+- const itemKey = peek$(ctx, `containerItemKey${i}`);
+- if (itemKey === key) {
+- return i;
+- }
++ 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);
+ }
+- return -1;
+ }
+-
+-// src/utils/getItemSize.ts
+-function getKnownOrFixedSize(ctx, key, index, data, resolved) {
++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 { getFixedItemSize, getItemType } = state.props;
+- let size = key ? state.sizesKnown.get(key) : void 0;
+- if (size === void 0 && key && getFixedItemSize) {
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- const fixedSize = (resolved == null ? void 0 : resolved.didResolveFixedItemSize) ? resolved.fixedItemSize : getFixedItemSize(data, index, itemType);
+- if (fixedSize !== void 0) {
+- size = fixedSize + ctx.scrollAxisGap;
+- state.sizesKnown.set(key, size);
++ 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);
+ }
+ }
+- return size;
+ }
+-function getKnownOrFixedItemSize(ctx, index) {
+- const key = getId(ctx.state, index);
+- return getKnownOrFixedSize(ctx, key, index, ctx.state.props.data[index]);
++
++// 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 areKnownOrFixedItemSizesAvailable(ctx, startIndex, endIndex) {
+- for (let index = startIndex; index <= endIndex; index++) {
+- if (getKnownOrFixedItemSize(ctx, index) === void 0) {
+- return false;
+- }
++function scheduleFullDrawDistancePrewarm(ctx) {
++ const { state } = ctx;
++ if (state.props.drawDistance <= INITIAL_DRAW_DISTANCE || state.scheduledWork.has("fullDrawDistancePrewarm")) {
++ return;
+ }
+- return true;
++ state.scheduledWork.frame(() => {
++ var _a3;
++ return (_a3 = state.triggerCalculateItemsInView) == null ? void 0 : _a3.call(state);
++ }, "fullDrawDistancePrewarm");
+ }
+-function getItemSize(ctx, key, index, data, useAverageSize, preferCachedSize, notifyTotalSize, resolved) {
+- var _a3, _b, _c, _d;
+- const state = ctx.state;
+- const {
+- sizes,
+- averageSizes,
+- props: { estimatedItemSize, getItemType },
+- scrollingTo
+- } = state;
+- const sizeKnown = state.sizesKnown.get(key);
+- if (sizeKnown !== void 0) {
+- return sizeKnown;
+- }
+- let size;
+- const renderedSize = sizes.get(key);
+- if (preferCachedSize) {
+- if (renderedSize !== void 0) {
+- return renderedSize;
+- }
+- }
+- size = getKnownOrFixedSize(ctx, key, index, data, resolved);
+- if (size !== void 0) {
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+- }
+- const itemType = (_b = resolved == null ? void 0 : resolved.itemType) != null ? _b : getItemType ? (_a3 = getItemType(data, index)) != null ? _a3 : "" : "";
+- if (useAverageSize && !scrollingTo) {
+- const averageSizeForType = (_c = averageSizes[itemType]) == null ? void 0 : _c.avg;
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0 && renderedSize !== void 0) {
+- return renderedSize;
+- }
+- if (size === void 0 && useAverageSize && scrollingTo) {
+- const averageSizeForType = (_d = scrollingTo.averageSizeSnapshot) == null ? void 0 : _d[itemType];
+- if (averageSizeForType !== void 0) {
+- size = roundSize(averageSizeForType);
+- }
+- }
+- if (size === void 0) {
+- size = estimatedItemSize + ctx.scrollAxisGap;
++
++// src/utils/setInitialRenderState.ts
++function resetInitialRenderState(ctx, {
++ resetLayout,
++ resetInitialScroll
++}) {
++ const { state } = ctx;
++ if (resetLayout) {
++ state.didContainersLayout = false;
++ state.queuedInitialLayout = false;
+ }
+- setSize(ctx, key, size, notifyTotalSize);
+- return size;
+-}
+-function getItemSizeAtIndex(ctx, index) {
+- if (index === void 0 || index < 0) {
+- return void 0;
++ if (resetInitialScroll) {
++ state.didFinishInitialScroll = false;
+ }
+- const targetId = getId(ctx.state, index);
+- return getItemSize(ctx, targetId, index, ctx.state.props.data[index]);
++ set$(ctx, "readyToRender", false);
++ resetAdaptiveRender(ctx);
+ }
+-
+-// 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;
++function setInitialRenderState(ctx, {
++ didLayout,
++ didInitialScroll
++}) {
++ const { state } = ctx;
++ const {
++ loadStartTime,
++ props: { onLoad }
++ } = state;
++ if (didLayout) {
++ state.didContainersLayout = true;
+ }
+- if (index !== void 0) {
+- const startOffsetAdjustment = getStartOffsetAdjustment(ctx);
+- if (startOffsetAdjustment) {
+- offset += startOffsetAdjustment;
+- }
++ if (didInitialScroll) {
++ state.didFinishInitialScroll = true;
+ }
+- 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;
++ 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 });
++ }
+ }
+ }
+- 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/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;
+ }
+-
+-// src/core/finishScrollTo.ts
+-function finishScrollTo(ctx) {
+- var _a3, _b;
++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;
+- if (state == null ? void 0 : state.scrollingTo) {
+- cancelScrollCompletionChecks(state);
+- const resolvePendingScroll = state.pendingScrollResolve;
+- state.pendingScrollResolve = void 0;
+- const scrollingTo = state.scrollingTo;
+- state.scrollHistory.length = 0;
+- state.scrollingTo = void 0;
+- state.scrollTargetPinnedRange = void 0;
+- if (state.pendingTotalSize !== void 0) {
+- addTotalSize(ctx, null, state.pendingTotalSize);
+- }
+- {
+- state.scrollAdjustHandler.commitPendingAdjust(scrollingTo);
++ 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 (scrollingTo.isInitialScroll || state.initialScroll) {
+- const isOffsetSession = ((_a3 = state.initialScrollSession) == null ? void 0 : _a3.kind) === "offset";
+- const shouldPreserveResizeTarget = !!scrollingTo.isInitialScroll && !state.clearPreservedInitialScrollOnNextFinish && state.props.data.length > 0 && ((_b = state.initialScroll) == null ? void 0 : _b.viewPosition) === 1;
+- finishInitialScroll(ctx, {
+- onFinished: () => {
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
+- },
+- preserveTarget: isOffsetSession && state.props.data.length === 0 || shouldPreserveResizeTarget,
+- recalculateItems: true,
+- schedulePreservedTargetClear: shouldPreserveResizeTarget,
+- syncObservedOffset: isOffsetSession,
+- waitForCompletionFrame: !!scrollingTo.waitForInitialScrollCompletionFrame
+- });
+- return;
++ if ((_c = state.scrollingTo) == null ? void 0 : _c.isInitialScroll) {
++ cancelScrollCompletionChecks(state);
++ state.scrollingTo = void 0;
++ state.scrollTargetPinnedRange = void 0;
+ }
+- recalculateSettledScroll(ctx);
+- resolvePendingScroll == null ? void 0 : resolvePendingScroll();
++ initialScrollCompletion.resetFlags(state);
++ setInitialScrollSession(state, { bootstrap: null });
++ finishInitialScroll(ctx);
+ }
+ }
+-
+-// src/core/doScrollTo.ts
+-var SCROLL_END_IDLE_MS = 80;
+-var SCROLL_END_MAX_MS = 1500;
+-var SMOOTH_SCROLL_DURATION_MS = 320;
+-var SCROLL_END_TARGET_EPSILON = 1;
+-function doScrollTo(ctx, params) {
+- var _a3, _b;
++function finishInitialScroll(ctx, options) {
++ var _a3, _b, _c;
+ const state = ctx.state;
+- const { animated, horizontal, offset } = params;
+- state.scheduledWork.cancel("platformScrollCompletion");
+- const scroller = state.refScroller.current;
+- const node = scroller == null ? void 0 : scroller.getScrollableNode();
+- if (!scroller || !node) {
+- return;
+- }
+- const isAnimated = !!animated;
+- const isHorizontal = !!horizontal;
+- const contentSize = isHorizontal ? getContentSize(ctx) : void 0;
+- const left = isHorizontal ? toNativeHorizontalOffset(state, offset, contentSize) : 0;
+- const top = isHorizontal ? 0 : offset;
+- scroller.scrollTo({ animated: isAnimated, x: left, y: top });
+- if (isAnimated) {
+- const target = (_b = (_a3 = scroller.getScrollEventTarget) == null ? void 0 : _a3.call(scroller)) != null ? _b : null;
+- listenForScrollEnd(ctx, {
+- readOffset: () => scroller.getCurrentScrollOffset(),
+- target,
+- targetOffset: offset
+- });
+- } else {
+- state.scroll = offset;
+- const targetToken = state.scrollingTo;
+- state.scheduledWork.timeout(
+- () => {
+- if (targetToken === state.scrollingTo) {
+- finishScrollTo(ctx);
+- }
+- },
+- 100,
+- "platformScrollCompletion"
+- );
+- }
+-}
+-function listenForScrollEnd(ctx, params) {
+- const { readOffset, target, targetOffset } = params;
+- if (!target) {
+- finishScrollTo(ctx);
+- return;
+- }
+- const supportsScrollEnd = "onscrollend" in target;
+- let idleTimeout;
+- let settled = false;
+- const { scheduledWork, scrollingTo: targetToken } = ctx.state;
+- const maxTimeout = setTimeout(() => finish("max"), SCROLL_END_MAX_MS);
+- const cleanup = () => {
+- target.removeEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.removeEventListener("scrollend", onScrollEnd);
+- }
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
+- }
+- clearTimeout(maxTimeout);
+- };
+- const cancel = () => {
+- if (!settled) {
+- settled = true;
+- cleanup();
++ 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 finish = (reason) => {
+- if (settled) return;
+- if (targetToken !== ctx.state.scrollingTo) {
+- scheduledWork.cancel("platformScrollCompletion");
+- return;
++ }
++ 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);
+ }
+- const currentOffset = readOffset();
+- const isNearTarget = Math.abs(currentOffset - targetOffset) <= SCROLL_END_TARGET_EPSILON;
+- if (reason === "scrollend" && !isNearTarget) {
+- return;
++ if (options == null ? void 0 : options.recalculateItems) {
++ recalculateSettledScroll(ctx);
+ }
+- scheduledWork.cancel("platformScrollCompletion");
+- finishScrollTo(ctx);
+- };
+- const onScroll2 = () => {
+- if (idleTimeout !== void 0) {
+- clearTimeout(idleTimeout);
++ setInitialRenderState(ctx, { didInitialScroll: true });
++ if (shouldReleaseDeferredPublicOnScroll) {
++ releaseDeferredPublicOnScroll(ctx, finalScrollOffset);
+ }
+- idleTimeout = setTimeout(() => finish("idle"), SCROLL_END_IDLE_MS);
++ (_e = options == null ? void 0 : options.onFinished) == null ? void 0 : _e.call(options);
+ };
+- const onScrollEnd = () => finish("scrollend");
+- target.addEventListener("scroll", onScroll2);
+- if (supportsScrollEnd) {
+- target.addEventListener("scrollend", onScrollEnd);
+- } else {
+- idleTimeout = setTimeout(() => finish("idle"), SMOOTH_SCROLL_DURATION_MS);
++ if (options == null ? void 0 : options.waitForCompletionFrame) {
++ requestAnimationFrame(complete);
++ return;
+ }
+- scheduledWork.register("platformScrollCompletion", cancel);
++ complete();
++}
++
++// src/core/calculateOffsetForIndex.ts
++function calculateOffsetForIndex(ctx, index) {
++ const state = ctx.state;
++ return index !== void 0 ? state.positions[index] || 0 : 0;
+ }
+
+ // src/core/scrollRequestTracker.ts
+@@ -1462,30 +1552,6 @@ function getScrollRequestTracker(ctx) {
+ return ctx.scrollRequestTracker;
+ }
+
+-// src/utils/requestAdjust.ts
+-function requestAdjust(ctx, positionDiff, source) {
+- const state = ctx.state;
+- if (Math.abs(positionDiff) > 0.1) {
+- const doit = () => {
+- {
+- state.scrollAdjustHandler.requestAdjust(positionDiff);
+- if (state.adjustingFromInitialMount) {
+- state.adjustingFromInitialMount--;
+- }
+- }
+- };
+- state.scroll += positionDiff;
+- state.scrollForNextCalculateItemsInView = void 0;
+- const readyToRender = peek$(ctx, "readyToRender");
+- if (readyToRender) {
+- doit();
+- } else {
+- state.adjustingFromInitialMount = (state.adjustingFromInitialMount || 0) + 1;
+- requestAnimationFrame(doit);
+- }
+- }
+-}
+-
+ // src/core/doMaintainScrollAtEnd.ts
+ function finishMaintainScrollAtEnd(ctx) {
+ var _a3;
+@@ -2120,7 +2186,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 {
+@@ -2152,6 +2218,15 @@ function scrollTo(ctx, params) {
+ };
+ if (!isInitialScroll) {
+ pinScrollTargetRenderRange(ctx, targetOffset, scrollTarget.index);
++ if (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;
+@@ -2159,7 +2234,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 });
+@@ -4423,7 +4498,8 @@ 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);
++ const didSettleScrollTarget = suppressInitialScrollSideEffects ? false : settleScrollTarget(ctx);
++ const didMVCPAdjustScroll = didSettleScrollTarget || !!checkMVCP && (state.scroll !== scrollBeforeMVCP || ((_g = peek$(ctx, "scrollAdjustPending")) != null ? _g : 0) !== scrollAdjustPendingBeforeMVCP);
+ if (didMVCPAdjustScroll) {
+ updateScroll2(state.scroll);
+ updateScrollRange();
+@@ -6586,6 +6662,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;
+@@ -6595,6 +6672,9 @@ 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) {
++ handleBootstrapInitialScrollLayoutChange(ctx);
++ doMaintainScrollAtEnd(ctx);
+ }
+ }
+ state.didMeasureHeader = true;
+@@ -7658,13 +7738,14 @@ function getRenderedItem(ctx, key, containerId) {
+
+ // 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) {
+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-link-jump.test.ts b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts
new file mode 100644
index 000000000000..3060a9483e1c
--- /dev/null
+++ b/shared/tests/e2e/electron/flows/chat-link-jump.test.ts
@@ -0,0 +1,323 @@
+import type {Locator, 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 directly onto one of its messages — the cold path.
+//
+// chat-search-hit covers the warm one: it opens a thread and then searches inside it, so the list
+// has always rendered content by the time anything asks it to centre. The branch this covers is the
+// other one — a thread mounting with a centred target already pending, which is what a permalink
+// does. It is reached the way a reader reaches it, through the app's own affordances: copy a link to
+// a message in conversation B, paste and send it in conversation A, click it. That routes through
+// handleKeybaseLink -> previewConversation -> navigateToThread(..., highlightMessageID), which on
+// desktop resets the chat root params and mounts a fresh thread. No OS-level deeplink needed.
+//
+// The link must point at a genuinely different conversation: navigateToThread takes a
+// sameVisibleThread branch when the target is already on screen, and that branch does not remount,
+// which is the warm path again.
+//
+// Sending a link message is real data in the smoke user's real chat history, so every run that
+// sends one leaves it behind permanently. Only one such message is ever needed - the app doesn't
+// care who authored the link it's clicking, or when. So this test looks for one an earlier run
+// already left in conversation A before resorting to sending a new one, and only falls back to
+// sending when no leftover link is usable.
+
+// A row has to be visible by more than a hairline to count as landed on, matching chat-search-hit
+// and the iOS flow.
+// No retries. The second attempt runs against a thread this test has already fetched once, so a
+// pass on retry would not be a pass on the path the first attempt took - the same reason
+// chat-thread-bottom turns them off.
+test.describe.configure({retries: 0})
+
+const MIN_VISIBLE_HEIGHT = 24
+// Centring is checked loosely on purpose. Sub-pixel precision is not the point — landing on the
+// right message rather than at one end of the thread is. A third of the viewport either side of the
+// middle still fails every way this can go wrong (top of the loaded window, bottom of the thread,
+// off screen entirely).
+const CENTRE_BAND_FRACTION = 1 / 3
+// The target has to sit well above the newest message, or a thread that simply stayed at its end
+// would pass without anything having jumped. This also gates link reuse: an old link is only worth
+// clicking if its target is still this far from B's current newest message, or a wrong landing
+// (staying put) would go undetected.
+const MIN_DISTANCE_FROM_END = 1_200
+// A conversation with less scrollable history than this cannot show the difference.
+const MIN_SCROLLABLE_OVERFLOW = 1_500
+
+// The scroller is the element LegendList renders inside the wrapper carrying the testID. Reached
+// through the wrapper's first child, the same way chat-thread-bottom does it.
+const distanceFromEnd = 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 undefined
+ return Math.round(scroller.scrollHeight - scroller.clientHeight - scroller.scrollTop)
+ }, T.CHAT_MESSAGE_LIST)
+
+const scrollableOverflow = async (page: Page): Promise =>
+ page.evaluate((testID: string) => {
+ type Scroller = {clientHeight: number; scrollHeight: 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 undefined
+ return Math.round(scroller.scrollHeight - scroller.clientHeight)
+ }, T.CHAT_MESSAGE_LIST)
+
+// Inbox rows carry the conversation name on their first line. Matched exactly rather than with
+// hasText: a row's second line is the latest message, and this flow sends a message that contains
+// another conversation's name, so a substring match picks the wrong row from the second run on.
+const rowName = async (row: Locator): Promise =>
+ (await row.innerText()).split('\n')[0]?.trim() ?? ''
+
+// A row's displayed name isn't always the conversation's full label. A team's default "general"
+// channel shows as a small-team row with just the team name; any other channel shows as its own
+// row with just "# channelname" (the team name isn't repeated there). getConversationLabel, which
+// is what a copied link encodes, always writes "team#channel" though - so a link's conv name needs
+// both forms accepted, not just an exact match against what a row shows.
+const rowMatchesConvName = async (row: Locator, convName: string): Promise => {
+ const text = await rowName(row)
+ if (text === convName) return true
+ const hashIndex = convName.indexOf('#')
+ if (hashIndex === -1) return false
+ const team = convName.slice(0, hashIndex)
+ const channel = convName.slice(hashIndex + 1)
+ if (channel === 'general' && text === team) return true
+ return text.replace(/^#\s*/, '') === channel
+}
+
+const openConversationNamed = async (page: Page, rows: Locator, name: string): Promise => {
+ const count = await rows.count()
+ for (let i = 0; i < count; i++) {
+ const row = rows.nth(i)
+ if (!(await rowMatchesConvName(row, name))) continue
+ await row.click({force: true, timeout: 10_000})
+ await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000})
+ await page.waitForTimeout(2_500)
+ return
+ }
+ throw new Error(`no inbox row named "${name}"`)
+}
+
+// What "Copy a link to this message" puts on the clipboard: constants/deeplinks.tsx's
+// linkFromConvAndMessage(conv, messageID) - conv is the conversation's display label (what
+// rowName also reads), messageID is what the app renders as data-ordinal once a message has sent.
+const LINK_PATTERN = /^keybase:\/\/chat\/(.+)\/(\d+)$/
+
+// Looks for a message already sitting in conversation A whose text is a link from an earlier run
+// of this test. Recent messages are checked first without scrolling - every prior run that fell
+// back to sending left one near the bottom, so the common case needs no scroll at all - and a
+// bounded scroll back covers the case where other messages have since pushed all of them out of
+// view.
+const findExistingLink = async (page: Page): Promise<{convName: string; ordinal: string; text: string} | undefined> => {
+ for (let attempt = 0; attempt < 6; attempt++) {
+ const candidates = page.locator('[data-ordinal]')
+ const n = await candidates.count()
+ for (let i = n - 1; i >= 0; i--) {
+ const text = (await candidates.nth(i).innerText()).trim()
+ const match = LINK_PATTERN.exec(text)
+ if (match) return {convName: match[1]!, ordinal: match[2]!, text}
+ }
+ await page.mouse.wheel(0, -600)
+ await page.waitForTimeout(250)
+ }
+ return undefined
+}
+
+// Scrolls conversation B upward looking for the reused link's target row. If it turns up wholly on
+// screen at a point at least MIN_DISTANCE_FROM_END from B's current newest message, the reused link
+// is still good. If the scroll runs out first - the target has scrolled closer to the end since the
+// link was made, or is no longer reachable at all - the caller falls back to sending a fresh link
+// instead: asserting a landing that could not distinguish "jumped" from "already there" would prove
+// nothing.
+const findReusedTargetRow = async (
+ page: Page,
+ listBox: {x: number; y: number; width: number; height: number},
+ ordinal: string
+): Promise => {
+ for (let attempt = 0; attempt < 30; attempt++) {
+ await page.mouse.wheel(0, -600)
+ await page.waitForTimeout(250)
+ if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue
+ await page.waitForTimeout(500)
+ const candidate = page.locator(`[data-ordinal="${ordinal}"]`).first()
+ if ((await candidate.count()) === 0) continue
+ const box = await candidate.boundingBox()
+ if (!box) continue
+ const wholly = box.y >= listBox.y + 20 && box.y + box.height <= listBox.y + listBox.height - 20
+ if (wholly) return candidate
+ }
+ return undefined
+}
+
+test('opens a conversation onto the message a link points at', async ({page}) => {
+ test.setTimeout(180_000)
+ const smokeUser = process.env['KB_SMOKE_USER']
+ expect(smokeUser, 'KB_SMOKE_USER is not set').toBeTruthy()
+
+ await navigateToChat(page)
+ const rows = page.locator(
+ `[data-testid="${T.CHAT_INBOX_ROW}"], [data-testid="${T.CHAT_INBOX_CHANNEL_ROW}"]`
+ )
+
+ let convB = ''
+ let targetOrdinal = ''
+ let link = ''
+
+ // ---- Try to reuse a link an earlier run already left in conversation A. ----
+ await openConversationNamed(page, rows, smokeUser!)
+ const existing = await findExistingLink(page)
+ if (existing) {
+ try {
+ await openConversationNamed(page, rows, existing.convName)
+ const overflow = await scrollableOverflow(page)
+ const listBox =
+ overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW
+ ? await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox()
+ : null
+ if (listBox) {
+ await page.mouse.move(listBox.x + listBox.width / 2, listBox.y + listBox.height / 2)
+ const row = await findReusedTargetRow(page, listBox, existing.ordinal)
+ if (row) {
+ convB = existing.convName
+ targetOrdinal = existing.ordinal
+ link = existing.text
+ }
+ }
+ } catch {
+ // existing.convName no longer names an inbox row (conversation renamed or gone) - fall
+ // through to sending a fresh link below.
+ }
+ }
+ console.log(
+ link
+ ? `[chat-link-jump] reusing an existing link: ${link}`
+ : '[chat-link-jump] no reusable link found; sending a new one'
+ )
+
+ // ---- Fall back to the original flow: discover a conversation and send a fresh link. ----
+ if (!link) {
+ // B is discovered rather than hard-coded — a real username in a committed test would pin the
+ // suite to one machine — but it is pinned by the name it turns out to have for the rest of the
+ // run, so the second visit is provably the same conversation as the first.
+ const rowCount = Math.min(await rows.count(), 12)
+ for (let i = 0; i < rowCount && !convB; i++) {
+ const name = await rowName(rows.nth(i))
+ // A is the smoke account's own conversation: the one message this flow sends goes there.
+ if (!name || name === smokeUser) continue
+ await openConversationNamed(page, rows, name)
+ const overflow = await scrollableOverflow(page)
+ if (overflow !== undefined && overflow >= MIN_SCROLLABLE_OVERFLOW) convB = name
+ }
+ expect(
+ convB,
+ 'no conversation had enough history to jump within, so nothing was checked'
+ ).toBeTruthy()
+
+ // Scroll back through B until the newest message is far enough away, then take a row that is
+ // wholly on screen and small enough to measure. `data-ordinal` is the app's own per-message
+ // attribute; there is no testID for "a message row", and the ordinal is what the copied link
+ // encodes, so it is also how the landing is checked at the other end.
+ const listBox = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox()
+ expect(listBox, 'the message list has no box').not.toBeNull()
+ await page.mouse.move(listBox!.x + listBox!.width / 2, listBox!.y + listBox!.height / 2)
+
+ let targetRow: Locator | undefined
+ for (let attempt = 0; attempt < 30 && !targetRow; attempt++) {
+ await page.mouse.wheel(0, -600)
+ await page.waitForTimeout(250)
+ if (((await distanceFromEnd(page)) ?? 0) < MIN_DISTANCE_FROM_END) continue
+ await page.waitForTimeout(500)
+ const candidates = page.locator('[data-ordinal]')
+ const n = await candidates.count()
+ for (let i = 0; i < n; i++) {
+ const candidate = candidates.nth(i)
+ const box = await candidate.boundingBox()
+ if (!box) continue
+ const text = (await candidate.innerText()).trim()
+ const wholly = box.y >= listBox!.y + 20 && box.y + box.height <= listBox!.y + listBox!.height - 20
+ // Media rows are taller than the viewport's usable band and often have no text to identify
+ // them by, so a short text row is the honest handle here.
+ if (box.height > 250 || !wholly || text.length < 6) continue
+ targetOrdinal = (await candidate.getAttribute('data-ordinal')) ?? ''
+ targetRow = candidate
+ break
+ }
+ }
+ expect(
+ targetRow,
+ `no message in "${convB}" was far enough above its newest one to link to`
+ ).toBeTruthy()
+
+ await targetRow!.hover()
+ await targetRow!.getByTestId(T.CHAT_MESSAGE_MENU_BUTTON).first().click({force: true, timeout: 10_000})
+ await page.getByText('Copy a link to this message').first().click({timeout: 10_000})
+ await page.waitForTimeout(700)
+
+ // Pasting rather than typing the link out: the copy above put it on the real clipboard and
+ // there is no way to read that back from the renderer (clipboard-read permission is denied in
+ // the app), so the paste both delivers it and reveals what was copied. A plain Meta+V is a
+ // synthetic key event the renderer ignores; the editing command has to be attached to it.
+ await openConversationNamed(page, rows, smokeUser!)
+ const input = page.getByTestId(T.CHAT_INPUT)
+ await input.click()
+ await input.fill('')
+ const cdp = await page.context().newCDPSession(page)
+ const key = {code: 'KeyV', key: 'v', modifiers: 4, nativeVirtualKeyCode: 86, windowsVirtualKeyCode: 86}
+ await cdp.send('Input.dispatchKeyEvent', {...key, commands: ['paste'], type: 'keyDown'})
+ await cdp.send('Input.dispatchKeyEvent', {...key, type: 'keyUp'})
+ await page.waitForTimeout(600)
+ link = await input.inputValue()
+ expect(link, 'the message menu did not put a keybase chat link on the clipboard').toMatch(
+ new RegExp(`^keybase://chat/.+/${targetOrdinal}$`)
+ )
+ await input.press('Enter')
+ }
+
+ await openConversationNamed(page, rows, smokeUser!)
+ const sentLink = page.locator(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`).getByText(link, {exact: true})
+ await expect(sentLink.last()).toBeVisible({timeout: 15_000})
+ await sentLink.last().click()
+
+ // The thread this lands in is a fresh mount with a centred target already pending. Poll rather
+ // than sleep once: the centred fetch clears and refetches the thread, so the row arrives well
+ // after the navigation does.
+ const hit = page.getByTestId(T.CHAT_SEARCH_HIT).first()
+ await expect(
+ hit,
+ `clicking the link to ${link} never landed on the message: the thread settled ${await distanceFromEnd(page)}px from its end without the target on screen`
+ ).toBeVisible({timeout: 20_000})
+ // Let any late row measurement settle before reading positions, the same way the search flow does.
+ await page.waitForTimeout(2_000)
+
+ // We really did leave A: the link message is A's newest and would still be rendered if we had not.
+ await expect(sentLink).toHaveCount(0)
+
+ const hitBox = await hit.boundingBox({timeout: 5_000})
+ const listAfter = await page.getByTestId(T.CHAT_MESSAGE_LIST).first().boundingBox()
+ expect(hitBox, 'the linked message was highlighted but had no box, so nothing was measured').not.toBeNull()
+ expect(listAfter, 'the message list has no box').not.toBeNull()
+
+ const visibleHeight =
+ Math.min(hitBox!.y + hitBox!.height, listAfter!.y + listAfter!.height) - Math.max(hitBox!.y, listAfter!.y)
+ expect(
+ visibleHeight >= Math.min(hitBox!.height, MIN_VISIBLE_HEIGHT),
+ `the linked message is outside the list: hit y=${Math.round(hitBox!.y)} h=${Math.round(hitBox!.height)}, list y=${Math.round(listAfter!.y)} h=${Math.round(listAfter!.height)}`
+ ).toBe(true)
+
+ const offCentre = Math.abs(
+ hitBox!.y + hitBox!.height / 2 - (listAfter!.y + listAfter!.height / 2)
+ )
+ expect(
+ offCentre,
+ `the linked message landed ${Math.round(offCentre)}px from the middle of the list (viewport ${Math.round(listAfter!.height)}px)`
+ ).toBeLessThanOrEqual(listAfter!.height * CENTRE_BAND_FRACTION)
+})
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..0e94c5f0d0f7
--- /dev/null
+++ b/shared/tests/e2e/electron/flows/chat-thread-bottom.test.ts
@@ -0,0 +1,108 @@
+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}`
+ // Open a neighbour first so the click below is a real open. The inbox row of the conversation
+ // already on screen has no click handler at all (chat/inbox/row/small-team: onSelectConversation
+ // is undefined when isSelected), so clicking it changes nothing - and a thread left parked in
+ // its history by an earlier flow would be measured as if this test had just opened it. That is
+ // what made this fail only when a search flow ran first and left its conversation open.
+ if (rowCount > 1) {
+ await rows
+ .nth((i + 1) % rowCount)
+ .click({force: true, timeout: 10_000})
+ .catch(() => {})
+ await page.waitForSelector(`[data-testid="${T.CHAT_MESSAGE_LIST}"]`, {timeout: 10_000})
+ await page.waitForTimeout(500)
+ }
+ 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 2dff41c035b0..39f3552e82de 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..eb1003bf10e6 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,27 @@ 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 per-message "..." actions button (desktop only, revealed on hover). Icon-only, so there is
+// no text to match on, and it is the only way into "Copy a link to this message".
+export const CHAT_MESSAGE_MENU_BUTTON = 'chat-message-menu-button'
+// 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'