authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-01 12:18:21-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-01 12:18:21-07:00
log261380f91f764673c7fcefcb6a3f775526dbb776
tree34ffd932a4b0891ef284c6048877ce2b5326b145
parent2d156a83b96ebf2e4a9c6017251baad357ae6ab1
signature Signed by PGP key B5690EEEBB952194

fix(mobile): keep thread scroll bounds current after animations (#9013)

Co-authored-by: Julius Marminge <julius@mac.lan>

7 files changed, 253 insertions(+), 164 deletions(-)

apps/mobile/src/features/threads/ThreadDetailScreen.tsx+1-1
......@@ -688,7 +688,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread
688688 onTouchCancel={handleFeedTouchCancel}
689689 >
690690 <ThreadFeed
691 key={props.selectedThread.id}
691 key={selectedThreadKey}
692692 environmentId={props.environmentId}
693693 threadId={props.selectedThread.id}
694694 workspaceRoot={props.threadCwd}
apps/mobile/src/features/threads/ThreadFeed.tsx+30-50
......@@ -75,7 +75,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context";
7575import Animated, {
7676 FadeIn,
7777 FadeInUp,
78 FadeOut,
7978 LinearTransition,
8079 type SharedValue,
8180} from "react-native-reanimated";
......@@ -137,6 +136,7 @@ import {
137136} from "./thread-feed-live-follow";
138137import {
139138 collapsedWorkLogHeight,
139 ThreadDisclosureChevron,
140140 ThreadWorkGroupToggle,
141141 ThreadWorkLog,
142142 THREAD_DISCLOSURE_TRANSITION_MS,
......@@ -171,11 +171,12 @@ function formatMessageTime(input: string): string {
171171// text-sm line at every supported base font size (26px at the 22pt maximum),
172172// so its height is a constant; a drifted value costs one correction on
173173// measure, not a persistent offset.
174const TURN_FOLD_HEIGHT = 56; // min-h-11 (44) + mb-3 (12)
174const TURN_FOLD_HEIGHT = 48; // min-h-11 (44) + mb-1 (4)
175175const THREAD_FEED_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_TRANSITION_MS);
176const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.duration(140);
177const THREAD_FEED_DISCLOSURE_EXIT_TRANSITION = FadeOut.duration(120);
178const EMPTY_DISCLOSURE_ENTRY_IDS: ReadonlySet<string> = new Set();
176// Let neighboring rows move out of the new rows' space before showing their text.
177const THREAD_FEED_DISCLOSURE_ENTER_TRANSITION = FadeIn.delay(
178 THREAD_DISCLOSURE_TRANSITION_MS,
179).duration(140);
179180
180181// Entering animations must only play for rows born just now — LegendList
181182// remounts rows when they scroll back into view, and replaying an entrance for
......@@ -1357,16 +1358,16 @@ function renderFeedEntry(
13571358 accessibilityState={{ expanded: entry.expanded }}
13581359 onPress={() => props.onToggleTurnFold(entry.turnId)}
13591360 hitSlop={4}
1360 className="mb-3 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2"
1361 className="mb-1 min-h-11 flex-row items-center gap-2 border-b border-adaptive-neutral-200-a80-white-a8 px-2"
13611362 >
13621363 <Text className="font-t3-medium text-sm tabular-nums text-foreground-muted">
13631364 {entry.label}
13641365 </Text>
1365 <SymbolView
1366 name={entry.expanded ? "chevron.down" : "chevron.right"}
1366 <ThreadDisclosureChevron
1367 expanded={entry.expanded}
1368 collapsedDirection="right"
13671369 size={15}
1368 tintColorClassName={"accent-icon-subtle"}
1369 type="monochrome"
1370 tintColor={iconSubtleColor}
13701371 />
13711372 </Pressable>
13721373 );
......@@ -1490,7 +1491,7 @@ function renderFeedEntry(
14901491 const enterAnimated = isFreshTimestamp(message.createdAt);
14911492 return (
14921493 <Animated.View
1493 className={cn(showAssistantMeta ? "mb-5 px-1" : "mb-2 px-1")}
1494 className={cn(showAssistantMeta ? "mb-5 px-1" : "mb-1 px-1")}
14941495 {...(enterAnimated ? { entering: FadeIn.duration(220) } : {})}
14951496 >
14961497 {renderedText.trim().length > 0 ? (
......@@ -1832,7 +1833,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
18321833 const disclosureSettleFrameRef = useRef<number | null>(null);
18331834 const disclosureSettleSecondFrameRef = useRef<number | null>(null);
18341835 const disclosureAnchorKeyRef = useRef<string | null>(null);
1835 const previousPresentedFeedRef = useRef<ReadonlyArray<ThreadFeedEntry> | null>(null);
18361836 const headerMaterialVisibleRef = useRef(false);
18371837 const previousLatestTurnRef = useRef(props.latestTurn);
18381838 const userScrollSettleTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
......@@ -1845,12 +1845,12 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
18451845 // Live-follow latch. LegendList's maintainScrollAtEnd alone re-pins the feed
18461846 // whenever the viewport drifts back inside its geometric threshold, which
18471847 // yanked users off history they were reading every time a stream chunk grew
1848 // a row. Follow breaks when the user scrolls up and away, and re-arms only
1849 // when the list actually returns to the end (or on send / thread switch).
1848 // a row. Scrolling away or expanding a disclosure above the end breaks
1849 // follow; reaching the end (or sending / switching threads) re-arms it.
18501850 const [endFollowEnabled, setEndFollowEnabled] = useState(true);
18511851 const endFollowEnabledRef = useRef(true);
18521852 // A "user scroll session" spans from drag start through the end of its
1853 // momentum; only motion inside a session can break follow, so MVCP
1853 // momentum; scroll events only break follow inside that session, so MVCP
18541854 // compensations and programmatic scrolls never strand a follower.
18551855 const userScrollSessionRef = useRef(false);
18561856 const setEndFollow = useCallback(
......@@ -2187,33 +2187,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
21872187 props.latestTurn,
21882188 ],
21892189 );
2190 const disclosureEnteringEntryIds = useMemo(() => {
2191 const anchorKey = disclosureAnchorKeyRef.current;
2192 const previousPresentedFeed = previousPresentedFeedRef.current;
2193 if (!disclosureToggleSettling || anchorKey === null || previousPresentedFeed === null) {
2194 return EMPTY_DISCLOSURE_ENTRY_IDS;
2195 }
2196
2197 const previousIds = new Set(previousPresentedFeed.map((entry) => entry.id));
2198 const anchorIndex = presentedFeed.findIndex((entry) => entry.id === anchorKey);
2199 const enteringIds = new Set<string>();
2200 if (anchorIndex < 0) {
2201 return enteringIds;
2202 }
2203 for (let index = anchorIndex + 1; index < presentedFeed.length; index += 1) {
2204 const entryId = presentedFeed[index]!.id;
2205 if (previousIds.has(entryId)) {
2206 break;
2207 }
2208 enteringIds.add(entryId);
2209 }
2210 return enteringIds;
2211 }, [disclosureToggleSettling, presentedFeed]);
2212
2213 useLayoutEffect(() => {
2214 previousPresentedFeedRef.current = presentedFeed;
2215 }, [presentedFeed]);
2216
22172190 // The empty↔filled key below remounts the list and resets its imperative
22182191 // content-inset override. Seed the fresh instance synchronously with the
22192192 // current overlay height before the scroll integration's next reaction;
......@@ -2300,13 +2273,23 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
23002273 }
23012274 disclosureSettleFrameRef.current = requestAnimationFrame(() => {
23022275 disclosureSettleSecondFrameRef.current = requestAnimationFrame(() => {
2276 // A disclosure can leave the reader above the end without a drag.
2277 // Reconcile follow before a later layout or resume can re-pin it.
2278 const listState = props.listRef.current?.getState();
2279 if (listState) {
2280 transitionEndFollow({
2281 type: "disclosure-settled",
2282 isAtEnd: listState.isAtEnd,
2283 userScrollSessionActive: userScrollSessionRef.current,
2284 });
2285 }
23032286 disclosureAnchorKeyRef.current = null;
23042287 setDisclosureToggleSettling(false);
23052288 disclosureSettleFrameRef.current = null;
23062289 disclosureSettleSecondFrameRef.current = null;
23072290 });
23082291 });
2309 }, []);
2292 }, [props.listRef, transitionEndFollow]);
23102293
23112294 const suspendEndScrollMaintenanceForDisclosure = useCallback((anchorKey: string | null) => {
23122295 disclosureAnchorKeyRef.current = anchorKey;
......@@ -2447,16 +2430,13 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
24472430 [expandedWorkRows],
24482431 );
24492432
2433 // Disclosures can mount existing offscreen rows as well as new work rows.
2434 // Fade those in after movement; never retain removed rows over replacements.
24502435 const renderItem = useCallback(
24512436 (info: { item: ThreadFeedEntry; index: number }) => (
24522437 <Animated.View
24532438 key={info.item.id}
2454 entering={
2455 disclosureEnteringEntryIds.has(info.item.id)
2456 ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION
2457 : undefined
2458 }
2459 exiting={THREAD_FEED_DISCLOSURE_EXIT_TRANSITION}
2439 entering={disclosureToggleSettling ? THREAD_FEED_DISCLOSURE_ENTER_TRANSITION : undefined}
24602440 >
24612441 {renderFeedEntry(info, {
24622442 environmentId: props.environmentId,
......@@ -2486,7 +2466,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) {
24862466 ),
24872467 [
24882468 copiedRowId,
2489 disclosureEnteringEntryIds,
2469 disclosureToggleSettling,
24902470 expandedWorkRows,
24912471 terminalAssistantMessageIds,
24922472 unsettledTurnId,
apps/mobile/src/features/threads/thread-feed-live-follow.test.ts+11
......@@ -102,6 +102,17 @@ describe("resolveThreadFeedLiveFollow", () => {
102102 ).toBe(false);
103103 });
104104
105 it.each([
106 { isAtEnd: false, userScrollSessionActive: false, expected: false },
107 { isAtEnd: true, userScrollSessionActive: false, expected: true },
108 { isAtEnd: false, userScrollSessionActive: true, expected: false },
109 { isAtEnd: true, userScrollSessionActive: true, expected: false },
110 ])("reconciles follow after a disclosure settles: %j", ({ expected, ...state }) => {
111 expect(resolveThreadFeedLiveFollow(!expected, { type: "disclosure-settled", ...state })).toBe(
112 expected,
113 );
114 });
115
105116 it("re-arms at the actual end only after the user scroll session ends", () => {
106117 expect(
107118 resolveThreadFeedLiveFollow(false, {
apps/mobile/src/features/threads/thread-feed-live-follow.ts+3-1
......@@ -7,7 +7,7 @@ export type ThreadFeedLiveFollowEvent =
77 readonly userScrollSessionActive: boolean;
88 }
99 | {
10 readonly type: "scroll";
10 readonly type: "scroll" | "disclosure-settled";
1111 readonly isAtEnd: boolean;
1212 readonly userScrollSessionActive: boolean;
1313 };
......@@ -41,6 +41,8 @@ export function resolveThreadFeedLiveFollow(
4141 return false;
4242 case "user-scroll-end":
4343 return event.userScrollSessionActive ? event.isAtEnd : current;
44 case "disclosure-settled":
45 return !event.userScrollSessionActive && event.isAtEnd;
4446 case "scroll":
4547 if (event.userScrollSessionActive) {
4648 return false;
apps/mobile/src/features/threads/thread-work-log.tsx+47-17
......@@ -2,7 +2,7 @@ import * as Haptics from "expo-haptics";
22import { type AppSymbolName, SymbolView } from "../../components/AppSymbol";
33import { MaskedView } from "@expo/ui/community/masked-view";
44import { useIsFocused } from "@react-navigation/native";
5import { useEffect, useId, useState, type ComponentProps } from "react";
5import { useEffect, useId, useLayoutEffect, useState, type ComponentProps } from "react";
66import {
77 AccessibilityInfo,
88 AppState,
......@@ -46,6 +46,44 @@ const WORK_LOG_LAYOUT_TRANSITION = LinearTransition.duration(THREAD_DISCLOSURE_T
4646const WORK_LOG_DETAIL_ENTER_TRANSITION = FadeIn.duration(140);
4747const WORK_LOG_DETAIL_EXIT_TRANSITION = FadeOut.duration(120);
4848
49export function ThreadDisclosureChevron(props: {
50 readonly expanded: boolean;
51 readonly collapsedDirection: "right" | "down";
52 readonly size: number;
53 readonly tintColor: ColorValue;
54}) {
55 const expandedAngle = props.collapsedDirection === "right" ? 90 : 180;
56 const rotation = useSharedValue(props.expanded ? expandedAngle : 0);
57
58 useLayoutEffect(() => {
59 rotation.value = withTiming(props.expanded ? expandedAngle : 0, {
60 duration: THREAD_DISCLOSURE_TRANSITION_MS,
61 reduceMotion: ReduceMotion.System,
62 });
63 }, [expandedAngle, props.expanded, rotation]);
64
65 const rotationStyle = useAnimatedStyle(() => ({
66 transform: [{ rotate: `${rotation.value}deg` }],
67 }));
68
69 return (
70 <Animated.View
71 accessible={false}
72 accessibilityElementsHidden
73 importantForAccessibility="no-hide-descendants"
74 pointerEvents="none"
75 style={[{ width: props.size, height: props.size }, rotationStyle]}
76 >
77 <SymbolView
78 name={props.collapsedDirection === "right" ? "chevron.right" : "chevron.down"}
79 size={props.size}
80 tintColor={props.tintColor}
81 type="monochrome"
82 />
83 </Animated.View>
84 );
85}
86
4987function ShimmerWorkContent(props: {
5088 readonly highlighted: boolean;
5189 readonly icon: AppSymbolName;
......@@ -264,7 +302,7 @@ const WORK_ROW_HEIGHT = 32; // min-h-8
264302const WORK_ROW_GAP = 1; // gap-px
265303const WORK_LOG_BOTTOM_MARGIN = 4; // mb-1
266304
267export const WORK_GROUP_TOGGLE_HEIGHT = 36; // min-h-8 (32) + mb-1 (4)
305export const WORK_GROUP_TOGGLE_HEIGHT = 32; // min-h-8
268306
269307export function collapsedWorkLogHeight(activities: ReadonlyArray<ThreadFeedActivity>): number {
270308 const rows = activities;
......@@ -376,15 +414,11 @@ export function ThreadWorkLog(props: {
376414 ) : null}
377415 <View className="h-4 w-4 items-center justify-center">
378416 {canExpand ? (
379 <SymbolView
380 name={
381 expanded
382 ? { ios: "chevron.up", android: "keyboard_arrow_up" }
383 : { ios: "chevron.down", android: "keyboard_arrow_down" }
384 }
417 <ThreadDisclosureChevron
418 expanded={expanded}
419 collapsedDirection="down"
385420 size={11}
386421 tintColor={props.iconSubtleColor}
387 type="monochrome"
388422 />
389423 ) : null}
390424 </View>
......@@ -444,7 +478,7 @@ export function ThreadWorkGroupToggle(props: {
444478 const icon = toolGroupSummarySymbolName(props.summaryKind);
445479
446480 return (
447 <View className="-mx-1 mb-1 px-1 py-0">
481 <View className="-mx-1 px-1 py-0">
448482 <Pressable
449483 accessibilityRole="button"
450484 accessibilityState={{ expanded: props.expanded }}
......@@ -479,15 +513,11 @@ export function ThreadWorkGroupToggle(props: {
479513 </Text>
480514 </>
481515 )}
482 <SymbolView
483 name={
484 props.expanded
485 ? { ios: "chevron.up", android: "keyboard_arrow_up" }
486 : { ios: "chevron.down", android: "keyboard_arrow_down" }
487 }
516 <ThreadDisclosureChevron
517 expanded={props.expanded}
518 collapsedDirection="down"
488519 size={11}
489520 tintColor={props.iconSubtleColor}
490 type="monochrome"
491521 />
492522 </Pressable>
493523 </View>
patches/@legendapp__list@3.3.5.patch+156-90
......@@ -241,10 +241,35 @@ index ce1fe00001c9e5aee6c6ea8bb2d4757d4586d002..3ccf6f16067152dfcb0c143371e2ec6a
241241 * Number of columns to render items in.
242242 * @default 1
243243diff --git a/react-native.js b/react-native.js
244index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c3ffe83af 100644
244index b3c5a306b293f797a8b338adfca3060c0f6db22b..24d0763aef074411eb7d17f0feb8df752a843de0 100644
245245--- a/react-native.js
246246+++ b/react-native.js
247@@ -954,7 +954,7 @@ function setInitialRenderState(ctx, {
247@@ -717,6 +717,15 @@ function hasActiveInitialScroll(state) {
248 return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
249 }
250
251+// Size-only changes may not emit a scroll event to refresh the edge signal.
252+function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) {
253+ const { queuedInitialLayout, scroll, scrollLength } = ctx.state;
254+ if (!(contentSize > 0 && queuedInitialLayout)) {
255+ return peek$(ctx, "isAtEnd");
256+ }
257+ return contentSize < scrollLength || contentSize - scroll - scrollLength - getContentInsetEnd(ctx) <= EDGE_POSITION_EPSILON;
258+}
259+
260 // src/utils/checkAtBottom.ts
261 function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
262 var _a3;
263@@ -737,7 +746,7 @@ function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
264 const insetEnd = getContentInsetEnd(ctx);
265 const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
266 const isContentLess = contentSize < scrollLength;
267- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
268+ set$(ctx, "isAtEnd", getIsAtEnd(ctx, contentSize));
269 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
270 set$(
271 ctx,
272@@ -954,7 +963,7 @@ function setInitialRenderState(ctx, {
248273 if (didInitialScroll) {
249274 state.didFinishInitialScroll = true;
250275 }
......@@ -253,7 +278,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
253278 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
254279 set$(ctx, "readyToRender", true);
255280 setAdaptiveRender(ctx, "normal", "ready");
256@@ -1090,7 +1090,7 @@ function getRawContentLength(ctx) {
281@@ -1090,7 +1099,7 @@ function getRawContentLength(ctx) {
257282 function getAlignItemsAtEndPadding(ctx) {
258283 const { state } = ctx;
259284 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
......@@ -262,7 +287,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
262287 }
263288 function updateContentMetricsState(ctx) {
264289 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
265@@ -1115,6 +1115,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
290@@ -1115,6 +1124,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
266291 totalSize += add;
267292 }
268293 if (prevTotalSize !== totalSize) {
......@@ -273,7 +298,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
273298 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
274299 state.pendingTotalSize = totalSize;
275300 } else {
276@@ -1304,18 +1308,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
301@@ -1304,18 +1317,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
277302 }
278303
279304 // src/core/clampScrollOffset.ts
......@@ -299,7 +324,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
299324 return clampedOffset;
300325 }
301326
302@@ -1451,10 +1460,10 @@ function checkFinishedScrollFrame(ctx) {
327@@ -1451,10 +1469,10 @@ function checkFinishedScrollFrame(ctx) {
303328 finishScrollTo(ctx);
304329 }
305330 }
......@@ -312,7 +337,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
312337 x: ctx.state.props.horizontal ? offset : 0,
313338 y: ctx.state.props.horizontal ? 0 : offset
314339 });
315@@ -1503,7 +1512,10 @@ function checkFinishedScrollFallback(ctx) {
340@@ -1503,7 +1521,10 @@ function checkFinishedScrollFallback(ctx) {
316341 );
317342 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
318343 } else if (shouldRetryUnalignedEndScroll) {
......@@ -324,7 +349,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
324349 scheduleFallbackCheck(100);
325350 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
326351 finishScrollTo(ctx);
327@@ -1560,15 +1572,28 @@ function doMaintainScrollAtEnd(ctx) {
352@@ -1560,15 +1581,28 @@ function doMaintainScrollAtEnd(ctx) {
328353 } = state;
329354 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
330355 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
......@@ -354,7 +379,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
354379 }
355380 if (!state.maintainingScrollAtEnd) {
356381 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
357@@ -1591,9 +1616,18 @@ function doMaintainScrollAtEnd(ctx) {
382@@ -1591,9 +1625,18 @@ function doMaintainScrollAtEnd(ctx) {
358383 y: 0
359384 });
360385 } else {
......@@ -376,7 +401,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
376401 }
377402 setTimeout(
378403 () => {
379@@ -1624,6 +1658,10 @@ function doMaintainScrollAtEnd(ctx) {
404@@ -1624,6 +1667,10 @@ function doMaintainScrollAtEnd(ctx) {
380405 function requestAdjust(ctx, positionDiff, dataChanged) {
381406 const state = ctx.state;
382407 if (Math.abs(positionDiff) > 0.1) {
......@@ -387,7 +412,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
387412 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
388413 const doit = () => {
389414 if (needsScrollWorkaround) {
390@@ -1728,7 +1766,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
415@@ -1728,7 +1775,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
391416 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
392417 return 0;
393418 }
......@@ -398,7 +423,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
398423 const clampDelta = maxScroll - state.scroll;
399424 if (unresolvedAmount < 0) {
400425 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
401@@ -1790,7 +1830,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
426@@ -1790,7 +1839,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
402427 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
403428 return true;
404429 }
......@@ -407,7 +432,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
407432 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
408433 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
409434 if (isAtExpectedNativeClamp) {
410@@ -1923,7 +1963,7 @@ function prepareMVCP(ctx, dataChanged) {
435@@ -1923,7 +1972,7 @@ function prepareMVCP(ctx, dataChanged) {
411436 if (diff > 0) {
412437 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
413438 } else {
......@@ -416,7 +441,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
416441 state.scroll = maxScroll;
417442 state.scrollPending = maxScroll;
418443 diff = 0;
419@@ -2320,8 +2360,121 @@ function scrollToIndex(ctx, {
444@@ -2320,8 +2369,121 @@ function scrollToIndex(ctx, {
420445 }
421446
422447 // src/core/initialScroll.ts
......@@ -538,7 +563,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
538563 const requestedIndex = target.index;
539564 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
540565 const itemSize = getItemSizeAtIndex(ctx, index);
541@@ -2747,7 +2900,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
566@@ -2747,7 +2909,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
542567 return;
543568 }
544569 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
......@@ -549,7 +574,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
549574 if (!shouldKeepEndTargetAlive) {
550575 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
551576 clearPendingInitialScrollFooterLayout(ctx, {
552@@ -4672,7 +4827,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
577@@ -4672,7 +4836,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
553578 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
554579 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
555580 isReady = !hasUnknownTailSize;
......@@ -559,7 +584,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
559584 } else if (anchorIndex >= 0) {
560585 isReady = false;
561586 }
562@@ -4692,6 +4848,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
587@@ -4692,6 +4857,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
563588 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
564589 }
565590 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
......@@ -572,7 +597,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
572597 }
573598 return nextSize;
574599 }
575@@ -5715,6 +5877,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
600@@ -5715,6 +5886,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
576601 horizontal
577602 }) {
578603 const ctx = useStateContext();
......@@ -580,7 +605,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
580605 const columnWrapperStyle = ctx.columnWrapperStyle;
581606 const animSize = useValue$("totalSize");
582607 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
583@@ -5725,6 +5888,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
608@@ -5725,6 +5897,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
584609 opacity: isVisible ? 1 : 0,
585610 width: animSize
586611 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
......@@ -594,7 +619,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
594619 if (columnWrapperStyle) {
595620 const { columnGap, rowGap, gap } = columnWrapperStyle;
596621 const gapX = columnGap || gap || 0;
597@@ -5745,7 +5915,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
622@@ -5745,7 +5924,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
598623 }
599624 }
600625 }
......@@ -604,7 +629,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
604629 });
605630 var Containers = typedMemo(function Containers2({
606631 freshDataTransitionEpoch,
607@@ -5896,7 +6067,12 @@ var StyleSheet = ReactNative.StyleSheet;
632@@ -5896,7 +6076,12 @@ var StyleSheet = ReactNative.StyleSheet;
608633
609634 // src/components/ListComponent.tsx
610635 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
......@@ -617,7 +642,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
617642 if (alignItemsAtEndPadding <= 0) {
618643 return null;
619644 }
620@@ -5929,8 +6105,12 @@ var ListComponent = typedMemo(function ListComponent2({
645@@ -5929,8 +6114,12 @@ var ListComponent = typedMemo(function ListComponent2({
621646 refScrollView,
622647 renderScrollComponent,
623648 onLayoutFooter,
......@@ -630,7 +655,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
630655 scrollAdjustHandler,
631656 snapToIndices,
632657 stickyHeaderConfig,
633@@ -6001,7 +6181,17 @@ var ListComponent = typedMemo(function ListComponent2({
658@@ -6001,7 +6190,17 @@ var ListComponent = typedMemo(function ListComponent2({
634659 SnapOrScroll,
635660 {
636661 ...rest,
......@@ -649,7 +674,16 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
649674 contentContainerStyle: [
650675 horizontal ? { height: "100%" } : {},
651676 contentContainerStyle,
652@@ -7075,6 +7265,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
677@@ -6751,7 +6950,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
678 endBuffered: state.endBuffered,
679 getAverageItemSizes: () => getAverageItemSizes(state),
680 indexByKey: (key) => state.indexByKey.get(key),
681- isAtEnd: peek$(ctx, "isAtEnd"),
682+ isAtEnd: getIsAtEnd(ctx),
683 isAtStart: peek$(ctx, "isAtStart"),
684 isEndReached: state.isEndReached,
685 isNearEnd: peek$(ctx, "isNearEnd"),
686@@ -7075,6 +7274,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
653687 dataVersion,
654688 drawDistance = 250,
655689 contentInsetEndAdjustment,
......@@ -657,7 +691,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
657691 estimatedItemSize = 100,
658692 estimatedListSize,
659693 extraData,
660@@ -7132,10 +7323,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
694@@ -7132,10 +7332,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
661695 const animatedPropsInternal = props.animatedPropsInternal;
662696 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
663697 const positionComponentInternal = props.positionComponentInternal;
......@@ -670,7 +704,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
670704 stickyPositionComponentInternal: _stickyPositionComponentInternal,
671705 ...restProps
672706 } = rest;
673@@ -7200,7 +7393,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
707@@ -7200,7 +7402,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
674708 const combinedRef = useCombinedRef(refScroller, refScrollView);
675709 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
676710 const stickyHeaderIndices = stickyHeaderIndicesProp;
......@@ -679,7 +713,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
679713 const previousContentInsetEndAdjustmentRef = React2.useRef(contentInsetEndAdjustmentResolved);
680714 const alwaysRenderIndices = React2.useMemo(() => {
681715 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
682@@ -7341,6 +7534,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
716@@ -7341,6 +7543,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
683717 contentContainerAlignItems: contentContainerStyle.alignItems,
684718 contentInset,
685719 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
......@@ -687,7 +721,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
687721 data: dataProp,
688722 dataKey,
689723 dataVersion,
690@@ -7372,6 +7566,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
724@@ -7372,6 +7575,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
691725 renderItem,
692726 rtl,
693727 snapToIndices,
......@@ -695,7 +729,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
695729 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
696730 stickyHeaderIndicesSet: React2.useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
697731 stickyPositionComponentInternal,
698@@ -7423,6 +7618,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
732@@ -7423,6 +7627,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
699733 return void 0;
700734 }
701735 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
......@@ -709,7 +743,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
709743 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
710744 }, [usesBootstrapInitialScroll]);
711745 React2.useLayoutEffect(() => {
712@@ -7547,6 +7749,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
746@@ -7547,6 +7758,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
713747 [
714748 dataKey,
715749 dataVersion,
......@@ -717,7 +751,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
717751 memoizedLastItemKeys.join(","),
718752 numColumnsProp,
719753 nextScrollAxisGap,
720@@ -7643,6 +7846,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
754@@ -7643,6 +7855,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
721755 () => ({
722756 getRenderedItem: (key) => getRenderedItem(ctx, key),
723757 onMomentumScrollEnd: (event) => {
......@@ -725,7 +759,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
725759 checkFinishedScrollFallback(ctx);
726760 if (state.props.onMomentumScrollEnd) {
727761 state.props.onMomentumScrollEnd(event);
728@@ -7651,6 +7855,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
762@@ -7651,6 +7864,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
729763 onScroll: (event) => onScroll(ctx, event),
730764 onScrollBeginDrag: (event) => {
731765 var _a4, _b2;
......@@ -734,7 +768,7 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
734768 prepareReachedEdgeForNextUserScroll(ctx);
735769 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
736770 },
737@@ -7676,11 +7882,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
771@@ -7676,11 +7891,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
738772 ListFooterComponent,
739773 ListFooterComponentStyle,
740774 ListHeaderComponent,
......@@ -754,10 +788,35 @@ index b3c5a306b293f797a8b338adfca3060c0f6db22b..eb51ff0c375d7e5bd3473fca83995c7c
754788 recycleItems,
755789 refreshControl: refreshControlElement ? stylePaddingTopState > 0 ? React2__namespace.cloneElement(refreshControlElement, {
756790diff --git a/react-native.mjs b/react-native.mjs
757index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318d57e29dc 100644
791index 40e87cda8c9bc79a889e5542f29af429a24b24d4..90e0d1a9dfd07d0212aae308f547b9ff7e3ad022 100644
758792--- a/react-native.mjs
759793+++ b/react-native.mjs
760@@ -933,7 +933,7 @@ function setInitialRenderState(ctx, {
794@@ -696,6 +696,15 @@ function hasActiveInitialScroll(state) {
795 return !!(state == null ? void 0 : state.initialScroll) && !state.didFinishInitialScroll;
796 }
797
798+// Size-only changes may not emit a scroll event to refresh the edge signal.
799+function getIsAtEnd(ctx, contentSize = getContentSize(ctx)) {
800+ const { queuedInitialLayout, scroll, scrollLength } = ctx.state;
801+ if (!(contentSize > 0 && queuedInitialLayout)) {
802+ return peek$(ctx, "isAtEnd");
803+ }
804+ return contentSize < scrollLength || contentSize - scroll - scrollLength - getContentInsetEnd(ctx) <= EDGE_POSITION_EPSILON;
805+}
806+
807 // src/utils/checkAtBottom.ts
808 function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
809 var _a3;
810@@ -716,7 +725,7 @@ function checkAtBottom(ctx, allowedEdge, allowGateCreatedInCurrentCheck) {
811 const insetEnd = getContentInsetEnd(ctx);
812 const distanceFromEnd = contentSize - scroll - scrollLength - insetEnd;
813 const isContentLess = contentSize < scrollLength;
814- set$(ctx, "isAtEnd", isContentLess || distanceFromEnd <= EDGE_POSITION_EPSILON);
815+ set$(ctx, "isAtEnd", getIsAtEnd(ctx, contentSize));
816 set$(ctx, "isNearEnd", isContentLess || distanceFromEnd <= onEndReachedThreshold * scrollLength);
817 set$(
818 ctx,
819@@ -933,7 +942,7 @@ function setInitialRenderState(ctx, {
761820 if (didInitialScroll) {
762821 state.didFinishInitialScroll = true;
763822 }
......@@ -766,7 +825,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
766825 if (isReadyToRender && !peek$(ctx, "readyToRender")) {
767826 set$(ctx, "readyToRender", true);
768827 setAdaptiveRender(ctx, "normal", "ready");
769@@ -1069,7 +1069,7 @@ function getRawContentLength(ctx) {
828@@ -1069,7 +1078,7 @@ function getRawContentLength(ctx) {
770829 function getAlignItemsAtEndPadding(ctx) {
771830 const { state } = ctx;
772831 const shouldPad = !!state.props.alignItemsAtEndPaddingEnabled && !state.props.horizontal && state.props.data.length > 0 && state.scrollLength > 0;
......@@ -775,7 +834,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
775834 }
776835 function updateContentMetricsState(ctx) {
777836 const previousPadding = peek$(ctx, "alignItemsAtEndPadding") || 0;
778@@ -1094,6 +1094,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
837@@ -1094,6 +1103,10 @@ function addTotalSize(ctx, key, add, notifyTotalSize = true) {
779838 totalSize += add;
780839 }
781840 if (prevTotalSize !== totalSize) {
......@@ -786,7 +845,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
786845 if (!IsNewArchitecture && state.initialScroll && totalSize < prevTotalSize) {
787846 state.pendingTotalSize = totalSize;
788847 } else {
789@@ -1283,18 +1287,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
848@@ -1283,18 +1296,23 @@ function calculateOffsetWithOffsetPosition(ctx, offsetParam, params) {
790849 }
791850
792851 // src/core/clampScrollOffset.ts
......@@ -812,7 +871,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
812871 return clampedOffset;
813872 }
814873
815@@ -1430,10 +1439,10 @@ function checkFinishedScrollFrame(ctx) {
874@@ -1430,10 +1448,10 @@ function checkFinishedScrollFrame(ctx) {
816875 finishScrollTo(ctx);
817876 }
818877 }
......@@ -825,7 +884,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
825884 x: ctx.state.props.horizontal ? offset : 0,
826885 y: ctx.state.props.horizontal ? 0 : offset
827886 });
828@@ -1482,7 +1491,10 @@ function checkFinishedScrollFallback(ctx) {
887@@ -1482,7 +1500,10 @@ function checkFinishedScrollFallback(ctx) {
829888 );
830889 scheduleFallbackCheck(SILENT_INITIAL_SCROLL_RETRY_DELAY_MS);
831890 } else if (shouldRetryUnalignedEndScroll) {
......@@ -837,7 +896,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
837896 scheduleFallbackCheck(100);
838897 } else if (shouldFinishZeroTarget || shouldFinishAfterObservedScroll || canFinishInitialScrollWithoutNativeProgress || canFinishAfterSilentNativeDispatch || numChecks > maxChecks) {
839898 finishScrollTo(ctx);
840@@ -1539,15 +1551,28 @@ function doMaintainScrollAtEnd(ctx) {
899@@ -1539,15 +1560,28 @@ function doMaintainScrollAtEnd(ctx) {
841900 } = state;
842901 const isWithinMaintainScrollAtEndThreshold = peek$(ctx, "isWithinMaintainScrollAtEndThreshold");
843902 const shouldMaintainScrollAtEnd = !!(isWithinMaintainScrollAtEndThreshold && maintainScrollAtEnd && didContainersLayout);
......@@ -867,7 +926,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
867926 }
868927 if (!state.maintainingScrollAtEnd) {
869928 const pendingState = maintainScrollAtEnd.animated ? "pending-animated" : "pending-instant";
870@@ -1570,9 +1595,18 @@ function doMaintainScrollAtEnd(ctx) {
929@@ -1570,9 +1604,18 @@ function doMaintainScrollAtEnd(ctx) {
871930 y: 0
872931 });
873932 } else {
......@@ -889,7 +948,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
889948 }
890949 setTimeout(
891950 () => {
892@@ -1603,6 +1637,10 @@ function doMaintainScrollAtEnd(ctx) {
951@@ -1603,6 +1646,10 @@ function doMaintainScrollAtEnd(ctx) {
893952 function requestAdjust(ctx, positionDiff, dataChanged) {
894953 const state = ctx.state;
895954 if (Math.abs(positionDiff) > 0.1) {
......@@ -900,7 +959,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
900959 const needsScrollWorkaround = Platform.OS === "android" && !IsNewArchitecture && dataChanged && state.scroll <= positionDiff;
901960 const doit = () => {
902961 if (needsScrollWorkaround) {
903@@ -1707,7 +1745,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
962@@ -1707,7 +1754,9 @@ function getPredictedNativeClamp(state, unresolvedAmount, totalSize) {
904963 if (Math.abs(unresolvedAmount) <= MVCP_POSITION_EPSILON) {
905964 return 0;
906965 }
......@@ -911,7 +970,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
911970 const clampDelta = maxScroll - state.scroll;
912971 if (unresolvedAmount < 0) {
913972 return Math.max(unresolvedAmount, Math.min(0, clampDelta));
914@@ -1769,7 +1809,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
973@@ -1769,7 +1818,7 @@ function resolvePendingNativeMVCPAdjust(ctx, newScroll) {
915974 settlePendingNativeMVCPAdjust(ctx, remainingAfterManual, nativeDelta);
916975 return true;
917976 }
......@@ -920,7 +979,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
920979 const distanceToClamp = Math.abs(newScroll - expectedNativeClampScroll);
921980 const isAtExpectedNativeClamp = distanceToClamp <= NATIVE_END_CLAMP_EPSILON;
922981 if (isAtExpectedNativeClamp) {
923@@ -1902,7 +1942,7 @@ function prepareMVCP(ctx, dataChanged) {
982@@ -1902,7 +1951,7 @@ function prepareMVCP(ctx, dataChanged) {
924983 if (diff > 0) {
925984 diff = Math.max(0, totalSize - state.scroll - state.scrollLength);
926985 } else {
......@@ -929,7 +988,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
929988 state.scroll = maxScroll;
930989 state.scrollPending = maxScroll;
931990 diff = 0;
932@@ -2299,8 +2339,121 @@ function scrollToIndex(ctx, {
991@@ -2299,8 +2348,121 @@ function scrollToIndex(ctx, {
933992 }
934993
935994 // src/core/initialScroll.ts
......@@ -1051,7 +1110,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
10511110 const requestedIndex = target.index;
10521111 const index = requestedIndex !== void 0 ? clampScrollIndex(requestedIndex, ctx.state.props.data.length) : void 0;
10531112 const itemSize = getItemSizeAtIndex(ctx, index);
1054@@ -2726,7 +2879,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
1113@@ -2726,7 +2888,9 @@ function clearFinishedBootstrapInitialScrollTargetIfMovedAway(ctx) {
10551114 return;
10561115 }
10571116 if (didFinishedInitialScrollMoveAwayFromTarget(ctx, initialScroll)) {
......@@ -1062,7 +1121,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
10621121 if (!shouldKeepEndTargetAlive) {
10631122 if (shouldPreserveInitialScrollForFooterLayout(initialScroll)) {
10641123 clearPendingInitialScrollFooterLayout(ctx, {
1065@@ -4651,7 +4806,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1124@@ -4651,7 +4815,8 @@ function maybeUpdateAnchoredEndSpace(ctx) {
10661125 contentBelowAnchor = Math.max(0, contentBelowAnchor - ctx.scrollAxisGap);
10671126 contentBelowAnchor += (ctx.values.get("footerSize") || 0) + getStylePaddingEnd(state.props);
10681127 isReady = !hasUnknownTailSize;
......@@ -1072,7 +1131,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
10721131 } else if (anchorIndex >= 0) {
10731132 isReady = false;
10741133 }
1075@@ -4671,6 +4827,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
1134@@ -4671,6 +4836,12 @@ function maybeUpdateAnchoredEndSpace(ctx) {
10761135 updateScroll(ctx, state.scroll, true, { markHasScrolled: false });
10771136 }
10781137 (_b = anchoredEndSpace == null ? void 0 : anchoredEndSpace.onReady) == null ? void 0 : _b.call(anchoredEndSpace, { anchorIndex: nextAnchorIndex, anchorKey: nextAnchorKey, size: nextSize });
......@@ -1085,7 +1144,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
10851144 }
10861145 return nextSize;
10871146 }
1088@@ -5694,6 +5856,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1147@@ -5694,6 +5865,7 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
10891148 horizontal
10901149 }) {
10911150 const ctx = useStateContext();
......@@ -1093,7 +1152,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
10931152 const columnWrapperStyle = ctx.columnWrapperStyle;
10941153 const animSize = useValue$("totalSize");
10951154 const [readyToRender, numColumns, otherAxisSize = 0] = useArr$(["readyToRender", "numColumns", "otherAxisSize"]);
1096@@ -5704,6 +5867,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1155@@ -5704,6 +5876,13 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
10971156 opacity: isVisible ? 1 : 0,
10981157 width: animSize
10991158 } : { height: animSize, minWidth: otherAxisSize, opacity: isVisible ? 1 : 0 };
......@@ -1107,7 +1166,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11071166 if (columnWrapperStyle) {
11081167 const { columnGap, rowGap, gap } = columnWrapperStyle;
11091168 const gapX = columnGap || gap || 0;
1110@@ -5724,7 +5894,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
1169@@ -5724,7 +5903,8 @@ var ContainersLayer = typedMemo(function ContainersLayer2({
11111170 }
11121171 }
11131172 }
......@@ -1117,7 +1176,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11171176 });
11181177 var Containers = typedMemo(function Containers2({
11191178 freshDataTransitionEpoch,
1120@@ -5875,7 +6046,12 @@ var StyleSheet = StyleSheet$1;
1179@@ -5875,7 +6055,12 @@ var StyleSheet = StyleSheet$1;
11211180
11221181 // src/components/ListComponent.tsx
11231182 var AlignItemsAtEndSpacer = typedMemo(function AlignItemsAtEndSpacer2({ horizontal }) {
......@@ -1130,7 +1189,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11301189 if (alignItemsAtEndPadding <= 0) {
11311190 return null;
11321191 }
1133@@ -5908,8 +6084,12 @@ var ListComponent = typedMemo(function ListComponent2({
1192@@ -5908,8 +6093,12 @@ var ListComponent = typedMemo(function ListComponent2({
11341193 refScrollView,
11351194 renderScrollComponent,
11361195 onLayoutFooter,
......@@ -1143,7 +1202,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11431202 scrollAdjustHandler,
11441203 snapToIndices,
11451204 stickyHeaderConfig,
1146@@ -5980,7 +6160,17 @@ var ListComponent = typedMemo(function ListComponent2({
1205@@ -5980,7 +6169,17 @@ var ListComponent = typedMemo(function ListComponent2({
11471206 SnapOrScroll,
11481207 {
11491208 ...rest,
......@@ -1162,7 +1221,16 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11621221 contentContainerStyle: [
11631222 horizontal ? { height: "100%" } : {},
11641223 contentContainerStyle,
1165@@ -7054,6 +7244,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1224@@ -6730,7 +6929,7 @@ function createImperativeHandle(ctx, scheduleImperativeScrollCommit) {
1225 endBuffered: state.endBuffered,
1226 getAverageItemSizes: () => getAverageItemSizes(state),
1227 indexByKey: (key) => state.indexByKey.get(key),
1228- isAtEnd: peek$(ctx, "isAtEnd"),
1229+ isAtEnd: getIsAtEnd(ctx),
1230 isAtStart: peek$(ctx, "isAtStart"),
1231 isEndReached: state.isEndReached,
1232 isNearEnd: peek$(ctx, "isNearEnd"),
1233@@ -7054,6 +7253,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
11661234 dataVersion,
11671235 drawDistance = 250,
11681236 contentInsetEndAdjustment,
......@@ -1170,7 +1238,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11701238 estimatedItemSize = 100,
11711239 estimatedListSize,
11721240 extraData,
1173@@ -7111,10 +7302,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1241@@ -7111,10 +7311,12 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
11741242 const animatedPropsInternal = props.animatedPropsInternal;
11751243 const anchoredEndSpaceOwner = (_a3 = props.anchoredEndSpaceOwnerInternal) != null ? _a3 : "list";
11761244 const positionComponentInternal = props.positionComponentInternal;
......@@ -1183,7 +1251,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11831251 stickyPositionComponentInternal: _stickyPositionComponentInternal,
11841252 ...restProps
11851253 } = rest;
1186@@ -7179,7 +7372,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1254@@ -7179,7 +7381,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
11871255 const combinedRef = useCombinedRef(refScroller, refScrollView);
11881256 const keyExtractor = keyExtractorProp != null ? keyExtractorProp : ((_item, index) => index.toString());
11891257 const stickyHeaderIndices = stickyHeaderIndicesProp;
......@@ -1192,7 +1260,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
11921260 const previousContentInsetEndAdjustmentRef = useRef(contentInsetEndAdjustmentResolved);
11931261 const alwaysRenderIndices = useMemo(() => {
11941262 const indices = getAlwaysRenderIndices(alwaysRender, dataProp, keyExtractor, anchoredEndSpace == null ? void 0 : anchoredEndSpace.anchorIndex);
1195@@ -7320,6 +7513,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1263@@ -7320,6 +7522,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
11961264 contentContainerAlignItems: contentContainerStyle.alignItems,
11971265 contentInset,
11981266 contentInsetEndAdjustment: contentInsetEndAdjustmentResolved,
......@@ -1200,7 +1268,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12001268 data: dataProp,
12011269 dataKey,
12021270 dataVersion,
1203@@ -7351,6 +7545,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1271@@ -7351,6 +7554,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12041272 renderItem,
12051273 rtl,
12061274 snapToIndices,
......@@ -1208,7 +1276,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12081276 stickyHeaderIndicesArr: stickyHeaderIndices != null ? stickyHeaderIndices : [],
12091277 stickyHeaderIndicesSet: useMemo(() => new Set(stickyHeaderIndices != null ? stickyHeaderIndices : []), [stickyHeaderIndices == null ? void 0 : stickyHeaderIndices.join(",")]),
12101278 stickyPositionComponentInternal,
1211@@ -7402,6 +7597,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1279@@ -7402,6 +7606,13 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12121280 return void 0;
12131281 }
12141282 const resolvedOffset = (_a4 = initialScroll.contentOffset) != null ? _a4 : resolveInitialScrollOffset(ctx, initialScroll);
......@@ -1222,7 +1290,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12221290 return usesBootstrapInitialScroll && ((_b2 = state.initialScrollSession) == null ? void 0 : _b2.kind) === "bootstrap" && Platform.OS === "web" ? void 0 : resolvedOffset;
12231291 }, [usesBootstrapInitialScroll]);
12241292 useLayoutEffect(() => {
1225@@ -7526,6 +7728,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1293@@ -7526,6 +7737,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12261294 [
12271295 dataKey,
12281296 dataVersion,
......@@ -1230,7 +1298,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12301298 memoizedLastItemKeys.join(","),
12311299 numColumnsProp,
12321300 nextScrollAxisGap,
1233@@ -7622,6 +7825,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1301@@ -7622,6 +7834,7 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12341302 () => ({
12351303 getRenderedItem: (key) => getRenderedItem(ctx, key),
12361304 onMomentumScrollEnd: (event) => {
......@@ -1238,7 +1306,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12381306 checkFinishedScrollFallback(ctx);
12391307 if (state.props.onMomentumScrollEnd) {
12401308 state.props.onMomentumScrollEnd(event);
1241@@ -7630,6 +7834,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1309@@ -7630,6 +7843,8 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12421310 onScroll: (event) => onScroll(ctx, event),
12431311 onScrollBeginDrag: (event) => {
12441312 var _a4, _b2;
......@@ -1247,7 +1315,7 @@ index 40e87cda8c9bc79a889e5542f29af429a24b24d4..700515b52c27a4ccd5343176121e7318
12471315 prepareReachedEdgeForNextUserScroll(ctx);
12481316 (_b2 = (_a4 = state.props).onScrollBeginDrag) == null ? void 0 : _b2.call(_a4, event);
12491317 },
1250@@ -7655,11 +7861,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
1318@@ -7655,11 +7870,18 @@ var LegendListInner = typedForwardRef(function LegendListInner2(props, forwarded
12511319 ListFooterComponent,
12521320 ListFooterComponentStyle,
12531321 ListHeaderComponent,
......@@ -1358,7 +1426,7 @@ index e5043320700b12f34f4c0babbc341f85ca8135c1..2ce63830a28636b21937a0741fd0e613
13581426 * Number of columns to render items in.
13591427 * @default 1
13601428diff --git a/reanimated.js b/reanimated.js
1361index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71ff8ce9f4d 100644
1429index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..fc03be2190c046f743ffde45ed189ee7868d0cec 100644
13621430--- a/reanimated.js
13631431+++ b/reanimated.js
13641432@@ -115,8 +115,10 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent(
......@@ -1404,7 +1472,7 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14041472 ref: refView,
14051473 style: viewStyle,
14061474 ...rest
1407@@ -141,6 +162,96 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent(
1475@@ -141,6 +162,95 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent(
14081476 children
14091477 );
14101478 });
......@@ -1418,12 +1486,12 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14181486+ const previousEpochRef = React__namespace.useRef(ctx.state.contentSizeAnimationEpoch || 0);
14191487+ const animationRunRef = React__namespace.useRef(0);
14201488+ const isAnimatingRef = React__namespace.useRef(false);
1421+ const [isAnimating, setIsAnimating] = React__namespace.useState(false);
14221489+ const transition = props.layoutTransition;
14231490+ const canAnimate = !!transition && typeof transition.getAnimationAndConfig === "function";
14241491+ const renderEpoch = ctx.state.contentSizeAnimationEpoch || 0;
1425+ const startsEligibleAnimation = canAnimate && logicalSize !== previousLogicalSizeRef.current && renderEpoch !== previousEpochRef.current && ctx.state.contentSizeAnimationEligible;
1426+ const useAnimatedAxis = isAnimating || startsEligibleAnimation;
1492+ // Keep the animated size attached after transitions finish. Reanimated
1493+ // retains removed animated props, so switching to a static size leaves
1494+ // native scroll bounds stuck while logical measurements keep changing.
14271495+ const animatedStyle = Reanimated.useAnimatedStyle(() => {
14281496+ const size = Math.max(0, baseSize.value + animatedDelta.value);
14291497+ return horizontal ? { width: size } : { height: size };
......@@ -1433,7 +1501,6 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14331501+ return;
14341502+ }
14351503+ isAnimatingRef.current = false;
1436+ setIsAnimating(false);
14371504+ const state = ctx.state;
14381505+ if (state.contentSizeAnimationActiveEpoch !== epoch) {
14391506+ return;
......@@ -1450,13 +1517,15 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14501517+ const previousLogicalSize = previousLogicalSizeRef.current;
14511518+ const delta = logicalSize - previousLogicalSize;
14521519+ const isNewEpoch = epoch !== previousEpochRef.current;
1453+ const isNewEligibleEpoch = canAnimate && isNewEpoch && state.contentSizeAnimationEligible;
1520+ // Retarget measurements received mid-animation, even in the same epoch.
1521+ // Updating only the base keeps the old delta and can shrink scroll bounds.
1522+ const shouldAnimate = canAnimate && (isAnimatingRef.current || isNewEpoch && state.contentSizeAnimationEligible);
14541523+ previousLogicalSizeRef.current = logicalSize;
14551524+ previousEpochRef.current = epoch;
14561525+ if (!delta) {
14571526+ return;
14581527+ }
1459+ if (!isNewEligibleEpoch) {
1528+ if (!shouldAnimate) {
14601529+ baseSize.value = logicalSize;
14611530+ if (signalName === "totalSize" && isNewEpoch && state.contentSizeAnimationEligible && !canAnimate) {
14621531+ state.contentSizeAnimationEligible = false;
......@@ -1480,7 +1549,6 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14801549+ state.contentSizeAnimationActiveSignals.add(signalName);
14811550+ const wasAnimating = isAnimatingRef.current;
14821551+ isAnimatingRef.current = true;
1483+ setIsAnimating(true);
14841552+ const run = ++animationRunRef.current;
14851553+ Reanimated.runOnUI((absoluteSize, previousSize, continuesAnimation, animationRun, animationEpoch) => {
14861554+ "worklet";
......@@ -1495,13 +1563,12 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
14951563+ }));
14961564+ })(logicalSize, previousLogicalSize, wasAnimating, run, epoch);
14971565+ }, [baseSize, animatedDelta, canAnimate, completeAnimation, ctx.state, logicalSize, renderEpoch, signalName, transition]);
1498+ const sizeStyle = useAnimatedAxis ? animatedStyle : horizontal ? { width: logicalSize } : { height: logicalSize };
1499+ return /* @__PURE__ */ React__namespace.createElement(Reanimated__default.default.View, { ...rest, style: [style, sizeStyle] }, children);
1566+ return /* @__PURE__ */ React__namespace.createElement(Reanimated__default.default.View, { ...rest, style: [style, animatedStyle] }, children);
15001567+});
15011568 function setSharedValueValue(sharedValue, value) {
15021569 if (!sharedValue) {
15031570 return;
1504@@ -245,10 +356,19 @@ var LegendListForwardedRef = typedMemo(
1571@@ -245,10 +355,19 @@ var LegendListForwardedRef = typedMemo(
15051572 );
15061573 };
15071574 }, [hasItemLayoutAnimation, recycleItems]);
......@@ -1522,7 +1589,7 @@ index f1265fad74189591b5aae86cf2e3a31f9c0fdb02..59e218cb79f56c34995fb86c5de5a71f
15221589 renderScrollComponent: renderReanimatedScrollComponent,
15231590 ...IsNewArchitecture ? { stickyPositionComponentInternal } : {}
15241591diff --git a/reanimated.mjs b/reanimated.mjs
1525index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a5725398942 100644
1592index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..6ab929250c8924e2d9919ee09a4f774238fbb90a 100644
15261593--- a/reanimated.mjs
15271594+++ b/reanimated.mjs
15281595@@ -1,7 +1,7 @@
......@@ -1577,7 +1644,7 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
15771644 ref: refView,
15781645 style: viewStyle,
15791646 ...rest
1580@@ -117,6 +138,96 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent(
1647@@ -117,6 +138,95 @@ var ReanimatedPositionView = typedMemo(function ReanimatedPositionViewComponent(
15811648 children
15821649 );
15831650 });
......@@ -1591,12 +1658,12 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
15911658+ const previousEpochRef = React.useRef(ctx.state.contentSizeAnimationEpoch || 0);
15921659+ const animationRunRef = React.useRef(0);
15931660+ const isAnimatingRef = React.useRef(false);
1594+ const [isAnimating, setIsAnimating] = React.useState(false);
15951661+ const transition = props.layoutTransition;
15961662+ const canAnimate = !!transition && typeof transition.getAnimationAndConfig === "function";
15971663+ const renderEpoch = ctx.state.contentSizeAnimationEpoch || 0;
1598+ const startsEligibleAnimation = canAnimate && logicalSize !== previousLogicalSizeRef.current && renderEpoch !== previousEpochRef.current && ctx.state.contentSizeAnimationEligible;
1599+ const useAnimatedAxis = isAnimating || startsEligibleAnimation;
1664+ // Keep the animated size attached after transitions finish. Reanimated
1665+ // retains removed animated props, so switching to a static size leaves
1666+ // native scroll bounds stuck while logical measurements keep changing.
16001667+ const animatedStyle = useAnimatedStyle(() => {
16011668+ const size = Math.max(0, baseSize.value + animatedDelta.value);
16021669+ return horizontal ? { width: size } : { height: size };
......@@ -1606,7 +1673,6 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
16061673+ return;
16071674+ }
16081675+ isAnimatingRef.current = false;
1609+ setIsAnimating(false);
16101676+ const state = ctx.state;
16111677+ if (state.contentSizeAnimationActiveEpoch !== epoch) {
16121678+ return;
......@@ -1623,13 +1689,15 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
16231689+ const previousLogicalSize = previousLogicalSizeRef.current;
16241690+ const delta = logicalSize - previousLogicalSize;
16251691+ const isNewEpoch = epoch !== previousEpochRef.current;
1626+ const isNewEligibleEpoch = canAnimate && isNewEpoch && state.contentSizeAnimationEligible;
1692+ // Retarget measurements received mid-animation, even in the same epoch.
1693+ // Updating only the base keeps the old delta and can shrink scroll bounds.
1694+ const shouldAnimate = canAnimate && (isAnimatingRef.current || isNewEpoch && state.contentSizeAnimationEligible);
16271695+ previousLogicalSizeRef.current = logicalSize;
16281696+ previousEpochRef.current = epoch;
16291697+ if (!delta) {
16301698+ return;
16311699+ }
1632+ if (!isNewEligibleEpoch) {
1700+ if (!shouldAnimate) {
16331701+ baseSize.value = logicalSize;
16341702+ if (signalName === "totalSize" && isNewEpoch && state.contentSizeAnimationEligible && !canAnimate) {
16351703+ state.contentSizeAnimationEligible = false;
......@@ -1653,7 +1721,6 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
16531721+ state.contentSizeAnimationActiveSignals.add(signalName);
16541722+ const wasAnimating = isAnimatingRef.current;
16551723+ isAnimatingRef.current = true;
1656+ setIsAnimating(true);
16571724+ const run = ++animationRunRef.current;
16581725+ runOnUI((absoluteSize, previousSize, continuesAnimation, animationRun, animationEpoch) => {
16591726+ "worklet";
......@@ -1668,13 +1735,12 @@ index 29a00d5dc084fd408a0e0a0a4d64e856d0ed1525..bd3c680eb4abeca786d538ebf7b23a57
16681735+ }));
16691736+ })(logicalSize, previousLogicalSize, wasAnimating, run, epoch);
16701737+ }, [baseSize, animatedDelta, canAnimate, completeAnimation, ctx.state, logicalSize, renderEpoch, signalName, transition]);
1671+ const sizeStyle = useAnimatedAxis ? animatedStyle : horizontal ? { width: logicalSize } : { height: logicalSize };
1672+ return /* @__PURE__ */ React.createElement(Reanimated.View, { ...rest, style: [style, sizeStyle] }, children);
1738+ return /* @__PURE__ */ React.createElement(Reanimated.View, { ...rest, style: [style, animatedStyle] }, children);
16731739+});
16741740 function setSharedValueValue(sharedValue, value) {
16751741 if (!sharedValue) {
16761742 return;
1677@@ -221,10 +332,19 @@ var LegendListForwardedRef = typedMemo(
1743@@ -221,10 +331,19 @@ var LegendListForwardedRef = typedMemo(
16781744 );
16791745 };
16801746 }, [hasItemLayoutAnimation, recycleItems]);
pnpm-lock.yaml+5-5
......@@ -86,7 +86,7 @@ patchedDependencies:
8686 '@effect/vitest@4.0.0-beta.103': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b
8787 '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2
8888 '@ff-labs/fff-node@0.9.4': ab9ff544009e1891cfe3930105862d3699007f38922a79f3c98d90018deca368
89 '@legendapp/list@3.3.5': f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b
89 '@legendapp/list@3.3.5': 03ec41339cd915ecb9a774a6b90cc2197c29038f7db67c4d2e55cd3971e5be43
9090 '@pierre/diffs@1.3.0-beta.10': 7ef7cb0cbabb17c15cdb137554068b36f15f6f5265e73fb51452aa3380db91aa
9191 '@react-native-ai/apple@0.12.0': 2d09870c2848d185cb05b53ed823a46e12dba519324d8dd8e584e28731990f9d
9292 '@react-native-menu/menu@2.0.0': f63d256bf6a97a873b5e628eb595bd6ef0075ddd5bdd890fc920f7a6024290dd
......@@ -228,7 +228,7 @@ importers:
228228 version: 57.0.14(@babel/core@7.29.7)(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(expo@57.0.18)(react-dom@19.2.3(react@19.2.3))(react-native-worklets@0.10.1(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
229229 '@legendapp/list':
230230 specifier: 'catalog:'
231 version: 3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
231 version: 3.3.5(patch_hash=03ec41339cd915ecb9a774a6b90cc2197c29038f7db67c4d2e55cd3971e5be43)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)
232232 '@noble/curves':
233233 specifier: 'catalog:'
234234 version: 1.9.1
......@@ -572,7 +572,7 @@ importers:
572572 version: 0.9.0
573573 '@legendapp/list':
574574 specifier: 'catalog:'
575 version: 3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
575 version: 3.3.5(patch_hash=03ec41339cd915ecb9a774a6b90cc2197c29038f7db67c4d2e55cd3971e5be43)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
576576 '@lexical/react':
577577 specifier: ^0.41.0
578578 version: 0.41.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(yjs@13.6.31)
......@@ -12892,7 +12892,7 @@ snapshots:
1289212892 dependencies:
1289312893 jsbi: 4.3.2
1289412894
12895 '@legendapp/list@3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
12895 '@legendapp/list@3.3.5(patch_hash=03ec41339cd915ecb9a774a6b90cc2197c29038f7db67c4d2e55cd3971e5be43)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)':
1289612896 dependencies:
1289712897 react: 19.2.3
1289812898 use-sync-external-store: 1.6.0(react@19.2.3)
......@@ -12900,7 +12900,7 @@ snapshots:
1290012900 react-dom: 19.2.3(react@19.2.3)
1290112901 react-native: 0.86.3(@babel/core@7.29.7)(@react-native/metro-config@0.86.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)
1290212902
12903 '@legendapp/list@3.3.5(patch_hash=f786e0ada19a32703f71a7230a819697ebff2527873dfb9997c8455947e0060b)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
12903 '@legendapp/list@3.3.5(patch_hash=03ec41339cd915ecb9a774a6b90cc2197c29038f7db67c4d2e55cd3971e5be43)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
1290412904 dependencies:
1290512905 react: 19.2.6
1290612906 use-sync-external-store: 1.6.0(react@19.2.6)