@souscheflabs/reanimated-flashlist 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/lib/AnimatedFlashList.d.ts +2 -2
  2. package/lib/AnimatedFlashList.d.ts.map +1 -1
  3. package/lib/AnimatedFlashList.js +291 -33
  4. package/lib/AnimatedFlashListItem.d.ts.map +1 -1
  5. package/lib/AnimatedFlashListItem.js +59 -48
  6. package/lib/__tests__/utils/test-utils.d.ts +62 -1
  7. package/lib/__tests__/utils/test-utils.d.ts.map +1 -1
  8. package/lib/__tests__/utils/test-utils.js +110 -2
  9. package/lib/constants/drag.d.ts +42 -7
  10. package/lib/constants/drag.d.ts.map +1 -1
  11. package/lib/constants/drag.js +41 -8
  12. package/lib/contexts/DragStateContext.d.ts +105 -2
  13. package/lib/contexts/DragStateContext.d.ts.map +1 -1
  14. package/lib/contexts/DragStateContext.js +178 -12
  15. package/lib/hooks/drag/index.d.ts +8 -0
  16. package/lib/hooks/drag/index.d.ts.map +1 -1
  17. package/lib/hooks/drag/index.js +10 -1
  18. package/lib/hooks/drag/useAutoscroll.d.ts +41 -0
  19. package/lib/hooks/drag/useAutoscroll.d.ts.map +1 -0
  20. package/lib/hooks/drag/useAutoscroll.js +60 -0
  21. package/lib/hooks/drag/useDragAnimatedStyle.d.ts +8 -12
  22. package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
  23. package/lib/hooks/drag/useDragAnimatedStyle.js +279 -243
  24. package/lib/hooks/drag/useDragCleanup.d.ts +27 -0
  25. package/lib/hooks/drag/useDragCleanup.d.ts.map +1 -0
  26. package/lib/hooks/drag/useDragCleanup.js +100 -0
  27. package/lib/hooks/drag/useDragGesture.d.ts +6 -1
  28. package/lib/hooks/drag/useDragGesture.d.ts.map +1 -1
  29. package/lib/hooks/drag/useDragGesture.js +179 -412
  30. package/lib/hooks/drag/useDragOverlay.d.ts +48 -0
  31. package/lib/hooks/drag/useDragOverlay.d.ts.map +1 -0
  32. package/lib/hooks/drag/useDragOverlay.js +87 -0
  33. package/lib/hooks/drag/useDragShift.d.ts +7 -5
  34. package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
  35. package/lib/hooks/drag/useDragShift.js +36 -243
  36. package/lib/hooks/drag/useDropCompensation.d.ts +6 -10
  37. package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
  38. package/lib/hooks/drag/useDropCompensation.js +16 -49
  39. package/lib/hooks/drag/useDropHandler.d.ts +48 -0
  40. package/lib/hooks/drag/useDropHandler.d.ts.map +1 -0
  41. package/lib/hooks/drag/useDropHandler.js +221 -0
  42. package/lib/hooks/drag/useOverlayCrossfade.d.ts +25 -0
  43. package/lib/hooks/drag/useOverlayCrossfade.d.ts.map +1 -0
  44. package/lib/hooks/drag/useOverlayCrossfade.js +70 -0
  45. package/lib/index.d.ts +1 -0
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +5 -1
  48. package/lib/types/drag.d.ts +0 -4
  49. package/lib/types/drag.d.ts.map +1 -1
  50. package/lib/utils/dragStateReset.d.ts +134 -0
  51. package/lib/utils/dragStateReset.d.ts.map +1 -0
  52. package/lib/utils/dragStateReset.js +114 -0
  53. package/lib/utils/hoverCalculation.d.ts +81 -0
  54. package/lib/utils/hoverCalculation.d.ts.map +1 -0
  55. package/lib/utils/hoverCalculation.js +87 -0
  56. package/package.json +12 -10
  57. package/src/AnimatedFlashList.tsx +483 -53
  58. package/src/AnimatedFlashListItem.tsx +72 -59
  59. package/src/__tests__/components/AnimatedFlashList.test.tsx +51 -51
  60. package/src/__tests__/components/AnimatedFlashListItem.test.tsx +33 -33
  61. package/src/__tests__/contexts/DragStateContext.test.tsx +30 -30
  62. package/src/__tests__/contexts/ListAnimationContext.test.tsx +58 -58
  63. package/src/__tests__/hooks/useDragAnimatedStyle.test.tsx +15 -15
  64. package/src/__tests__/hooks/useDragGesture.test.tsx +27 -29
  65. package/src/__tests__/hooks/useDragShift.test.tsx +23 -24
  66. package/src/__tests__/hooks/useDropCompensation.test.tsx +36 -35
  67. package/src/__tests__/hooks/useListEntryAnimation.test.tsx +24 -24
  68. package/src/__tests__/hooks/useListExitAnimation.test.tsx +34 -34
  69. package/src/__tests__/hooks/useOverlayCrossfade.test.tsx +57 -0
  70. package/src/__tests__/utils/test-utils.tsx +148 -2
  71. package/src/constants/drag.ts +46 -7
  72. package/src/contexts/DragStateContext.tsx +304 -16
  73. package/src/hooks/drag/index.ts +18 -0
  74. package/src/hooks/drag/useAutoscroll.ts +109 -0
  75. package/src/hooks/drag/useDragAnimatedStyle.ts +344 -293
  76. package/src/hooks/drag/useDragCleanup.ts +134 -0
  77. package/src/hooks/drag/useDragGesture.ts +244 -526
  78. package/src/hooks/drag/useDragOverlay.ts +141 -0
  79. package/src/hooks/drag/useDragShift.ts +37 -307
  80. package/src/hooks/drag/useDropCompensation.ts +16 -57
  81. package/src/hooks/drag/useDropHandler.ts +329 -0
  82. package/src/hooks/drag/useOverlayCrossfade.ts +84 -0
  83. package/src/index.ts +5 -0
  84. package/src/types/drag.ts +0 -4
  85. package/src/utils/dragStateReset.ts +202 -0
  86. package/src/utils/hoverCalculation.ts +180 -0
