@souscheflabs/reanimated-flashlist 0.2.6 → 0.2.7

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.
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  A high-performance animated FlashList with drag-to-reorder and entry/exit animations for React Native.
4
4
 
5
+ <p align="center">
6
+ <img src="assets/example-screenshot.png" width="300" alt="Example screenshot showing drag-to-reorder list" />
7
+ </p>
8
+
5
9
  ## Features
6
10
 
7
11
  - **Drag-to-reorder** with smooth animations and autoscroll
Binary file
@@ -1 +1 @@
1
- {"version":3,"file":"AnimatedFlashList.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AA6XjB,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,KAON,MAAM,OAAO,CAAC;AAaf,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,oBAAoB,EAErB,MAAM,SAAS,CAAC;AAsYjB,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"}
@@ -120,12 +120,20 @@ function InnerFlashList({ data, totalItemsRef, flashListRef, renderItem, keyExtr
120
120
  * ```
121
121
  */
122
122
  function AnimatedFlashListInner(props, ref) {
123
- const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, ...flashListProps } = props;
123
+ const { data, keyExtractor, renderItem, dragEnabled = false, onReorder, onReorderByNeighbors, canDrag, onHapticFeedback, config, onPrepareLayoutAnimation, ListFooterComponent, onRefresh, refreshing = false, onEndReached, onEndReachedThreshold = 0.5, contentContainerStyle, estimatedItemSize, ...flashListProps } = props;
124
124
  // Merge config with defaults
125
- const dragConfig = (0, react_1.useMemo)(() => ({
126
- ...constants_1.DEFAULT_DRAG_CONFIG,
127
- ...config?.drag,
128
- }), [config?.drag]);
125
+ // Use estimatedItemSize as default itemHeight if not explicitly configured
126
+ const dragConfig = (0, react_1.useMemo)(() => {
127
+ const mergedConfig = {
128
+ ...constants_1.DEFAULT_DRAG_CONFIG,
129
+ ...config?.drag,
130
+ itemHeight: config?.drag?.itemHeight ?? estimatedItemSize ?? constants_1.DEFAULT_DRAG_CONFIG.itemHeight,
131
+ };
132
+ if (__DEV__) {
133
+ console.log(`[AnimatedFlashList] dragConfig.itemHeight=${mergedConfig.itemHeight}, estimatedItemSize=${estimatedItemSize}, config.drag?.itemHeight=${config?.drag?.itemHeight}`);
134
+ }
135
+ return mergedConfig;
136
+ }, [config?.drag, estimatedItemSize]);
129
137
  // Ref to FlashList
130
138
  const flashListRef = (0, react_1.useRef)(null);
131
139
  // Expose methods to parent via ref
@@ -1 +1 @@
1
- {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,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,CAkJ3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
1
+ {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,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,CA+J3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
@@ -50,6 +50,17 @@ const contexts_1 = require("./contexts");
50
50
  * @internal
51
51
  */
52
52
  function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, renderItem, onReorderByDelta, onHapticFeedback, }) {
53
+ // DEBUG: Log renders
54
+ if (__DEV__) {
55
+ console.log(`[AnimatedFlashListItem] RENDER ${item.id} at index=${index}`);
56
+ }
57
+ // Register this item's index in the central registry
58
+ // This handles FlashList's recycling behavior where components may not re-render
59
+ // after data changes, leaving their index props stale
60
+ const { updateItemIndex } = (0, contexts_1.useDragState)();
61
+ // Call updateItemIndex synchronously during render to ensure the registry
62
+ // is always up-to-date when any item renders
63
+ updateItemIndex(item.id, index);
53
64
  // Animated ref for measuring item height on drag start
54
65
  const containerRef = (0, react_native_reanimated_1.useAnimatedRef)();
55
66
  // Track measured height for layout compensation
@@ -37,6 +37,8 @@ export interface DragStateContextValue {
37
37
  measuredItemHeight: SharedValue<number>;
38
38
  /** Flag to freeze shift values during drop transition */
39
39
  isDropping: SharedValue<boolean>;
40
+ /** Counter incremented on each drag start, used to force shift recalculation */
41
+ dragSequence: SharedValue<number>;
40
42
  /** Register the FlashList ref for autoscroll operations */
41
43
  setListRef: (ref: FlashListRef<unknown> | null) => void;
42
44
  /** Scroll the list to a specific offset (for autoscroll during drag) */
@@ -45,6 +47,27 @@ export interface DragStateContextValue {
45
47
  resetDragState: () => void;
46
48
  /** Current drag configuration */
47
49
  config: DragConfig;
50
+ /**
51
+ * Index registry for tracking itemId -> index mapping.
52
+ * Updated on every item render to handle FlashList recycling.
53
+ * Stored on UI thread as SharedValue for worklet access.
54
+ */
55
+ itemIndexRegistry: SharedValue<Record<string, number>>;
56
+ /** Update an item's index in the registry (call from JS thread) */
57
+ updateItemIndex: (itemId: string, index: number) => void;
58
+ /** Get an item's index from the registry */
59
+ getItemIndex: (itemId: string) => number | undefined;
60
+ /**
61
+ * Snapshot of itemIndexRegistry taken at drag start.
62
+ * Used during drag to ensure consistent index lookup regardless of
63
+ * React re-renders or FlashList recycling.
64
+ */
65
+ dragStartIndexSnapshot: SharedValue<Record<string, number>>;
66
+ /**
67
+ * Capture a snapshot of the current itemIndexRegistry for use during drag.
68
+ * Call this at the start of a drag operation.
69
+ */
70
+ snapshotRegistryForDrag: () => void;
48
71
  }
49
72
  /**
50
73
  * Hook to access shared drag state from context.
@@ -1 +1 @@
1
- {"version":3,"file":"DragStateContext.d.ts","sourceRoot":"","sources":["../../src/contexts/DragStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,iFAAiF;IACjF,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,sEAAsE;IACtE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,+DAA+D;IAC/D,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,uFAAuF;IACvF,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,yEAAyE;IACzE,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,qDAAqD;IACrD,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,mFAAmF;IACnF,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,4EAA4E;IAC5E,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,2DAA2D;IAC3D,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,wEAAwE;IACxE,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,sDAAsD;IACtD,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,iCAAiC;IACjC,MAAM,EAAE,UAAU,CAAC;CACpB;AAID;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAO,qBAM/B,CAAC;AAEF,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAmDD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CAsG9D,CAAC"}
1
+ {"version":3,"file":"DragStateContext.d.ts","sourceRoot":"","sources":["../../src/contexts/DragStateContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AACf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAG3C;;;;;;;;;GASG;AACH,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,oEAAoE;IACpE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,oFAAoF;IACpF,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,iFAAiF;IACjF,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,sEAAsE;IACtE,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,+DAA+D;IAC/D,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,uFAAuF;IACvF,qBAAqB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC3C,yEAAyE;IACzE,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,qDAAqD;IACrD,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACnC,mFAAmF;IACnF,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC9B,4EAA4E;IAC5E,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACxC,yDAAyD;IACzD,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,gFAAgF;IAChF,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,2DAA2D;IAC3D,UAAU,EAAE,CAAC,GAAG,EAAE,YAAY,CAAC,OAAO,CAAC,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,wEAAwE;IACxE,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC7D,sDAAsD;IACtD,cAAc,EAAE,MAAM,IAAI,CAAC;IAC3B,iCAAiC;IACjC,MAAM,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mEAAmE;IACnE,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,4CAA4C;IAC5C,YAAY,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;IACrD;;;;OAIG;IACH,sBAAsB,EAAE,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC5D;;;OAGG;IACH,uBAAuB,EAAE,MAAM,IAAI,CAAC;CACrC;AAID;;;GAGG;AACH,eAAO,MAAM,YAAY,QAAO,qBAM/B,CAAC;AAEF,UAAU,sBAAsB;IAC9B,QAAQ,EAAE,SAAS,CAAC;IACpB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;CAC9B;AAmDD,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA2J9D,CAAC"}
@@ -106,6 +106,14 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
106
106
  const measuredItemHeight = (0, react_native_reanimated_1.useSharedValue)(0);
107
107
  // Flag to freeze shift values during drop transition
108
108
  const isDropping = (0, react_native_reanimated_1.useSharedValue)(false);
109
+ // Counter incremented on each drag start, used to force shift recalculation
110
+ const dragSequence = (0, react_native_reanimated_1.useSharedValue)(0);
111
+ // Index registry for tracking itemId -> index mapping
112
+ // This handles FlashList's recycling behavior where components may not re-render
113
+ // after data changes, leaving their index props stale
114
+ const itemIndexRegistry = (0, react_native_reanimated_1.useSharedValue)({});
115
+ // Snapshot of registry taken at drag start for consistent index lookup during drag
116
+ const dragStartIndexSnapshot = (0, react_native_reanimated_1.useSharedValue)({});
109
117
  // Ref to FlashList for autoscroll operations
110
118
  const listRef = (0, react_1.useRef)(null);
111
119
  // Register the FlashList ref
@@ -116,6 +124,27 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
116
124
  const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
117
125
  listRef.current?.scrollToOffset({ offset, animated });
118
126
  }, []);
127
+ // Update an item's index in the registry
128
+ // Called on every item render to keep indices fresh
129
+ const updateItemIndex = (0, react_1.useCallback)((itemId, index) => {
130
+ // Only update if the value has changed to avoid unnecessary SharedValue updates
131
+ if (itemIndexRegistry.value[itemId] !== index) {
132
+ itemIndexRegistry.value = {
133
+ ...itemIndexRegistry.value,
134
+ [itemId]: index,
135
+ };
136
+ }
137
+ }, [itemIndexRegistry]);
138
+ // Get an item's index from the registry
139
+ const getItemIndex = (0, react_1.useCallback)((itemId) => {
140
+ return itemIndexRegistry.value[itemId];
141
+ }, [itemIndexRegistry]);
142
+ // Capture a snapshot of the registry at drag start
143
+ // This ensures consistent index lookup during the entire drag operation,
144
+ // regardless of React re-renders or FlashList recycling
145
+ const snapshotRegistryForDrag = (0, react_1.useCallback)(() => {
146
+ dragStartIndexSnapshot.value = { ...itemIndexRegistry.value };
147
+ }, [dragStartIndexSnapshot, itemIndexRegistry]);
119
148
  // Reset drag state after drop
120
149
  const resetDragState = (0, react_1.useCallback)(() => {
121
150
  isDragging.value = false;
@@ -142,10 +171,16 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
142
171
  listTopY,
143
172
  measuredItemHeight,
144
173
  isDropping,
174
+ dragSequence,
145
175
  setListRef,
146
176
  scrollToOffset,
147
177
  resetDragState,
148
178
  config,
179
+ itemIndexRegistry,
180
+ updateItemIndex,
181
+ getItemIndex,
182
+ dragStartIndexSnapshot,
183
+ snapshotRegistryForDrag,
149
184
  }), [
150
185
  isDragging,
151
186
  draggedIndex,
@@ -159,10 +194,16 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
159
194
  listTopY,
160
195
  measuredItemHeight,
161
196
  isDropping,
197
+ dragSequence,
162
198
  setListRef,
163
199
  scrollToOffset,
164
200
  resetDragState,
165
201
  config,
202
+ itemIndexRegistry,
203
+ updateItemIndex,
204
+ getItemIndex,
205
+ dragStartIndexSnapshot,
206
+ snapshotRegistryForDrag,
166
207
  ]);
167
208
  return (<DragStateContext.Provider value={value}>
168
209
  {children}
@@ -1 +1 @@
1
- {"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,EAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAC1B,0BAA0B,CAmE5B"}
1
+ {"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,EAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAC1B,0BAA0B,CAsF5B"}
@@ -64,20 +64,37 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
64
64
  // Animated style for drag offset with scale and shadow
65
65
  const dragAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
66
66
  const isThisItemDragged = draggedItemId.value === itemId;
67
- // Keep elevated if: actively dragging OR has offset (animating back)
68
- const shouldBeElevated = isDragging.value ||
69
- Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
67
+ const hasSignificantTranslateY = Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
70
68
  // Calculate scroll delta for position compensation during autoscroll
71
69
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
72
- // Use drag translateY + scroll compensation for dragged item, shiftY for others
73
- const yOffset = isThisItemDragged
70
+ // Determine when to use translateY for positioning:
71
+ // 1. Actively being dragged (isThisItemDragged && isDragging)
72
+ // 2. Animating/settling after drop (hasSignificantTranslateY)
73
+ //
74
+ // Case 2 handles the FALLBACK scenario where FlashList doesn't re-render
75
+ // after reorder. The item uses translateY to stay at its new position
76
+ // until FlashList eventually re-renders and useDropCompensation runs.
77
+ const shouldUseDragOffset = (isThisItemDragged && isDragging.value) || hasSignificantTranslateY;
78
+ // Use drag translateY + scroll compensation for dragged/settling item,
79
+ // shiftY for others or for items at rest
80
+ const yOffset = shouldUseDragOffset
74
81
  ? translateY.value + scrollDelta
75
82
  : shiftY.value;
83
+ // Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
84
+ // Not when just settling at new position after drop
85
+ const isActivelyDragging = isThisItemDragged && isDragging.value;
86
+ // Keep elevated zIndex when actively dragging OR when settling at new position
87
+ // This prevents the dropped item from appearing behind other items
88
+ // Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
89
+ // after FALLBACK completes, draggedItemId is reset to '' but translateY remains
90
+ const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
76
91
  return {
77
92
  transform: [
78
93
  { translateY: yOffset },
79
- { scale: isThisItemDragged ? draggedScale.value : 1 },
94
+ // Only apply drag scale when actively dragging
95
+ { scale: isActivelyDragging ? draggedScale.value : 1 },
80
96
  ],
97
+ // Elevate zIndex when dragging or settling at new position
81
98
  zIndex: shouldBeElevated ? constants_1.DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
82
99
  // Use animated shadow opacity for smooth transitions
83
100
  shadowOpacity: animatedShadowOpacity.value,
@@ -1 +1 @@
1
- {"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,SAAS,EAAE,uBAAuB,GACjC,oBAAoB,CAqPtB"}
1
+ {"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EACV,oBAAoB,EACpB,uBAAuB,EACvB,oBAAoB,EACrB,MAAM,aAAa,CAAC;AAErB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,oBAAoB,EAC5B,SAAS,EAAE,uBAAuB,GACjC,oBAAoB,CA8WtB"}
@@ -5,6 +5,7 @@ const react_1 = require("react");
5
5
  const react_native_reanimated_1 = require("react-native-reanimated");
6
6
  const react_native_gesture_handler_1 = require("react-native-gesture-handler");
7
7
  const react_native_worklets_1 = require("react-native-worklets");
8
+ const flash_list_1 = require("@shopify/flash-list");
8
9
  const DragStateContext_1 = require("../../contexts/DragStateContext");
9
10
  /**
10
11
  * Hook that encapsulates all drag gesture logic.
@@ -40,28 +41,58 @@ function useDragGesture(config, callbacks) {
40
41
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
41
42
  const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
42
43
  // Global drag state for coordinating animations across all items
43
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
44
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, itemIndexRegistry, snapshotRegistryForDrag, dragSequence, } = (0, DragStateContext_1.useDragState)();
44
45
  // Performance optimization: Track last significant Y position and scroll time
45
46
  // to avoid updating on every pixel movement
46
47
  const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
47
48
  const lastScrollTime = (0, react_native_reanimated_1.useSharedValue)(0);
49
+ // Track current index in SharedValue for worklet access
50
+ // This is critical because FlashList may not re-render items after reorder,
51
+ // leaving the closure-captured `index` stale
52
+ const currentIndex = (0, react_native_reanimated_1.useSharedValue)(index);
53
+ // Use FlashList's useRecyclingState to detect when view is recycled
54
+ // This tracks the previous itemId so we can reset state when the view
55
+ // is reused for a different item during autoscroll
56
+ const [prevItemId, setPrevItemId] = (0, flash_list_1.useRecyclingState)(itemId, [itemId]);
57
+ // Reset translateY and currentIndex when view is recycled
58
+ // Using useLayoutEffect to run before paint and prevent visual glitches
59
+ (0, react_1.useLayoutEffect)(() => {
60
+ if (prevItemId !== itemId) {
61
+ // View was recycled - reset local drag state to prevent stale values
62
+ // from the previous item affecting this item's position
63
+ (0, react_native_reanimated_1.cancelAnimation)(translateY);
64
+ translateY.value = 0;
65
+ isDragging.value = false;
66
+ currentIndex.value = index;
67
+ setPrevItemId(itemId);
68
+ }
69
+ }, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
70
+ // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
71
+ (0, react_1.useEffect)(() => {
72
+ currentIndex.value = index;
73
+ }, [index, currentIndex]);
48
74
  // Store current values in refs for stable gesture callbacks
49
75
  const dragContextRef = (0, react_1.useRef)({
50
- index,
51
76
  totalItems,
52
77
  itemId,
53
78
  onReorderByDelta,
54
79
  });
55
80
  // Keep ref in sync with current values
56
81
  dragContextRef.current = {
57
- index,
58
82
  totalItems,
59
83
  itemId,
60
84
  onReorderByDelta,
61
85
  };
62
86
  // Calculate new position and call reorder callback
63
87
  const handleDragEnd = (0, react_1.useCallback)((finalTranslateY) => {
64
- const { index: currentIndex, totalItems: total, itemId: currentItemId, onReorderByDelta: reorder, } = dragContextRef.current;
88
+ const { totalItems: total, itemId: currentItemId, onReorderByDelta: reorder, } = dragContextRef.current;
89
+ // Use draggedIndex from context, which was set in onStart using the registry
90
+ // This handles FlashList's recycling where closure-captured index may be stale
91
+ const currentIndex = draggedIndex.value;
92
+ if (currentIndex === -1) {
93
+ // Drag was cancelled or reset
94
+ return;
95
+ }
65
96
  // Use configured itemHeight for consistent position calculations
66
97
  const itemHeight = dragConfig.itemHeight;
67
98
  // Calculate how many positions to move based on drag offset
@@ -70,14 +101,67 @@ function useDragGesture(config, callbacks) {
70
101
  const newIndex = Math.max(0, Math.min(total - 1, currentIndex + positionDelta));
71
102
  const positionChanged = reorder && positionDelta !== 0 && newIndex !== currentIndex;
72
103
  if (positionChanged) {
73
- // Position changes - call reorder, let useDropCompensation handle animation
104
+ // Position changes - call reorder and animate back
105
+ // useDropCompensation handles state reset if re-renders happen,
106
+ // but we also animate here as a fallback since FlashList's virtualization
107
+ // may not trigger re-renders when data changes.
74
108
  isDropping.value = true;
75
109
  onHapticFeedback?.('medium');
76
110
  reorder(currentItemId, positionDelta);
111
+ // Note: We no longer call applyReorderToRegistry here.
112
+ // The snapshot-based approach means shift calculations use the frozen
113
+ // snapshot taken at drag start. After drag ends, React re-renders
114
+ // will naturally update the registry via updateItemIndex calls.
77
115
  // Reset currentTranslateY to stop shift calculations for other items
78
- // Note: dragStartScrollOffset is NOT reset here to prevent visual jump
79
- // It will be reset after the settle animation completes in useDropCompensation
80
116
  currentTranslateY.value = 0;
117
+ // Trigger a micro-scroll to force FlashList to refresh its views
118
+ // This helps FlashList notice the data change and re-render
119
+ const currentOffset = scrollOffset.value;
120
+ (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, currentOffset + 1, false);
121
+ // Scroll back after a short delay
122
+ setTimeout(() => {
123
+ scrollToOffset(currentOffset, false);
124
+ }, 16);
125
+ // Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
126
+ // we need to animate the item to its new position and reset state.
127
+ //
128
+ // Since FlashList hasn't re-rendered, the item's base position is still at the
129
+ // old index. To make it appear at the new index, we calculate the target translateY
130
+ // that positions the item correctly.
131
+ //
132
+ // When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
133
+ // will see the index change and animate translateY back to 0.
134
+ (0, react_native_reanimated_1.cancelAnimation)(translateY);
135
+ // Calculate the target translateY to position item at its new index
136
+ // Since FlashList hasn't updated base position, we need:
137
+ // targetTranslateY = (newIndex - currentIndex) * itemHeight
138
+ const targetTranslateY = (newIndex - currentIndex) * itemHeight;
139
+ // Use a delayed animation to give useDropCompensation a chance to run first
140
+ translateY.value = (0, react_native_reanimated_1.withDelay)(150, // Delay to let React re-render and useDropCompensation run first
141
+ (0, react_native_reanimated_1.withTiming)(targetTranslateY, // Animate to the new position
142
+ { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
143
+ 'worklet';
144
+ // Only reset state if still in dropping state (useDropCompensation didn't handle it)
145
+ if (finished && isDropping.value) {
146
+ // Reset all drag state - the item is now visually at its new position
147
+ // via translateY offset. When FlashList re-renders, useDropCompensation
148
+ // will compensate and animate to 0.
149
+ globalIsDragging.value = false;
150
+ isDropping.value = false;
151
+ draggedIndex.value = -1;
152
+ draggedItemId.value = '';
153
+ measuredItemHeight.value = 0;
154
+ dragStartScrollOffset.value = 0;
155
+ // FlashList doesn't re-render visible items after data changes.
156
+ // Since useDropCompensation won't run, we need to animate translateY
157
+ // back to 0 ourselves. This causes a visual "snap" but prevents the
158
+ // item from being stuck floating forever.
159
+ translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
160
+ duration: 200,
161
+ easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
162
+ });
163
+ }
164
+ }));
81
165
  }
82
166
  else {
83
167
  // Same position - animate back and reset state
@@ -117,6 +201,17 @@ function useDragGesture(config, callbacks) {
117
201
  .enabled(enabled)
118
202
  .onStart(() => {
119
203
  'worklet';
204
+ // Reset any pending drop state from previous drag
205
+ // This is critical: if a new drag starts before the previous drop's
206
+ // fallback completed, isDropping would still be true, causing shift
207
+ // calculations to freeze at their previous values
208
+ if (isDropping.value) {
209
+ isDropping.value = false;
210
+ }
211
+ // Increment drag sequence to force all items to recalculate their shifts
212
+ // This handles the case where items had non-zero shiftY from a previous
213
+ // drag that didn't complete its animations
214
+ dragSequence.value = dragSequence.value + 1;
120
215
  // Measure actual item height for accurate drag calculations
121
216
  const measured = (0, react_native_reanimated_1.measure)(containerRef);
122
217
  if (measured) {
@@ -126,7 +221,13 @@ function useDragGesture(config, callbacks) {
126
221
  isDragging.value = true;
127
222
  // Global drag state for shift animations
128
223
  globalIsDragging.value = true;
129
- draggedIndex.value = index;
224
+ // Use index from the central registry, which is updated on every item render
225
+ // This handles FlashList's recycling behavior where currentIndex.value might be stale
226
+ const registryIndex = itemIndexRegistry.value[itemId];
227
+ const actualIndex = registryIndex !== undefined ? registryIndex : currentIndex.value;
228
+ draggedIndex.value = actualIndex;
229
+ // Also update currentIndex to stay in sync
230
+ currentIndex.value = actualIndex;
130
231
  draggedItemId.value = itemId;
131
232
  dragStartScrollOffset.value = scrollOffset.value;
132
233
  currentTranslateY.value = 0;
@@ -1 +1 @@
1
- {"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CA2G3E"}
1
+ {"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CAkK3E"}
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useDragShift = useDragShift;
4
+ const react_1 = require("react");
4
5
  const react_native_reanimated_1 = require("react-native-reanimated");
6
+ const flash_list_1 = require("@shopify/flash-list");
5
7
  const DragStateContext_1 = require("../../contexts/DragStateContext");
6
8
  const constants_1 = require("../../constants");
7
9
  /**
@@ -25,15 +27,43 @@ const constants_1 = require("../../constants");
25
27
  function useDragShift(config) {
26
28
  const { itemId, index } = config;
27
29
  // Global drag state for coordinating animations across all items
28
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
30
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, isDropping, config: dragConfig, itemIndexRegistry, dragSequence, } = (0, DragStateContext_1.useDragState)();
29
31
  // Shift animation SharedValue
30
32
  const shiftY = (0, react_native_reanimated_1.useSharedValue)(0);
33
+ // Track current index in SharedValue for worklet access
34
+ // This is critical because FlashList may not re-render items after reorder,
35
+ // leaving the closure-captured `index` stale
36
+ const currentIndex = (0, react_native_reanimated_1.useSharedValue)(index);
37
+ // Use FlashList's useRecyclingState to detect when view is recycled
38
+ // This tracks the previous itemId so we can reset state when the view
39
+ // is reused for a different item during autoscroll
40
+ const [prevItemId, setPrevItemId] = (0, flash_list_1.useRecyclingState)(itemId, [itemId]);
41
+ // Reset shiftY and update currentIndex immediately when view is recycled
42
+ // Using useLayoutEffect to run before paint and prevent visual glitches
43
+ (0, react_1.useLayoutEffect)(() => {
44
+ if (prevItemId !== itemId) {
45
+ // View was recycled - reset shiftY to prevent stale shift values
46
+ // from the previous item affecting this item's position
47
+ (0, react_native_reanimated_1.cancelAnimation)(shiftY);
48
+ shiftY.value = 0;
49
+ // Update currentIndex immediately for the new item
50
+ currentIndex.value = index;
51
+ setPrevItemId(itemId);
52
+ }
53
+ }, [itemId, prevItemId, setPrevItemId, shiftY, currentIndex, index]);
54
+ // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
55
+ (0, react_1.useEffect)(() => {
56
+ currentIndex.value = index;
57
+ }, [index, currentIndex]);
31
58
  // Calculate target shift using useDerivedValue
32
59
  // Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
33
60
  // so we don't need a manual trigger counter. When any read SharedValue changes,
34
61
  // Reanimated will re-execute this derived value.
35
62
  const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
36
63
  'worklet';
64
+ // Read dragSequence to force recalculation when a new drag starts
65
+ // This ensures stale shiftY values from previous drags are recalculated
66
+ const _sequence = dragSequence.value;
37
67
  // During drop transition, freeze shift values
38
68
  if (isDropping.value) {
39
69
  return shiftY.value;
@@ -42,6 +72,10 @@ function useDragShift(config) {
42
72
  const isDraggingNow = globalIsDragging.value;
43
73
  const translateYNow = currentTranslateY.value;
44
74
  const currentDraggedItemId = draggedItemId.value;
75
+ // Use index from the central registry if available, which is updated on every item render
76
+ // This handles FlashList's recycling behavior where currentIndex.value might be stale
77
+ const registryIndex = itemIndexRegistry.value[itemId];
78
+ const myIndex = registryIndex !== undefined ? registryIndex : currentIndex.value;
45
79
  // If I'm the dragged item, no shift needed
46
80
  if (currentDraggedItemId === itemId)
47
81
  return 0;
@@ -71,31 +105,43 @@ function useDragShift(config) {
71
105
  // Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
72
106
  const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
73
107
  const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
74
- if (index < minAffectedIndex || index > maxAffectedIndex) {
108
+ if (myIndex < minAffectedIndex || myIndex > maxAffectedIndex) {
75
109
  return 0;
76
110
  }
77
111
  // Moving DOWN: items between original and hovered positions shift UP
78
112
  if (hoveredIndex > currentDraggedIndex) {
79
- if (index > currentDraggedIndex && index <= hoveredIndex) {
113
+ if (myIndex > currentDraggedIndex && myIndex <= hoveredIndex) {
80
114
  return -itemHeight;
81
115
  }
82
116
  }
83
117
  // Moving UP: items between hovered and original positions shift DOWN
84
118
  else if (hoveredIndex < currentDraggedIndex) {
85
- if (index < currentDraggedIndex && index >= hoveredIndex) {
119
+ if (myIndex < currentDraggedIndex && myIndex >= hoveredIndex) {
86
120
  return itemHeight;
87
121
  }
88
122
  }
89
123
  return 0;
90
- }, [index, itemId]);
91
- // Animate shift when target changes
92
- (0, react_native_reanimated_1.useAnimatedReaction)(() => targetShiftY.value, (target, prev) => {
124
+ }, [itemId]);
125
+ // Animate shift when target changes or when a new drag starts
126
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => ({
127
+ target: targetShiftY.value,
128
+ sequence: dragSequence.value,
129
+ }), (state, prev) => {
93
130
  'worklet';
94
- if (target !== prev) {
131
+ const targetChanged = state.target !== prev?.target;
132
+ const newDragStarted = state.sequence !== prev?.sequence;
133
+ // Animate to target if:
134
+ // 1. The target changed, OR
135
+ // 2. A new drag started and we're not already at the target
136
+ const needsAnimation = targetChanged ||
137
+ (newDragStarted &&
138
+ Math.abs(shiftY.value - state.target) >
139
+ constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD);
140
+ if (needsAnimation) {
95
141
  // Cancel any in-flight animation before starting a new one
96
142
  // This prevents competing animations and visual glitches
97
143
  (0, react_native_reanimated_1.cancelAnimation)(shiftY);
98
- shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
144
+ shiftY.value = (0, react_native_reanimated_1.withTiming)(state.target, {
99
145
  duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
100
146
  easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
101
147
  });
@@ -115,6 +115,11 @@ export interface AnimatedFlashListProps<T extends AnimatedListItem> extends Omit
115
115
  onEndReached?: () => void;
116
116
  /** Pagination threshold */
117
117
  onEndReachedThreshold?: number;
118
+ /**
119
+ * Estimated size of each item (deprecated in FlashList v2, but used as default for drag itemHeight)
120
+ * @deprecated Use config.drag.itemHeight instead
121
+ */
122
+ estimatedItemSize?: number;
118
123
  }
119
124
  /**
120
125
  * Ref handle for AnimatedFlashList
@@ -1 +1 @@
1
- {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,OAAO,CAAC;AAChF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AACzC,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,cAAc,CAAC;AAEtB;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,gCAAgC;IAChC,OAAO,EAAE,WAAW,CAAC;IACrB,2CAA2C;IAC3C,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,gBAAgB;IAChE,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,iFAAiF;IACjF,aAAa,EAAE,SAAS,CAAC;IACzB,gFAAgF;IAChF,eAAe,EAAE,eAAe,GAAG,IAAI,CAAC;IACxC,mDAAmD;IACnD,UAAU,EAAE,OAAO,CAAC;IACpB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,oBAAoB,EAAE,CACpB,SAAS,EAAE,kBAAkB,EAC7B,UAAU,EAAE,MAAM,IAAI,EACtB,MAAM,CAAC,EAAE,mBAAmB,KACzB,IAAI,CAAC;IACV,iCAAiC;IACjC,kBAAkB,EAAE,MAAM,IAAI,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,uBAAuB;IACtC,kCAAkC;IAClC,IAAI,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC3B,mCAAmC;IACnC,aAAa,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IAC7C,oCAAoC;IACpC,cAAc,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB,CAAC,CAAC,SAAS,gBAAgB,CAChE,SAAQ,IAAI,CACV,cAAc,CAAC,CAAC,CAAC,EACjB,MAAM,GAAG,YAAY,GAAG,cAAc,GAAG,KAAK,CAC/C;IACD,gBAAgB;IAChB,IAAI,EAAE,CAAC,EAAE,CAAC;IAEV,6BAA6B;IAC7B,YAAY,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjD;;;OAGG;IACH,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,YAAY,CAAC;IAE9D;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAEzE;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAAG,IAAI,EAC1B,YAAY,EAAE,MAAM,GAAG,IAAI,KACxB,IAAI,CAAC;IAEV;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IAE9C;;;OAGG;IACH,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAEtD;;OAEG;IACH,MAAM,CAAC,EAAE,uBAAuB,CAAC;IAEjC;;;OAGG;IACH,wBAAwB,CAAC,EAAE,MAAM,IAAI,CAAC;IAEtC,uBAAuB;IACvB,mBAAmB,CAAC,EAChB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAAC,OAAO,CAAC,CAAC,GAC9D,aAAa,CAAC,OAAO,CAAC,GACtB,IAAI,CAAC;IAET,uBAAuB;IACvB,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,mCAAmC;IACnC,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB,0BAA0B;IAC1B,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAE1B,2BAA2B;IAC3B,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,yBAAyB,EAAE,MAAM,IAAI,CAAC;IAEtC;;OAEG;IACH,cAAc,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAE7D;;OAEG;IACH,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;CAC5D"}
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/types/list.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@souscheflabs/reanimated-flashlist",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "A high-performance animated FlashList with drag-to-reorder and entry/exit animations (New Architecture)",
5
5
  "main": "lib/index.js",
6
6
  "module": "lib/index.js",
@@ -8,7 +8,8 @@
8
8
  "source": "src/index.ts",
9
9
  "files": [
10
10
  "lib",
11
- "src"
11
+ "src",
12
+ "assets"
12
13
  ],
13
14
  "sideEffects": false,
14
15
  "scripts": {
@@ -274,16 +274,25 @@ function AnimatedFlashListInner<T extends AnimatedListItem>(
274
274
  onEndReached,
275
275
  onEndReachedThreshold = 0.5,
276
276
  contentContainerStyle,
277
+ estimatedItemSize,
277
278
  ...flashListProps
278
279
  } = props;
279
280
 
280
281
  // Merge config with defaults
282
+ // Use estimatedItemSize as default itemHeight if not explicitly configured
281
283
  const dragConfig = useMemo(
282
- () => ({
283
- ...DEFAULT_DRAG_CONFIG,
284
- ...config?.drag,
285
- }),
286
- [config?.drag],
284
+ () => {
285
+ const mergedConfig = {
286
+ ...DEFAULT_DRAG_CONFIG,
287
+ ...config?.drag,
288
+ itemHeight: config?.drag?.itemHeight ?? estimatedItemSize ?? DEFAULT_DRAG_CONFIG.itemHeight,
289
+ };
290
+ if (__DEV__) {
291
+ console.log(`[AnimatedFlashList] dragConfig.itemHeight=${mergedConfig.itemHeight}, estimatedItemSize=${estimatedItemSize}, config.drag?.itemHeight=${config?.drag?.itemHeight}`);
292
+ }
293
+ return mergedConfig;
294
+ },
295
+ [config?.drag, estimatedItemSize],
287
296
  );
288
297
 
289
298
  // Ref to FlashList
@@ -9,7 +9,7 @@ import {
9
9
  useListExitAnimation,
10
10
  useListEntryAnimation,
11
11
  } from './hooks';
12
- import { useListAnimationOptional } from './contexts';
12
+ import { useListAnimationOptional, useDragState } from './contexts';
13
13
  import type {
14
14
  AnimatedListItem,
15
15
  AnimatedRenderItemInfo,
@@ -53,6 +53,19 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
53
53
  onReorderByDelta,
54
54
  onHapticFeedback,
55
55
  }: AnimatedFlashListItemProps<T>): React.ReactElement | null {
56
+ // DEBUG: Log renders
57
+ if (__DEV__) {
58
+ console.log(`[AnimatedFlashListItem] RENDER ${item.id} at index=${index}`);
59
+ }
60
+
61
+ // Register this item's index in the central registry
62
+ // This handles FlashList's recycling behavior where components may not re-render
63
+ // after data changes, leaving their index props stale
64
+ const { updateItemIndex } = useDragState();
65
+ // Call updateItemIndex synchronously during render to ensure the registry
66
+ // is always up-to-date when any item renders
67
+ updateItemIndex(item.id, index);
68
+
56
69
  // Animated ref for measuring item height on drag start
57
70
  const containerRef = useAnimatedRef<Animated.View>();
58
71
 
@@ -46,6 +46,8 @@ export interface DragStateContextValue {
46
46
  measuredItemHeight: SharedValue<number>;
47
47
  /** Flag to freeze shift values during drop transition */
48
48
  isDropping: SharedValue<boolean>;
49
+ /** Counter incremented on each drag start, used to force shift recalculation */
50
+ dragSequence: SharedValue<number>;
49
51
  /** Register the FlashList ref for autoscroll operations */
50
52
  setListRef: (ref: FlashListRef<unknown> | null) => void;
51
53
  /** Scroll the list to a specific offset (for autoscroll during drag) */
@@ -54,6 +56,27 @@ export interface DragStateContextValue {
54
56
  resetDragState: () => void;
55
57
  /** Current drag configuration */
56
58
  config: DragConfig;
59
+ /**
60
+ * Index registry for tracking itemId -> index mapping.
61
+ * Updated on every item render to handle FlashList recycling.
62
+ * Stored on UI thread as SharedValue for worklet access.
63
+ */
64
+ itemIndexRegistry: SharedValue<Record<string, number>>;
65
+ /** Update an item's index in the registry (call from JS thread) */
66
+ updateItemIndex: (itemId: string, index: number) => void;
67
+ /** Get an item's index from the registry */
68
+ getItemIndex: (itemId: string) => number | undefined;
69
+ /**
70
+ * Snapshot of itemIndexRegistry taken at drag start.
71
+ * Used during drag to ensure consistent index lookup regardless of
72
+ * React re-renders or FlashList recycling.
73
+ */
74
+ dragStartIndexSnapshot: SharedValue<Record<string, number>>;
75
+ /**
76
+ * Capture a snapshot of the current itemIndexRegistry for use during drag.
77
+ * Call this at the start of a drag operation.
78
+ */
79
+ snapshotRegistryForDrag: () => void;
57
80
  }
58
81
 
59
82
  const DragStateContext = createContext<DragStateContextValue | null>(null);
@@ -156,6 +179,17 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
156
179
  // Flag to freeze shift values during drop transition
157
180
  const isDropping = useSharedValue(false);
158
181
 
182
+ // Counter incremented on each drag start, used to force shift recalculation
183
+ const dragSequence = useSharedValue(0);
184
+
185
+ // Index registry for tracking itemId -> index mapping
186
+ // This handles FlashList's recycling behavior where components may not re-render
187
+ // after data changes, leaving their index props stale
188
+ const itemIndexRegistry = useSharedValue<Record<string, number>>({});
189
+
190
+ // Snapshot of registry taken at drag start for consistent index lookup during drag
191
+ const dragStartIndexSnapshot = useSharedValue<Record<string, number>>({});
192
+
159
193
  // Ref to FlashList for autoscroll operations
160
194
  const listRef = useRef<FlashListRef<unknown> | null>(null);
161
195
 
@@ -169,6 +203,36 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
169
203
  listRef.current?.scrollToOffset({ offset, animated });
170
204
  }, []);
171
205
 
206
+ // Update an item's index in the registry
207
+ // Called on every item render to keep indices fresh
208
+ const updateItemIndex = useCallback(
209
+ (itemId: string, index: number) => {
210
+ // Only update if the value has changed to avoid unnecessary SharedValue updates
211
+ if (itemIndexRegistry.value[itemId] !== index) {
212
+ itemIndexRegistry.value = {
213
+ ...itemIndexRegistry.value,
214
+ [itemId]: index,
215
+ };
216
+ }
217
+ },
218
+ [itemIndexRegistry],
219
+ );
220
+
221
+ // Get an item's index from the registry
222
+ const getItemIndex = useCallback(
223
+ (itemId: string): number | undefined => {
224
+ return itemIndexRegistry.value[itemId];
225
+ },
226
+ [itemIndexRegistry],
227
+ );
228
+
229
+ // Capture a snapshot of the registry at drag start
230
+ // This ensures consistent index lookup during the entire drag operation,
231
+ // regardless of React re-renders or FlashList recycling
232
+ const snapshotRegistryForDrag = useCallback(() => {
233
+ dragStartIndexSnapshot.value = { ...itemIndexRegistry.value };
234
+ }, [dragStartIndexSnapshot, itemIndexRegistry]);
235
+
172
236
  // Reset drag state after drop
173
237
  const resetDragState = useCallback(() => {
174
238
  isDragging.value = false;
@@ -197,10 +261,16 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
197
261
  listTopY,
198
262
  measuredItemHeight,
199
263
  isDropping,
264
+ dragSequence,
200
265
  setListRef,
201
266
  scrollToOffset,
202
267
  resetDragState,
203
268
  config,
269
+ itemIndexRegistry,
270
+ updateItemIndex,
271
+ getItemIndex,
272
+ dragStartIndexSnapshot,
273
+ snapshotRegistryForDrag,
204
274
  }),
205
275
  [
206
276
  isDragging,
@@ -215,10 +285,16 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
215
285
  listTopY,
216
286
  measuredItemHeight,
217
287
  isDropping,
288
+ dragSequence,
218
289
  setListRef,
219
290
  scrollToOffset,
220
291
  resetDragState,
221
292
  config,
293
+ itemIndexRegistry,
294
+ updateItemIndex,
295
+ getItemIndex,
296
+ dragStartIndexSnapshot,
297
+ snapshotRegistryForDrag,
222
298
  ],
223
299
  );
224
300
 
@@ -85,25 +85,44 @@ export function useDragAnimatedStyle(
85
85
  // Animated style for drag offset with scale and shadow
86
86
  const dragAnimatedStyle = useAnimatedStyle(() => {
87
87
  const isThisItemDragged = draggedItemId.value === itemId;
88
-
89
- // Keep elevated if: actively dragging OR has offset (animating back)
90
- const shouldBeElevated =
91
- isDragging.value ||
92
- Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
88
+ const hasSignificantTranslateY = Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
93
89
 
94
90
  // Calculate scroll delta for position compensation during autoscroll
95
91
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
96
92
 
97
- // Use drag translateY + scroll compensation for dragged item, shiftY for others
98
- const yOffset = isThisItemDragged
93
+ // Determine when to use translateY for positioning:
94
+ // 1. Actively being dragged (isThisItemDragged && isDragging)
95
+ // 2. Animating/settling after drop (hasSignificantTranslateY)
96
+ //
97
+ // Case 2 handles the FALLBACK scenario where FlashList doesn't re-render
98
+ // after reorder. The item uses translateY to stay at its new position
99
+ // until FlashList eventually re-renders and useDropCompensation runs.
100
+ const shouldUseDragOffset =
101
+ (isThisItemDragged && isDragging.value) || hasSignificantTranslateY;
102
+
103
+ // Use drag translateY + scroll compensation for dragged/settling item,
104
+ // shiftY for others or for items at rest
105
+ const yOffset = shouldUseDragOffset
99
106
  ? translateY.value + scrollDelta
100
107
  : shiftY.value;
101
108
 
109
+ // Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
110
+ // Not when just settling at new position after drop
111
+ const isActivelyDragging = isThisItemDragged && isDragging.value;
112
+
113
+ // Keep elevated zIndex when actively dragging OR when settling at new position
114
+ // This prevents the dropped item from appearing behind other items
115
+ // Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
116
+ // after FALLBACK completes, draggedItemId is reset to '' but translateY remains
117
+ const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
118
+
102
119
  return {
103
120
  transform: [
104
121
  { translateY: yOffset },
105
- { scale: isThisItemDragged ? draggedScale.value : 1 },
122
+ // Only apply drag scale when actively dragging
123
+ { scale: isActivelyDragging ? draggedScale.value : 1 },
106
124
  ],
125
+ // Elevate zIndex when dragging or settling at new position
107
126
  zIndex: shouldBeElevated ? DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
108
127
  // Use animated shadow opacity for smooth transitions
109
128
  shadowOpacity: animatedShadowOpacity.value,
@@ -1,15 +1,17 @@
1
- import { useCallback, useRef, useMemo } from 'react';
1
+ import { useCallback, useRef, useMemo, useEffect, useLayoutEffect } from 'react';
2
2
  import {
3
3
  useSharedValue,
4
4
  measure,
5
5
  withSpring,
6
6
  withTiming,
7
+ withDelay,
7
8
  cancelAnimation,
8
9
  Easing,
9
10
  interpolate,
10
11
  } from 'react-native-reanimated';
11
12
  import { Gesture } from 'react-native-gesture-handler';
12
13
  import { scheduleOnRN } from 'react-native-worklets';
14
+ import { useRecyclingState } from '@shopify/flash-list';
13
15
  import { useDragState } from '../../contexts/DragStateContext';
14
16
  import type {
15
17
  UseDragGestureConfig,
@@ -71,6 +73,9 @@ export function useDragGesture(
71
73
  isDropping,
72
74
  scrollToOffset,
73
75
  config: dragConfig,
76
+ itemIndexRegistry,
77
+ snapshotRegistryForDrag,
78
+ dragSequence,
74
79
  } = useDragState();
75
80
 
76
81
  // Performance optimization: Track last significant Y position and scroll time
@@ -78,9 +83,37 @@ export function useDragGesture(
78
83
  const lastSignificantY = useSharedValue(0);
79
84
  const lastScrollTime = useSharedValue(0);
80
85
 
86
+ // Track current index in SharedValue for worklet access
87
+ // This is critical because FlashList may not re-render items after reorder,
88
+ // leaving the closure-captured `index` stale
89
+ const currentIndex = useSharedValue(index);
90
+
91
+ // Use FlashList's useRecyclingState to detect when view is recycled
92
+ // This tracks the previous itemId so we can reset state when the view
93
+ // is reused for a different item during autoscroll
94
+ const [prevItemId, setPrevItemId] = useRecyclingState(itemId, [itemId]);
95
+
96
+ // Reset translateY and currentIndex when view is recycled
97
+ // Using useLayoutEffect to run before paint and prevent visual glitches
98
+ useLayoutEffect(() => {
99
+ if (prevItemId !== itemId) {
100
+ // View was recycled - reset local drag state to prevent stale values
101
+ // from the previous item affecting this item's position
102
+ cancelAnimation(translateY);
103
+ translateY.value = 0;
104
+ isDragging.value = false;
105
+ currentIndex.value = index;
106
+ setPrevItemId(itemId);
107
+ }
108
+ }, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
109
+
110
+ // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
111
+ useEffect(() => {
112
+ currentIndex.value = index;
113
+ }, [index, currentIndex]);
114
+
81
115
  // Store current values in refs for stable gesture callbacks
82
116
  const dragContextRef = useRef({
83
- index,
84
117
  totalItems,
85
118
  itemId,
86
119
  onReorderByDelta,
@@ -88,7 +121,6 @@ export function useDragGesture(
88
121
 
89
122
  // Keep ref in sync with current values
90
123
  dragContextRef.current = {
91
- index,
92
124
  totalItems,
93
125
  itemId,
94
126
  onReorderByDelta,
@@ -98,12 +130,19 @@ export function useDragGesture(
98
130
  const handleDragEnd = useCallback(
99
131
  (finalTranslateY: number) => {
100
132
  const {
101
- index: currentIndex,
102
133
  totalItems: total,
103
134
  itemId: currentItemId,
104
135
  onReorderByDelta: reorder,
105
136
  } = dragContextRef.current;
106
137
 
138
+ // Use draggedIndex from context, which was set in onStart using the registry
139
+ // This handles FlashList's recycling where closure-captured index may be stale
140
+ const currentIndex = draggedIndex.value;
141
+ if (currentIndex === -1) {
142
+ // Drag was cancelled or reset
143
+ return;
144
+ }
145
+
107
146
  // Use configured itemHeight for consistent position calculations
108
147
  const itemHeight = dragConfig.itemHeight;
109
148
 
@@ -119,15 +158,79 @@ export function useDragGesture(
119
158
  reorder && positionDelta !== 0 && newIndex !== currentIndex;
120
159
 
121
160
  if (positionChanged) {
122
- // Position changes - call reorder, let useDropCompensation handle animation
161
+ // Position changes - call reorder and animate back
162
+ // useDropCompensation handles state reset if re-renders happen,
163
+ // but we also animate here as a fallback since FlashList's virtualization
164
+ // may not trigger re-renders when data changes.
123
165
  isDropping.value = true;
124
166
  onHapticFeedback?.('medium');
125
167
  reorder(currentItemId, positionDelta);
126
168
 
169
+ // Note: We no longer call applyReorderToRegistry here.
170
+ // The snapshot-based approach means shift calculations use the frozen
171
+ // snapshot taken at drag start. After drag ends, React re-renders
172
+ // will naturally update the registry via updateItemIndex calls.
173
+
127
174
  // Reset currentTranslateY to stop shift calculations for other items
128
- // Note: dragStartScrollOffset is NOT reset here to prevent visual jump
129
- // It will be reset after the settle animation completes in useDropCompensation
130
175
  currentTranslateY.value = 0;
176
+
177
+ // Trigger a micro-scroll to force FlashList to refresh its views
178
+ // This helps FlashList notice the data change and re-render
179
+ const currentOffset = scrollOffset.value;
180
+ scheduleOnRN(scrollToOffset, currentOffset + 1, false);
181
+ // Scroll back after a short delay
182
+ setTimeout(() => {
183
+ scrollToOffset(currentOffset, false);
184
+ }, 16);
185
+
186
+ // Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
187
+ // we need to animate the item to its new position and reset state.
188
+ //
189
+ // Since FlashList hasn't re-rendered, the item's base position is still at the
190
+ // old index. To make it appear at the new index, we calculate the target translateY
191
+ // that positions the item correctly.
192
+ //
193
+ // When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
194
+ // will see the index change and animate translateY back to 0.
195
+ cancelAnimation(translateY);
196
+
197
+ // Calculate the target translateY to position item at its new index
198
+ // Since FlashList hasn't updated base position, we need:
199
+ // targetTranslateY = (newIndex - currentIndex) * itemHeight
200
+ const targetTranslateY = (newIndex - currentIndex) * itemHeight;
201
+
202
+ // Use a delayed animation to give useDropCompensation a chance to run first
203
+ translateY.value = withDelay(
204
+ 150, // Delay to let React re-render and useDropCompensation run first
205
+ withTiming(
206
+ targetTranslateY, // Animate to the new position
207
+ { duration: 150, easing: Easing.out(Easing.ease) },
208
+ finished => {
209
+ 'worklet';
210
+ // Only reset state if still in dropping state (useDropCompensation didn't handle it)
211
+ if (finished && isDropping.value) {
212
+ // Reset all drag state - the item is now visually at its new position
213
+ // via translateY offset. When FlashList re-renders, useDropCompensation
214
+ // will compensate and animate to 0.
215
+ globalIsDragging.value = false;
216
+ isDropping.value = false;
217
+ draggedIndex.value = -1;
218
+ draggedItemId.value = '';
219
+ measuredItemHeight.value = 0;
220
+ dragStartScrollOffset.value = 0;
221
+
222
+ // FlashList doesn't re-render visible items after data changes.
223
+ // Since useDropCompensation won't run, we need to animate translateY
224
+ // back to 0 ourselves. This causes a visual "snap" but prevents the
225
+ // item from being stuck floating forever.
226
+ translateY.value = withTiming(0, {
227
+ duration: 200,
228
+ easing: Easing.out(Easing.ease),
229
+ });
230
+ }
231
+ },
232
+ ),
233
+ );
131
234
  } else {
132
235
  // Same position - animate back and reset state
133
236
  // Cancel any existing animation before starting the return animation
@@ -176,6 +279,19 @@ export function useDragGesture(
176
279
  .enabled(enabled)
177
280
  .onStart(() => {
178
281
  'worklet';
282
+ // Reset any pending drop state from previous drag
283
+ // This is critical: if a new drag starts before the previous drop's
284
+ // fallback completed, isDropping would still be true, causing shift
285
+ // calculations to freeze at their previous values
286
+ if (isDropping.value) {
287
+ isDropping.value = false;
288
+ }
289
+
290
+ // Increment drag sequence to force all items to recalculate their shifts
291
+ // This handles the case where items had non-zero shiftY from a previous
292
+ // drag that didn't complete its animations
293
+ dragSequence.value = dragSequence.value + 1;
294
+
179
295
  // Measure actual item height for accurate drag calculations
180
296
  const measured = measure(containerRef);
181
297
  if (measured) {
@@ -186,7 +302,14 @@ export function useDragGesture(
186
302
  isDragging.value = true;
187
303
  // Global drag state for shift animations
188
304
  globalIsDragging.value = true;
189
- draggedIndex.value = index;
305
+ // Use index from the central registry, which is updated on every item render
306
+ // This handles FlashList's recycling behavior where currentIndex.value might be stale
307
+ const registryIndex = itemIndexRegistry.value[itemId];
308
+ const actualIndex =
309
+ registryIndex !== undefined ? registryIndex : currentIndex.value;
310
+ draggedIndex.value = actualIndex;
311
+ // Also update currentIndex to stay in sync
312
+ currentIndex.value = actualIndex;
190
313
  draggedItemId.value = itemId;
191
314
  dragStartScrollOffset.value = scrollOffset.value;
192
315
  currentTranslateY.value = 0;
@@ -1,3 +1,4 @@
1
+ import { useEffect, useLayoutEffect } from 'react';
1
2
  import {
2
3
  useSharedValue,
3
4
  useDerivedValue,
@@ -6,6 +7,7 @@ import {
6
7
  cancelAnimation,
7
8
  Easing,
8
9
  } from 'react-native-reanimated';
10
+ import { useRecyclingState } from '@shopify/flash-list';
9
11
  import { useDragState } from '../../contexts/DragStateContext';
10
12
  import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
11
13
  import type { UseDragShiftConfig, UseDragShiftResult } from '../../types';
@@ -39,20 +41,54 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
39
41
  currentTranslateY,
40
42
  scrollOffset,
41
43
  dragStartScrollOffset,
42
- measuredItemHeight,
43
44
  isDropping,
44
45
  config: dragConfig,
46
+ itemIndexRegistry,
47
+ dragSequence,
45
48
  } = useDragState();
46
49
 
47
50
  // Shift animation SharedValue
48
51
  const shiftY = useSharedValue(0);
49
52
 
53
+ // Track current index in SharedValue for worklet access
54
+ // This is critical because FlashList may not re-render items after reorder,
55
+ // leaving the closure-captured `index` stale
56
+ const currentIndex = useSharedValue(index);
57
+
58
+ // Use FlashList's useRecyclingState to detect when view is recycled
59
+ // This tracks the previous itemId so we can reset state when the view
60
+ // is reused for a different item during autoscroll
61
+ const [prevItemId, setPrevItemId] = useRecyclingState(itemId, [itemId]);
62
+
63
+ // Reset shiftY and update currentIndex immediately when view is recycled
64
+ // Using useLayoutEffect to run before paint and prevent visual glitches
65
+ useLayoutEffect(() => {
66
+ if (prevItemId !== itemId) {
67
+ // View was recycled - reset shiftY to prevent stale shift values
68
+ // from the previous item affecting this item's position
69
+ cancelAnimation(shiftY);
70
+ shiftY.value = 0;
71
+ // Update currentIndex immediately for the new item
72
+ currentIndex.value = index;
73
+ setPrevItemId(itemId);
74
+ }
75
+ }, [itemId, prevItemId, setPrevItemId, shiftY, currentIndex, index]);
76
+
77
+ // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
78
+ useEffect(() => {
79
+ currentIndex.value = index;
80
+ }, [index, currentIndex]);
81
+
50
82
  // Calculate target shift using useDerivedValue
51
83
  // Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
52
84
  // so we don't need a manual trigger counter. When any read SharedValue changes,
53
85
  // Reanimated will re-execute this derived value.
54
86
  const targetShiftY = useDerivedValue(() => {
55
87
  'worklet';
88
+ // Read dragSequence to force recalculation when a new drag starts
89
+ // This ensures stale shiftY values from previous drags are recalculated
90
+ const _sequence = dragSequence.value;
91
+
56
92
  // During drop transition, freeze shift values
57
93
  if (isDropping.value) {
58
94
  return shiftY.value;
@@ -62,6 +98,11 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
62
98
  const isDraggingNow = globalIsDragging.value;
63
99
  const translateYNow = currentTranslateY.value;
64
100
  const currentDraggedItemId = draggedItemId.value;
101
+ // Use index from the central registry if available, which is updated on every item render
102
+ // This handles FlashList's recycling behavior where currentIndex.value might be stale
103
+ const registryIndex = itemIndexRegistry.value[itemId];
104
+ const myIndex =
105
+ registryIndex !== undefined ? registryIndex : currentIndex.value;
65
106
 
66
107
  // If I'm the dragged item, no shift needed
67
108
  if (currentDraggedItemId === itemId) return 0;
@@ -75,6 +116,7 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
75
116
  // The configured itemHeight (content + margins) provides reliable, predictable behavior.
76
117
  const itemHeight = dragConfig.itemHeight;
77
118
 
119
+
78
120
  // Calculate effective translateY including scroll delta
79
121
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
80
122
  const effectiveTranslateY = translateYNow + scrollDelta;
@@ -97,36 +139,51 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
97
139
  // Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
98
140
  const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
99
141
  const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
100
- if (index < minAffectedIndex || index > maxAffectedIndex) {
142
+ if (myIndex < minAffectedIndex || myIndex > maxAffectedIndex) {
101
143
  return 0;
102
144
  }
103
145
 
104
146
  // Moving DOWN: items between original and hovered positions shift UP
105
147
  if (hoveredIndex > currentDraggedIndex) {
106
- if (index > currentDraggedIndex && index <= hoveredIndex) {
148
+ if (myIndex > currentDraggedIndex && myIndex <= hoveredIndex) {
107
149
  return -itemHeight;
108
150
  }
109
151
  }
110
152
  // Moving UP: items between hovered and original positions shift DOWN
111
153
  else if (hoveredIndex < currentDraggedIndex) {
112
- if (index < currentDraggedIndex && index >= hoveredIndex) {
154
+ if (myIndex < currentDraggedIndex && myIndex >= hoveredIndex) {
113
155
  return itemHeight;
114
156
  }
115
157
  }
116
158
 
117
159
  return 0;
118
- }, [index, itemId]);
160
+ }, [itemId]);
119
161
 
120
- // Animate shift when target changes
162
+ // Animate shift when target changes or when a new drag starts
121
163
  useAnimatedReaction(
122
- () => targetShiftY.value,
123
- (target, prev) => {
164
+ () => ({
165
+ target: targetShiftY.value,
166
+ sequence: dragSequence.value,
167
+ }),
168
+ (state, prev) => {
124
169
  'worklet';
125
- if (target !== prev) {
170
+ const targetChanged = state.target !== prev?.target;
171
+ const newDragStarted = state.sequence !== prev?.sequence;
172
+
173
+ // Animate to target if:
174
+ // 1. The target changed, OR
175
+ // 2. A new drag started and we're not already at the target
176
+ const needsAnimation =
177
+ targetChanged ||
178
+ (newDragStarted &&
179
+ Math.abs(shiftY.value - state.target) >
180
+ DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD);
181
+
182
+ if (needsAnimation) {
126
183
  // Cancel any in-flight animation before starting a new one
127
184
  // This prevents competing animations and visual glitches
128
185
  cancelAnimation(shiftY);
129
- shiftY.value = withTiming(target, {
186
+ shiftY.value = withTiming(state.target, {
130
187
  duration: ANIMATION_TIMING.SHIFT_DURATION,
131
188
  easing: Easing.out(Easing.ease),
132
189
  });
package/src/types/list.ts CHANGED
@@ -155,6 +155,12 @@ export interface AnimatedFlashListProps<T extends AnimatedListItem>
155
155
 
156
156
  /** Pagination threshold */
157
157
  onEndReachedThreshold?: number;
158
+
159
+ /**
160
+ * Estimated size of each item (deprecated in FlashList v2, but used as default for drag itemHeight)
161
+ * @deprecated Use config.drag.itemHeight instead
162
+ */
163
+ estimatedItemSize?: number;
158
164
  }
159
165
 
160
166
  /**