@@ -1,5 +1,5 @@
1
- import React from 'react';
2
- import type { AnimatedListItem, AnimatedFlashListProps, AnimatedFlashListRef } from './types';
1
+ import React from "react";
2
+ import type { AnimatedListItem, AnimatedFlashListProps, AnimatedFlashListRef } from "./types";
3
3
  export declare const AnimatedFlashList: <T extends AnimatedListItem>(props: AnimatedFlashListProps<T> & {
4
4
  ref?: React.ForwardedRef<AnimatedFlashListRef>;
5
5
  }) => React.ReactElement | null;
@@ -1 +1 @@
1
- {"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AAggBjB,eAAO,MAAM,iBAAiB,EAAyC,CACrE,CAAC,SAAS,gBAAgB,EAE1B,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAA;CAAE,KAClF,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
1
+ {"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KASN,MAAM,OAAO,CAAC;AA2Bf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AA65BjB,eAAO,MAAM,iBAAiB,EAAyC,CACrE,CAAC,SAAS,gBAAgB,EAE1B,KAAK,EAAE,sBAAsB,CAAC,CAAC,CAAC,GAAG;IACjC,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,oBAAoB,CAAC,CAAC;CAChD,KACE,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC"}
@@ -36,10 +36,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AnimatedFlashList = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
+ const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
40
+ const react_native_gesture_handler_1 = require("react-native-gesture-handler");
41
+ const react_native_worklets_1 = require("react-native-worklets");
39
42
  const flash_list_1 = require("@shopify/flash-list");
40
- const contexts_1 = require("./contexts");
43
+ const DragStateContext_1 = require("./contexts/DragStateContext");
44
+ const useOverlayCrossfade_1 = require("./hooks/drag/useOverlayCrossfade");
45
+ const ListAnimationContext_1 = require("./contexts/ListAnimationContext");
41
46
  const AnimatedFlashListItem_1 = require("./AnimatedFlashListItem");
42
- const constants_1 = require("./constants");
47
+ const drag_1 = require("./constants/drag");
43
48
  class AnimationErrorBoundary extends react_1.default.Component {
44
49
  state = { hasError: false };
45
50
  static getDerivedStateFromError() {
@@ -47,8 +52,8 @@ class AnimationErrorBoundary extends react_1.default.Component {
47
52
  }
48
53
  componentDidCatch(error) {
49
54
  if (__DEV__) {
50
- console.warn('[AnimatedFlashList] Animation error caught by error boundary. ' +
51
- 'Falling back to non-animated list.', error);
55
+ console.warn("[AnimatedFlashList] Animation error caught by error boundary. " +
56
+ "Falling back to non-animated list.", error);
52
57
  }
53
58
  }
54
59
  render() {
@@ -58,23 +63,270 @@ class AnimationErrorBoundary extends react_1.default.Component {
58
63
  return this.props.children;
59
64
  }
60
65
  }
66
+ const DragOverlay = react_1.default.memo(function DragOverlay({ data, totalItemsRef, renderItem }) {
67
+ const { overlayActive, overlayItemId, overlayBaseX, overlayBaseY, overlayWidth, overlayHeight, overlayTranslateY, overlayReady, overlayVisible, scrollOffset, dragStartScrollOffset, draggedScale, isDragging, overlayTouchOffset, overlayAbsoluteY, overlayContainerY, dragOverlayOpacity, overlayFrozenItemId, overlayRenderedOnUI, } = (0, DragStateContext_1.useDragState)();
68
+ // Drive overlay crossfade animation on UI thread
69
+ // This ensures smooth visibility transitions without JS scheduling races
70
+ (0, useOverlayCrossfade_1.useOverlayCrossfade)();
71
+ const [overlayId, setOverlayId] = (0, react_1.useState)(null);
72
+ const [frozenId, setFrozenId] = (0, react_1.useState)(null);
73
+ const setOverlayIdSafe = (0, react_1.useCallback)((nextId) => {
74
+ setOverlayId(nextId);
75
+ }, []);
76
+ const setFrozenIdSafe = (0, react_1.useCallback)((nextId) => {
77
+ setFrozenId(nextId);
78
+ }, []);
79
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => overlayItemId.value, (current, prev) => {
80
+ "worklet";
81
+ if (current === prev)
82
+ return;
83
+ (0, react_native_worklets_1.scheduleOnRN)(setOverlayIdSafe, current || null);
84
+ }, [setOverlayIdSafe]);
85
+ // Track frozen ID for rendering during fade-out
86
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => overlayFrozenItemId.value, (current, prev) => {
87
+ "worklet";
88
+ if (current === prev)
89
+ return;
90
+ (0, react_native_worklets_1.scheduleOnRN)(setFrozenIdSafe, current || null);
91
+ }, [setFrozenIdSafe]);
92
+ (0, react_1.useEffect)(() => {
93
+ overlayVisible.value = Boolean(overlayId);
94
+ }, [overlayId, overlayVisible]);
95
+ // Use frozenId to keep overlay rendered during fade-out
96
+ const overlayItem = (0, react_1.useMemo)(() => {
97
+ const id = frozenId || overlayId;
98
+ if (!id)
99
+ return null;
100
+ return data.find((item) => item.id === id) ?? null;
101
+ }, [data, frozenId, overlayId]);
102
+ // Create disabled gesture and false isDragging for overlay rendering
103
+ // This allows renderItem to show drag handle UI without functional gesture
104
+ const disabledGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan().enabled(false), []);
105
+ const falseIsDragging = (0, react_native_reanimated_1.useSharedValue)(false);
106
+ const overlayStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
107
+ // Use frozenItemId so overlay stays renderable during fade-out
108
+ const itemId = overlayFrozenItemId.value || overlayItemId.value;
109
+ // Keep rendering if frozenItemId is set (crossfade in progress) even if overlayReady is false.
110
+ // This prevents the overlay from becoming invisible before the crossfade completes,
111
+ // which would cause a brief moment where both overlay AND original are invisible.
112
+ // FIXED: Also require overlayVisible to ensure JS-side overlayItem exists before fading.
113
+ // overlayVisible is only cleared AFTER fade-out completes (in useOverlayCrossfade),
114
+ // so this doesn't break fade-out.
115
+ if (!itemId || (!overlayFrozenItemId.value && !overlayReady.value) || !overlayVisible.value) {
116
+ // Mark overlay as NOT rendered when returning invisible style
117
+ overlayRenderedOnUI.value = false;
118
+ return { opacity: 0, position: "absolute" };
119
+ }
120
+ // Mark overlay as rendered on UI thread - this is the KEY fix for the vanish bug.
121
+ // Setting this here (in useAnimatedStyle, which runs on UI thread) instead of
122
+ // in useEffect (JS thread) ensures the original item knows the overlay has
123
+ // actually rendered BEFORE the crossfade starts. This prevents the race condition
124
+ // where the original starts fading out before the overlay appears.
125
+ overlayRenderedOnUI.value = true;
126
+ const scale = isDragging.value ? draggedScale.value : 1;
127
+ // During active drag, use absolute finger position for reliable overlay tracking.
128
+ // This avoids lag from JS-thread scroll offset updates during simultaneous scroll+drag.
129
+ // Formula: top = absoluteY - touchOffset - containerY
130
+ // Where:
131
+ // - absoluteY = finger's current screen Y position
132
+ // - touchOffset = how far down in the item the finger started
133
+ // - containerY = overlay container's screen Y position
134
+ //
135
+ // During drop animation (isDropping), switch back to slot-based positioning
136
+ // since the finger is no longer tracking and we need to animate to final slot.
137
+ let top;
138
+ if (isDragging.value) {
139
+ // Active drag: follow finger precisely
140
+ top = overlayAbsoluteY.value - overlayTouchOffset.value - overlayContainerY.value;
141
+ }
142
+ else {
143
+ // Drop animation: use slot-based positioning with scroll compensation
144
+ // Note: scrollDelta is SUBTRACTED because overlayBaseY was measured at drag start
145
+ // (when scroll was dragStartScrollOffset). As user scrolls down (scrollDelta > 0),
146
+ // the target slot moves UP visually, so we subtract.
147
+ const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
148
+ top = overlayBaseY.value + overlayTranslateY.value - scrollDelta;
149
+ }
150
+ return {
151
+ position: "absolute",
152
+ left: overlayBaseX.value,
153
+ top,
154
+ width: overlayWidth.value,
155
+ height: overlayHeight.value,
156
+ zIndex: 1000,
157
+ transform: [{ scale }],
158
+ opacity: dragOverlayOpacity.value, // Uses shared crossfade value
159
+ };
160
+ });
161
+ // Only return null when BOTH overlayItem is null AND frozenId is empty
162
+ // This keeps the Animated.View mounted during fade-out (while frozenId is set)
163
+ if (!overlayItem && !frozenId)
164
+ return null;
165
+ const overlayIndex = overlayItem
166
+ ? data.findIndex((item) => item.id === overlayItem.id)
167
+ : -1;
168
+ const totalItems = totalItemsRef.current ?? data.length;
169
+ return (<react_native_reanimated_1.default.View pointerEvents="none" style={overlayStyle}>
170
+ {overlayItem &&
171
+ renderItem({
172
+ item: overlayItem,
173
+ index: overlayIndex,
174
+ totalItems,
175
+ animatedStyle: {},
176
+ dragHandleProps: {
177
+ gesture: disabledGesture,
178
+ isDragging: falseIsDragging,
179
+ },
180
+ isDragging: false,
181
+ isDragEnabled: false,
182
+ triggerExitAnimation: () => { },
183
+ resetExitAnimation: () => { },
184
+ })}
185
+ </react_native_reanimated_1.default.View>);
186
+ });
187
+ const ShiftedItemOverlays = react_1.default.memo(function ShiftedItemOverlays({ data, totalItemsRef, renderItem }) {
188
+ const { shiftedOverlays, shiftedOverlaysActive, shiftedOverlaysRendered } = (0, DragStateContext_1.useDragState)();
189
+ // State to hold overlay items on JS thread
190
+ const [overlayItems, setOverlayItems] = (0, react_1.useState)([]);
191
+ // Callback to update overlay items from JS thread
192
+ // v40.1: Y position is now viewport-relative (captured with scroll offset subtracted)
193
+ const updateOverlayItems = (0, react_1.useCallback)((overlays, active) => {
194
+ if (active && Object.keys(overlays).length > 0) {
195
+ const items = Object.entries(overlays)
196
+ .map(([id, pos]) => ({
197
+ item: data.find((d) => d.id === id),
198
+ position: { y: pos.baseY, height: pos.height },
199
+ }))
200
+ .filter((x) => x.item !== undefined);
201
+ setOverlayItems(items);
202
+ }
203
+ else {
204
+ setOverlayItems([]);
205
+ }
206
+ }, [data]);
207
+ // Watch for overlay changes
208
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => ({
209
+ active: shiftedOverlaysActive.value,
210
+ overlays: shiftedOverlays.value,
211
+ }), (state, prev) => {
212
+ "worklet";
213
+ // Only update when state changes
214
+ if (prev !== null &&
215
+ state.active === prev.active &&
216
+ Object.keys(state.overlays).length === Object.keys(prev.overlays).length) {
217
+ return;
218
+ }
219
+ (0, react_native_worklets_1.scheduleOnRN)(updateOverlayItems, state.overlays, state.active);
220
+ }, [updateOverlayItems]);
221
+ // v41.6: Use useLayoutEffect for earlier signal (runs after commit, before paint)
222
+ // This reduces the timing gap between overlay render and originals being hidden
223
+ (0, react_1.useLayoutEffect)(() => {
224
+ shiftedOverlaysRendered.value = overlayItems.length > 0;
225
+ }, [overlayItems.length, shiftedOverlaysRendered]);
226
+ // Create disabled gesture and false isDragging for overlay rendering
227
+ const disabledGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan().enabled(false), []);
228
+ const falseIsDragging = (0, react_native_reanimated_1.useSharedValue)(false);
229
+ if (overlayItems.length === 0)
230
+ return null;
231
+ const totalItems = totalItemsRef.current ?? data.length;
232
+ return (<>
233
+ {overlayItems.map(({ item, position }) => (<react_native_1.View key={`shifted-overlay-${item.id}`} style={{
234
+ position: "absolute",
235
+ top: position.y,
236
+ left: 0,
237
+ right: 0,
238
+ height: position.height,
239
+ zIndex: 999,
240
+ }} pointerEvents="none">
241
+ {renderItem({
242
+ item,
243
+ index: data.findIndex((d) => d.id === item.id),
244
+ totalItems,
245
+ animatedStyle: {},
246
+ dragHandleProps: {
247
+ gesture: disabledGesture,
248
+ isDragging: falseIsDragging,
249
+ },
250
+ isDragging: false,
251
+ isDragEnabled: false,
252
+ triggerExitAnimation: () => { },
253
+ resetExitAnimation: () => { },
254
+ })}
255
+ </react_native_1.View>))}
256
+ </>);
257
+ });
258
+ const DragContainer = react_1.default.memo(function DragContainer({ children, }) {
259
+ const { overlayContainerX, overlayContainerY, overlayContainerYLayout, overlayContainerReady, dragContainerRef, } = (0, DragStateContext_1.useDragState)();
260
+ const handleLayout = (0, react_1.useCallback)(() => {
261
+ const node = dragContainerRef.current;
262
+ if (!node)
263
+ return;
264
+ node.measureInWindow((x, y) => {
265
+ overlayContainerX.value = x;
266
+ overlayContainerY.value = y;
267
+ overlayContainerYLayout.value = y; // Keep measureInWindow value for shifted overlay calculations
268
+ overlayContainerReady.value = true;
269
+ });
270
+ }, [
271
+ overlayContainerX,
272
+ overlayContainerY,
273
+ overlayContainerYLayout,
274
+ overlayContainerReady,
275
+ dragContainerRef,
276
+ ]);
277
+ return (<react_native_reanimated_1.default.View ref={dragContainerRef} style={containerStyle} onLayout={handleLayout}>
278
+ {children}
279
+ </react_native_reanimated_1.default.View>);
280
+ });
61
281
  const ItemWrapper = react_1.default.memo(function ItemWrapper({ item, index, totalItemsRef, renderItem, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, }) {
62
282
  const isDragEnabled = dragEnabled && (canDrag ? canDrag(item, index) : true);
63
283
  return (<AnimatedFlashListItem_1.AnimatedFlashListItem item={item} index={index} totalItems={totalItemsRef.current ?? 0} isDragEnabled={isDragEnabled} renderItem={renderItem} onReorderByDelta={onReorderByDelta} onHapticFeedback={onHapticFeedback}/>);
64
284
  });
65
285
  function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtractor, canDrag, dragEnabled, onReorderByDelta, onHapticFeedback, itemHeight, ListFooterComponent, ListEmptyComponent, onEndReached, onEndReachedThreshold, onRefresh, refreshing, refreshTintColor, contentContainerStyle, drawDistance = 500, showsVerticalScrollIndicator = true, extraData, onCommitLayoutEffect, }) {
66
286
  // Get drag state context for scroll tracking
67
- const { scrollOffset, contentHeight, visibleHeight, listTopY, setListRef, totalItems, notifyLayoutCommit, } = (0, contexts_1.useDragState)();
68
- // Register FlashList ref with drag context for autoscroll
69
- (0, react_1.useEffect)(() => {
70
- // Cast to unknown to satisfy the generic constraint
71
- setListRef(flashListRef.current);
72
- return () => setListRef(null);
73
- }, [setListRef, flashListRef]);
287
+ const { scrollOffset, contentHeight, visibleHeight, listTopY, listLeftX, setListRef, totalItems, notifyLayoutCommit, isDragging, isDropping, itemIndexRegistry, } = (0, DragStateContext_1.useDragState)();
288
+ // v40.1: Disable scroll while dropping to prevent overlay drift
289
+ // FlashList needs scrollEnabled as a regular boolean, not a SharedValue
290
+ const [scrollEnabled, setScrollEnabled] = (0, react_1.useState)(true);
291
+ const setScrollEnabledSafe = (0, react_1.useCallback)((enabled) => {
292
+ setScrollEnabled(enabled);
293
+ }, []);
294
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => isDragging.value || isDropping.value, (draggingOrDropping, prevDraggingOrDropping) => {
295
+ "worklet";
296
+ if (draggingOrDropping !== prevDraggingOrDropping) {
297
+ (0, react_native_worklets_1.scheduleOnRN)(setScrollEnabledSafe, !draggingOrDropping);
298
+ }
299
+ }, [setScrollEnabledSafe]);
300
+ // Callback ref that notifies DragStateContext when FlashList is available.
301
+ // This fires immediately when FlashList sets the ref, unlike useEffect which
302
+ // would run after render with flashListRef.current potentially still null.
303
+ const handleFlashListRef = (0, react_1.useCallback)((instance) => {
304
+ // Update the ref object for other uses (scrollToOffset, prepareForLayoutAnimationRender, etc.)
305
+ flashListRef.current =
306
+ instance;
307
+ // Notify DragStateContext - this triggers native layout observation setup
308
+ setListRef(instance);
309
+ }, [flashListRef, setListRef]);
74
310
  // Sync totalItems SharedValue with data.length for worklet access
75
311
  (0, react_1.useEffect)(() => {
76
312
  totalItems.value = data.length;
77
313
  }, [data.length, totalItems]);
314
+ // Prune stale entries from itemIndexRegistry when data changes.
315
+ // Skip during active drag/drop to avoid corrupting indices mid-operation.
316
+ (0, react_1.useEffect)(() => {
317
+ if (isDragging.value || isDropping.value)
318
+ return;
319
+ const currentIds = new Set(data.map((item) => item.id));
320
+ const registry = itemIndexRegistry.value;
321
+ const staleIds = Object.keys(registry).filter((id) => !currentIds.has(id));
322
+ if (staleIds.length > 0) {
323
+ const pruned = { ...registry };
324
+ for (const id of staleIds) {
325
+ delete pruned[id];
326
+ }
327
+ itemIndexRegistry.value = pruned;
328
+ }
329
+ }, [data, itemIndexRegistry, isDragging, isDropping]);
78
330
  // Update scroll offset on scroll
79
331
  const handleScroll = (0, react_1.useCallback)((event) => {
80
332
  scrollOffset.value = event.nativeEvent.contentOffset.y;
@@ -89,14 +341,15 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
89
341
  // Get native scroll ref and validate it has measureInWindow before using
90
342
  const nativeRef = flashListRef.current?.getNativeScrollRef?.();
91
343
  if (nativeRef &&
92
- typeof nativeRef === 'object' &&
93
- 'measureInWindow' in nativeRef &&
94
- typeof nativeRef.measureInWindow === 'function') {
95
- nativeRef.measureInWindow((_x, y) => {
344
+ typeof nativeRef === "object" &&
345
+ "measureInWindow" in nativeRef &&
346
+ typeof nativeRef.measureInWindow === "function") {
347
+ nativeRef.measureInWindow((x, y) => {
96
348
  listTopY.value = y;
349
+ listLeftX.value = x;
97
350
  });
98
351
  }
99
- }, [visibleHeight, listTopY, flashListRef]);
352
+ }, [visibleHeight, listTopY, listLeftX, flashListRef]);
100
353
  const handleCommitLayoutEffect = (0, react_1.useCallback)(() => {
101
354
  notifyLayoutCommit();
102
355
  onCommitLayoutEffect?.();
@@ -111,14 +364,14 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
111
364
  onHapticFeedback,
112
365
  ]);
113
366
  // getItemType for FlashList recycling optimization
114
- const getItemType = (0, react_1.useCallback)(() => 'animated-item', []);
367
+ const getItemType = (0, react_1.useCallback)(() => "animated-item", []);
115
368
  // Override item layout for consistent drag calculations
116
369
  // Note: We cast the layout to include size for drag calculations
117
370
  const overrideItemLayout = (0, react_1.useCallback)((layout, _item, _index) => {
118
371
  // FlashList v2 uses this for span, but we extend for size in drag calculations
119
372
  layout.size = itemHeight;
120
373
  }, [itemHeight]);
121
- return (<flash_list_1.FlashList ref={flashListRef} data={data} renderItem={flashListRenderItem} keyExtractor={keyExtractor} getItemType={getItemType} overrideItemLayout={overrideItemLayout} onScroll={handleScroll} onContentSizeChange={handleContentSizeChange} onLayout={handleLayout} scrollEventThrottle={16} drawDistance={drawDistance} maintainVisibleContentPosition={{ disabled: true }} showsVerticalScrollIndicator={showsVerticalScrollIndicator} onCommitLayoutEffect={handleCommitLayoutEffect} extraData={extraData} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} ListEmptyComponent={ListEmptyComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} refreshControl={onRefresh ? (<react_native_1.RefreshControl refreshing={refreshing ?? false} onRefresh={onRefresh} tintColor={refreshTintColor} colors={refreshTintColor ? [refreshTintColor] : undefined}/>) : undefined}/>);
374
+ return (<flash_list_1.FlashList ref={handleFlashListRef} data={data} renderItem={flashListRenderItem} keyExtractor={keyExtractor} getItemType={getItemType} overrideItemLayout={overrideItemLayout} onScroll={handleScroll} onContentSizeChange={handleContentSizeChange} onLayout={handleLayout} scrollEventThrottle={16} drawDistance={drawDistance} maintainVisibleContentPosition={{ disabled: true }} showsVerticalScrollIndicator={showsVerticalScrollIndicator} onCommitLayoutEffect={handleCommitLayoutEffect} extraData={extraData} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} ListEmptyComponent={ListEmptyComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} scrollEnabled={scrollEnabled} refreshControl={onRefresh ? (<react_native_1.RefreshControl refreshing={refreshing ?? false} onRefresh={onRefresh} tintColor={refreshTintColor} colors={refreshTintColor ? [refreshTintColor] : undefined}/>) : undefined}/>);
122
375
  }
123
376
  /**
124
377
  * AnimatedFlashList - High-performance animated list with drag-to-reorder
@@ -150,15 +403,15 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
150
403
  * ```
151
404
  */
152
405
  function AnimatedFlashListInner(props, ref) {
153
- const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, ListEmptyComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, estimatedItemSize, ...flashListProps } = props;
406
+ const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, ListEmptyComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, ...flashListProps } = props;
154
407
  const { extraData: userExtraData, onCommitLayoutEffect: userOnCommitLayoutEffect, ...restFlashListProps } = flashListProps;
155
408
  // Merge config with defaults
156
- // Use estimatedItemSize as default itemHeight if not explicitly configured
409
+ // Note: estimatedItemSize was removed in FlashList v2
410
+ // Users should configure itemHeight via config.drag.itemHeight
157
411
  const dragConfig = (0, react_1.useMemo)(() => ({
158
- ...constants_1.DEFAULT_DRAG_CONFIG,
412
+ ...drag_1.DEFAULT_DRAG_CONFIG,
159
413
  ...config?.drag,
160
- itemHeight: config?.drag?.itemHeight ?? estimatedItemSize ?? constants_1.DEFAULT_DRAG_CONFIG.itemHeight,
161
- }), [config?.drag, estimatedItemSize]);
414
+ }), [config?.drag]);
162
415
  // Ref to FlashList
163
416
  const flashListRef = (0, react_1.useRef)(null);
164
417
  // Expose methods to parent via ref
@@ -184,11 +437,12 @@ function AnimatedFlashListInner(props, ref) {
184
437
  setLayoutVersion((version) => version + 1);
185
438
  }, []);
186
439
  // Handle reorder by index delta - converts to various callback formats
440
+ // NOTE: No LayoutAnimation needed - layout commit compensation handles visual transitions
187
441
  const handleReorderByDelta = (0, react_1.useCallback)((itemId, indexDelta) => {
188
442
  if (indexDelta === 0)
189
443
  return;
190
444
  const currentItems = dataRef.current;
191
- const currentIndex = currentItems.findIndex(item => item.id === itemId);
445
+ const currentIndex = currentItems.findIndex((item) => item.id === itemId);
192
446
  if (currentIndex === -1)
193
447
  return;
194
448
  const newIndex = Math.max(0, Math.min(currentItems.length - 1, currentIndex + indexDelta));
@@ -206,12 +460,12 @@ function AnimatedFlashListInner(props, ref) {
206
460
  afterItemId = currentItems[newIndex]?.id ?? null;
207
461
  beforeItemId =
208
462
  newIndex < currentItems.length - 1
209
- ? currentItems[newIndex + 1]?.id ?? null
463
+ ? (currentItems[newIndex + 1]?.id ?? null)
210
464
  : null;
211
465
  }
212
466
  else {
213
467
  afterItemId =
214
- newIndex > 0 ? currentItems[newIndex - 1]?.id ?? null : null;
468
+ newIndex > 0 ? (currentItems[newIndex - 1]?.id ?? null) : null;
215
469
  beforeItemId = currentItems[newIndex]?.id ?? null;
216
470
  }
217
471
  onReorderByNeighbors(itemId, afterItemId, beforeItemId);
@@ -256,13 +510,17 @@ function AnimatedFlashListInner(props, ref) {
256
510
  })} keyExtractor={keyExtractor} contentContainerStyle={contentContainerStyle} ListFooterComponent={ListFooterComponent ?? undefined} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold}/>
257
511
  </react_native_1.View>);
258
512
  return (<AnimationErrorBoundary fallback={plainFlashListFallback}>
259
- <contexts_1.ListAnimationProvider>
260
- <contexts_1.DragStateProvider config={dragConfig}>
261
- <react_native_1.View style={containerStyle}>
262
- <InnerFlashList {...restFlashListProps} data={data} totalItemsRef={totalItemsRef} flashListRef={flashListRef} renderItem={renderItem} keyExtractor={keyExtractor} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorder || onReorderByNeighbors ? handleReorderByDelta : undefined} onHapticFeedback={onHapticFeedback} itemHeight={dragConfig.itemHeight} ListFooterComponent={ListFooterComponent} ListEmptyComponent={ListEmptyComponent} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} onRefresh={onRefresh} refreshing={refreshing} contentContainerStyle={contentContainerStyle} extraData={combinedExtraData} onCommitLayoutEffect={userOnCommitLayoutEffect}/>
263
- </react_native_1.View>
264
- </contexts_1.DragStateProvider>
265
- </contexts_1.ListAnimationProvider>
513
+ <ListAnimationContext_1.ListAnimationProvider>
514
+ <DragStateContext_1.DragStateProvider config={dragConfig}>
515
+ <DragContainer>
516
+ <InnerFlashList {...restFlashListProps} data={data} totalItemsRef={totalItemsRef} flashListRef={flashListRef} renderItem={renderItem} keyExtractor={keyExtractor} canDrag={canDrag} dragEnabled={dragEnabled} onReorderByDelta={onReorder || onReorderByNeighbors
517
+ ? handleReorderByDelta
518
+ : undefined} onHapticFeedback={onHapticFeedback} itemHeight={dragConfig.itemHeight} ListFooterComponent={ListFooterComponent} ListEmptyComponent={ListEmptyComponent} onEndReached={onEndReached} onEndReachedThreshold={onEndReachedThreshold} onRefresh={onRefresh} refreshing={refreshing} contentContainerStyle={contentContainerStyle} extraData={combinedExtraData} onCommitLayoutEffect={userOnCommitLayoutEffect}/>
519
+ <DragOverlay data={data} totalItemsRef={totalItemsRef} renderItem={renderItem}/>
520
+ <ShiftedItemOverlays data={data} totalItemsRef={totalItemsRef} renderItem={renderItem}/>
521
+ </DragContainer>
522
+ </DragStateContext_1.DragStateProvider>
523
+ </ListAnimationContext_1.ListAnimationProvider>
266
524
  </AnimationErrorBoundary>);
267
525
  }
268
526
  const containerStyle = {
@@ -1 +1 @@
1
- {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAaxF,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAMjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAqO3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
1
+ {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAmE,MAAM,OAAO,CAAC;AAWxF,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAwP3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
@@ -36,12 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.AnimatedFlashListItem = void 0;
37
37
  const react_1 = __importStar(require("react"));
38
38
  const react_native_reanimated_1 = __importStar(require("react-native-reanimated"));
39
- const hooks_1 = require("./hooks");
40
- const contexts_1 = require("./contexts");
41
- const constants_1 = require("./constants");
42
- // Debug: Track component instances to detect recycling.
43
- // Note: IDs are not stable across hot reloads (counter persists at module scope).
44
- let instanceCounter = 0;
39
+ const useDragGesture_1 = require("./hooks/drag/useDragGesture");
40
+ const useDragShift_1 = require("./hooks/drag/useDragShift");
41
+ const useDragAnimatedStyle_1 = require("./hooks/drag/useDragAnimatedStyle");
42
+ const useDropCompensation_1 = require("./hooks/drag/useDropCompensation");
43
+ const useListExitAnimation_1 = require("./hooks/animations/useListExitAnimation");
44
+ const useListEntryAnimation_1 = require("./hooks/animations/useListEntryAnimation");
45
+ const ListAnimationContext_1 = require("./contexts/ListAnimationContext");
46
+ const DragStateContext_1 = require("./contexts/DragStateContext");
45
47
  /**
46
48
  * Internal item wrapper that provides all animation functionality.
47
49
  *
@@ -54,41 +56,17 @@ let instanceCounter = 0;
54
56
  * @internal
55
57
  */
56
58
  function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, renderItem, onReorderByDelta, onHapticFeedback, }) {
57
- // Debug: Track this component instance (only incremented in DEV)
58
- const instanceIdRef = (0, react_1.useRef)(null);
59
- if (__DEV__ && instanceIdRef.current === null) {
60
- instanceIdRef.current = ++instanceCounter;
61
- }
62
- const instanceId = instanceIdRef.current;
63
- // Track previous itemId to detect recycling
64
- const prevItemIdRef = (0, react_1.useRef)(item.id);
65
- // Log every render with instance tracking (verbose only - fires per render per item)
66
- if (__DEV__ && constants_1.VERBOSE_DRAG_DEBUG) {
67
- const isRecycled = prevItemIdRef.current !== item.id;
68
- console.log(`[FlashListItem:inst${instanceId}] RENDER: itemId=${item.id}, index=${index}` +
69
- (isRecycled ? ` (RECYCLED from ${prevItemIdRef.current})` : ''));
70
- prevItemIdRef.current = item.id;
71
- }
72
- // Track mount/unmount
73
- (0, react_1.useEffect)(() => {
74
- if (__DEV__) {
75
- console.log(`[FlashListItem:inst${instanceId}] MOUNT: itemId=${item.id}, index=${index}`);
76
- }
77
- return () => {
78
- if (__DEV__) {
79
- console.log(`[FlashListItem:inst${instanceId}] UNMOUNT: was itemId=${item.id}`);
80
- }
81
- };
82
- // eslint-disable-next-line react-hooks/exhaustive-deps
83
- }, []); // Empty deps - only track actual mount/unmount
84
59
  // Create shared value for index (for UI-thread access in animations)
85
60
  // This allows worklets to read the current index without closure stale capture
86
61
  const indexShared = (0, react_native_reanimated_1.useSharedValue)(index);
87
- const layoutSignal = (0, react_native_reanimated_1.useSharedValue)(0);
88
62
  // Register this item's index in the central registry
89
63
  // This handles FlashList's recycling behavior where components may not re-render
90
64
  // after data changes, leaving their index props stale
91
- const { updateItemIndex, isDragging: globalIsDragging, isDropping } = (0, contexts_1.useDragState)();
65
+ const { updateItemIndex, isDragging: globalIsDragging, isDropping, draggedItemId,
66
+ // v40.7: For shifted item overlay hiding
67
+ shiftedOverlaysActive, shiftedOverlays, shiftedOverlaysRendered, // v41.4: Wait for React render before hiding
68
+ // Crossfade support
69
+ dragOverlayOpacity, overlayFrozenItemId, overlayRenderedOnUI, } = (0, DragStateContext_1.useDragState)();
92
70
  // Sync index to SharedValue on every render, but skip during active drag/drop
93
71
  // This prevents stale React props from corrupting the index during the drop phase
94
72
  // when FlashList re-renders with new data order
@@ -112,9 +90,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
112
90
  (0, react_1.useEffect)(() => {
113
91
  if (globalIsDragging.value || isDropping.value || indexShared.value === index)
114
92
  return;
115
- if (__DEV__ && constants_1.VERBOSE_DRAG_DEBUG) {
116
- console.log(`[FlashListItem:${item.id}] Secondary sync: indexShared ${indexShared.value} → ${index}`);
117
- }
118
93
  indexShared.value = index;
119
94
  });
120
95
  // Update item index in useLayoutEffect to avoid side effects during render
@@ -128,11 +103,11 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
128
103
  // Track measured height for layout compensation
129
104
  const measuredHeightRef = (0, react_1.useRef)(0);
130
105
  // List animation context for subscription-triggered animations and layout compensation
131
- const animationContext = (0, contexts_1.useListAnimationOptional)();
106
+ const animationContext = (0, ListAnimationContext_1.useListAnimationOptional)();
132
107
  // === DRAG HOOKS ===
133
108
  // Pan gesture for drag-to-reorder
134
109
  // Note: totalItems is now accessed via SharedValue from DragStateContext
135
- const { panGesture, isDragging, translateY } = (0, hooks_1.useDragGesture)({
110
+ const { panGesture, isDragging, translateY } = (0, useDragGesture_1.useDragGesture)({
136
111
  itemId: item.id,
137
112
  index,
138
113
  enabled: isDragEnabled,
@@ -142,11 +117,46 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
142
117
  onHapticFeedback,
143
118
  });
144
119
  // Shift animation for non-dragged items
145
- const { shiftY } = (0, hooks_1.useDragShift)({ itemId: item.id, index, containerRef, layoutSignal });
120
+ const { shiftY } = (0, useDragShift_1.useDragShift)({ itemId: item.id, index });
146
121
  // Handle index changes after cache updates (drop compensation)
147
- (0, hooks_1.useDropCompensation)({ itemId: item.id, index, translateY });
122
+ (0, useDropCompensation_1.useDropCompensation)({ itemId: item.id, index, translateY });
148
123
  // Animated style for drag transforms
149
- const { dragAnimatedStyle } = (0, hooks_1.useDragAnimatedStyle)(item.id, isDragging, translateY, shiftY, containerRef);
124
+ const { dragAnimatedStyle } = (0, useDragAnimatedStyle_1.useDragAnimatedStyle)(item.id, isDragging, translateY, shiftY, containerRef);
125
+ const hideOriginalStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
126
+ // Crossfade visibility: use inverse of overlay opacity for smooth transition
127
+ // This ensures the original and overlay opacity always sum to 1, preventing
128
+ // any frame where both are invisible (the "vanish" bug).
129
+ const isThisDragged = draggedItemId.value === item.id;
130
+ const isThisFrozenOverlay = overlayFrozenItemId.value === item.id;
131
+ // Crossfade path: applies during drag/drop OR while frozen overlay fades out
132
+ if (isThisDragged || isThisFrozenOverlay) {
133
+ // During drag/drop or while overlay is fading out, crossfade with overlay
134
+ if (globalIsDragging.value || isDropping.value || overlayFrozenItemId.value === item.id) {
135
+ // FIX: Don't start fading out until overlay has rendered ON THE UI THREAD.
136
+ // overlayRenderedOnUI is set from useAnimatedStyle (UI thread) in DragOverlay,
137
+ // unlike overlayVisible which is set from useEffect (JS thread) causing races.
138
+ // Without this guard, the original fades out before overlay appears = vanish.
139
+ if (!overlayRenderedOnUI.value) {
140
+ return { opacity: 1 }; // Stay fully visible until overlay renders
141
+ }
142
+ // Always use crossfade - when overlay is at opacity X,
143
+ // the original is at opacity (1-X), so they always sum to 1.
144
+ return { opacity: 1 - dragOverlayOpacity.value };
145
+ }
146
+ }
147
+ // v41.4: Hide shifted items during drop ONLY after overlays have rendered.
148
+ // Previously (v41.2) we hid immediately when shiftedOverlaysActive=true,
149
+ // but overlays take ~2-5ms to render (React async), causing a blank flash.
150
+ // The shift values are FROZEN during drop (v41.1), so originals stay correctly
151
+ // positioned until overlays are ready.
152
+ if (isDropping.value &&
153
+ shiftedOverlaysActive.value &&
154
+ shiftedOverlaysRendered.value && // Wait for React render
155
+ shiftedOverlays.value[item.id] !== undefined) {
156
+ return { opacity: 0 };
157
+ }
158
+ return { opacity: 1 };
159
+ });
150
160
  // === ANIMATION HOOKS ===
151
161
  // Callbacks for layout compensation (register/unregister exiting items)
152
162
  const onExitStart = (0, react_1.useCallback)((exitIndex, height) => {
@@ -156,14 +166,14 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
156
166
  animationContext?.unregisterExitingItem(item.id);
157
167
  }, [animationContext, item.id]);
158
168
  // Exit animation for smooth slide-out (with layout compensation callbacks)
159
- const { exitAnimatedStyle, triggerExit, resetAnimation } = (0, hooks_1.useListExitAnimation)(item.id, {
169
+ const { exitAnimatedStyle, triggerExit, resetAnimation } = (0, useListExitAnimation_1.useListExitAnimation)(item.id, {
160
170
  index,
161
171
  measuredHeight: measuredHeightRef.current,
162
172
  onExitStart,
163
173
  onExitComplete,
164
174
  });
165
175
  // Entry animation for items appearing
166
- const { entryAnimatedStyle } = (0, hooks_1.useListEntryAnimation)(item.id);
176
+ const { entryAnimatedStyle } = (0, useListEntryAnimation_1.useListEntryAnimation)(item.id);
167
177
  // Register exit animation trigger (O(1) direct calls from subscriptions)
168
178
  (0, react_1.useLayoutEffect)(() => {
169
179
  if (!animationContext)
@@ -173,9 +183,9 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
173
183
  }, [item.id, triggerExit, animationContext]);
174
184
  // Track measured height for layout compensation
175
185
  const handleLayout = (0, react_1.useCallback)((event) => {
176
- measuredHeightRef.current = event.nativeEvent.layout.height;
177
- layoutSignal.value += 1;
178
- }, [layoutSignal]);
186
+ const { height } = event.nativeEvent.layout;
187
+ measuredHeightRef.current = height;
188
+ }, []);
179
189
  // Create drag handle props
180
190
  const dragHandleProps = (0, react_1.useMemo)(() => isDragEnabled
181
191
  ? {
@@ -215,6 +225,7 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
215
225
  return (<react_native_reanimated_1.default.View ref={containerRef} onLayout={handleLayout} style={[
216
226
  exitAnimatedStyle,
217
227
  entryAnimatedStyle,
228
+ hideOriginalStyle,
218
229
  isDragEnabled && dragAnimatedStyle,
219
230
  ]}>
220
231
  {renderedItem}