@souscheflabs/reanimated-flashlist 0.2.8 → 0.3.1

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.
@@ -1,7 +1,7 @@
1
- import React, { type ReactNode } from 'react';
2
- import { type SharedValue } from 'react-native-reanimated';
3
- import type { FlashListRef } from '@shopify/flash-list';
4
- import type { DragConfig } from '../types';
1
+ import React, { type ReactNode } from "react";
2
+ import { type SharedValue } from "react-native-reanimated";
3
+ import type { FlashListRef } from "@shopify/flash-list";
4
+ import type { DragConfig } from "../types";
5
5
  /**
6
6
  * Centralized drag state for coordinating animations across list items.
7
7
  * All animation values are Reanimated SharedValues for 60fps UI thread performance.
@@ -43,6 +43,8 @@ export interface DragStateContextValue {
43
43
  setListRef: (ref: FlashListRef<unknown> | null) => void;
44
44
  /** Scroll the list to a specific offset (for autoscroll during drag) */
45
45
  scrollToOffset: (offset: number, animated?: boolean) => void;
46
+ /** Ask FlashList to prepare cells for layout animation on the next render */
47
+ prepareForLayoutAnimationRender: () => void;
46
48
  /** Reset drag state after drop animation completes */
47
49
  resetDragState: () => void;
48
50
  /** Current drag configuration */
@@ -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;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAoB3C;;;;;;;;;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;IACpC;;;OAGG;IACH,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrF;;;OAGG;IACH,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC;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,CAyN9D,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;AAE3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAgC3C;;;;;;;;;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,6EAA6E;IAC7E,+BAA+B,EAAE,MAAM,IAAI,CAAC;IAC5C,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;IACpC;;;OAGG;IACH,sBAAsB,EAAE,CACtB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,KACZ,IAAI,CAAC;IACV;;;OAGG;IACH,mBAAmB,EAAE,MAAM,IAAI,CAAC;CACjC;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,CAyO9D,CAAC"}
@@ -44,12 +44,21 @@ const constants_1 = require("../constants");
44
44
  * visible to other worklets (like onStart in useDragGesture).
45
45
  */
46
46
  const updateRegistryOnUI = (registry, itemId, index) => {
47
- 'worklet';
47
+ "worklet";
48
48
  const existingIndex = registry.value[itemId];
49
49
  if (existingIndex === undefined || existingIndex !== index) {
50
50
  registry.value = { ...registry.value, [itemId]: index };
51
51
  }
52
52
  };
53
+ // In tests or non-worklet environments, scheduleOnUI may be undefined. Provide a safe fallback.
54
+ const scheduleOnUIOrImmediate = (fn, ...args) => {
55
+ if (typeof react_native_worklets_1.scheduleOnUI === "function") {
56
+ (0, react_native_worklets_1.scheduleOnUI)(fn, ...args);
57
+ }
58
+ else {
59
+ fn(...args);
60
+ }
61
+ };
53
62
  const DragStateContext = (0, react_1.createContext)(null);
54
63
  /**
55
64
  * Hook to access shared drag state from context.
@@ -58,7 +67,7 @@ const DragStateContext = (0, react_1.createContext)(null);
58
67
  const useDragState = () => {
59
68
  const context = (0, react_1.useContext)(DragStateContext);
60
69
  if (!context) {
61
- throw new Error('useDragState must be used within DragStateProvider');
70
+ throw new Error("useDragState must be used within DragStateProvider");
62
71
  }
63
72
  return context;
64
73
  };
@@ -80,19 +89,19 @@ exports.useDragState = useDragState;
80
89
  function validateConfig(config) {
81
90
  if (__DEV__) {
82
91
  if (config.itemHeight <= 0) {
83
- console.warn('[AnimatedFlashList] Invalid itemHeight: must be positive. Got:', config.itemHeight);
92
+ console.warn("[AnimatedFlashList] Invalid itemHeight: must be positive. Got:", config.itemHeight);
84
93
  }
85
94
  if (config.longPressDuration <= 0) {
86
- console.warn('[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:', config.longPressDuration);
95
+ console.warn("[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:", config.longPressDuration);
87
96
  }
88
97
  if (config.dragScale <= 0) {
89
- console.warn('[AnimatedFlashList] Invalid dragScale: must be positive. Got:', config.dragScale);
98
+ console.warn("[AnimatedFlashList] Invalid dragScale: must be positive. Got:", config.dragScale);
90
99
  }
91
100
  if (config.edgeThreshold < 0) {
92
- console.warn('[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:', config.edgeThreshold);
101
+ console.warn("[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:", config.edgeThreshold);
93
102
  }
94
103
  if (config.maxScrollSpeed <= 0) {
95
- console.warn('[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:', config.maxScrollSpeed);
104
+ console.warn("[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:", config.maxScrollSpeed);
96
105
  }
97
106
  }
98
107
  }
@@ -106,7 +115,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
106
115
  // Shared values are created once and persist for the lifetime of the provider
107
116
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
108
117
  const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
109
- const draggedItemId = (0, react_native_reanimated_1.useSharedValue)('');
118
+ const draggedItemId = (0, react_native_reanimated_1.useSharedValue)("");
110
119
  const currentTranslateY = (0, react_native_reanimated_1.useSharedValue)(0);
111
120
  const draggedScale = (0, react_native_reanimated_1.useSharedValue)(1);
112
121
  // Scroll state for viewport-aware calculations and autoscroll
@@ -139,6 +148,10 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
139
148
  const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
140
149
  listRef.current?.scrollToOffset({ offset, animated });
141
150
  }, []);
151
+ // Request FlashList to prepare cells for layout animation on next render
152
+ const prepareForLayoutAnimationRender = (0, react_1.useCallback)(() => {
153
+ listRef.current?.prepareForLayoutAnimationRender?.();
154
+ }, []);
142
155
  // Update an item's index in the registry
143
156
  // Uses scheduleOnUI to ensure SharedValue modifications are immediately visible
144
157
  // to UI thread worklets (fixing the registry sync bug).
@@ -149,9 +162,9 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
149
162
  const existingIndex = itemIndexRegistry.value[itemId];
150
163
  const timeSinceUpdate = Date.now() - registryUpdateTimestamp.current;
151
164
  const isProtected = timeSinceUpdate < 500; // 500ms protection window
152
- // Add new items always - schedule on UI thread for immediate visibility
165
+ // Add new items always - schedule on UI thread for immediate visibility (or synchronous fallback in tests)
153
166
  if (existingIndex === undefined) {
154
- (0, react_native_worklets_1.scheduleOnUI)(updateRegistryOnUI, itemIndexRegistry, itemId, index);
167
+ scheduleOnUIOrImmediate(updateRegistryOnUI, itemIndexRegistry, itemId, index);
155
168
  return;
156
169
  }
157
170
  // Skip update if within protection window (onEnd just updated the registry)
@@ -161,7 +174,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
161
174
  }
162
175
  // Outside protection window, update if value changed - schedule on UI thread
163
176
  if (existingIndex !== index) {
164
- (0, react_native_worklets_1.scheduleOnUI)(updateRegistryOnUI, itemIndexRegistry, itemId, index);
177
+ scheduleOnUIOrImmediate(updateRegistryOnUI, itemIndexRegistry, itemId, index);
165
178
  }
166
179
  }, [itemIndexRegistry]);
167
180
  // Mark registry as recently updated (call this from handleDragEnd)
@@ -210,7 +223,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
210
223
  const resetDragState = (0, react_1.useCallback)(() => {
211
224
  isDragging.value = false;
212
225
  draggedIndex.value = -1;
213
- draggedItemId.value = '';
226
+ draggedItemId.value = "";
214
227
  currentTranslateY.value = 0;
215
228
  draggedScale.value = 1;
216
229
  dragStartScrollOffset.value = 0;
@@ -235,6 +248,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
235
248
  dragSequence,
236
249
  setListRef,
237
250
  scrollToOffset,
251
+ prepareForLayoutAnimationRender,
238
252
  resetDragState,
239
253
  config,
240
254
  itemIndexRegistry,
@@ -1,5 +1,5 @@
1
- import type { SharedValue } from 'react-native-reanimated';
2
- import type { UseDragAnimatedStyleResult } from '../../types';
1
+ import type { SharedValue } from "react-native-reanimated";
2
+ import type { UseDragAnimatedStyleResult } from "../../types";
3
3
  /**
4
4
  * Hook that creates animated styles for drag operations.
5
5
  *
@@ -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,CA4F5B"}
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,CAmF5B"}
@@ -35,7 +35,7 @@ const constants_1 = require("../../constants");
35
35
  */
36
36
  function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
37
37
  // Global drag state for scale, identity check, and scroll compensation
38
- const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset, isDropping } = (0, DragStateContext_1.useDragState)();
38
+ const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset, } = (0, DragStateContext_1.useDragState)();
39
39
  // Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
40
40
  // When drag ends, shadow opacity should fade smoothly instead of snapping
41
41
  const animatedShadowOpacity = (0, react_native_reanimated_1.useSharedValue)(0.1);
@@ -44,7 +44,7 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
44
44
  isThisItemDragged: draggedItemId.value === itemId,
45
45
  scale: draggedScale.value,
46
46
  }), (current, previous) => {
47
- 'worklet';
47
+ "worklet";
48
48
  if (current.isThisItemDragged) {
49
49
  // When dragged, interpolate shadow opacity based on scale
50
50
  const targetOpacity = (0, react_native_reanimated_1.interpolate)(current.scale, [1, config.dragScale], [0.1, config.dragShadowOpacity]);
@@ -67,20 +67,9 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
67
67
  const hasSignificantTranslateY = Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
68
68
  // Calculate scroll delta for position compensation during autoscroll
69
69
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
70
- // Determine when to use translateY for positioning:
71
- // 1. Actively being dragged (isThisItemDragged && isDragging)
72
- // 2. Actively settling after drop (isSettling)
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
- //
78
- // CRITICAL: We use isSettling (isDropping && draggedItemId matches) instead
79
- // of hasSignificantTranslateY. This ensures only the CURRENT dropping item
80
- // uses its translateY. Other items with stale translateY from interrupted
81
- // animations won't incorrectly apply their values with a new drag's scrollDelta.
82
- const isSettling = isDropping.value && draggedItemId.value === itemId;
83
- const shouldUseDragOffset = (isThisItemDragged && isDragging.value) || isSettling;
70
+ // Use translateY only while actively dragging. Once drop begins we rely on
71
+ // the new base layout (shiftY/FlashList) to avoid any post-drop bounce.
72
+ const shouldUseDragOffset = isThisItemDragged && isDragging.value;
84
73
  // Use drag translateY + scroll compensation for dragged/settling item,
85
74
  // shiftY for others or for items at rest
86
75
  const yOffset = shouldUseDragOffset
@@ -89,11 +78,8 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
89
78
  // Only apply visual effects (scale, elevation, zIndex) when ACTIVELY dragging
90
79
  // Not when just settling at new position after drop
91
80
  const isActivelyDragging = isThisItemDragged && isDragging.value;
92
- // Keep elevated zIndex when actively dragging OR when settling at new position
93
- // This prevents the dropped item from appearing behind other items
94
- // Note: We use hasSignificantTranslateY alone (not isThisItemDragged) because
95
- // after FALLBACK completes, draggedItemId is reset to '' but translateY remains
96
- const shouldBeElevated = isActivelyDragging || hasSignificantTranslateY;
81
+ // Keep elevated zIndex only while actively dragging
82
+ const shouldBeElevated = isActivelyDragging;
97
83
  return {
98
84
  transform: [
99
85
  { translateY: yOffset },
@@ -1,4 +1,4 @@
1
- import type { UseDragGestureConfig, UseDragGestureCallbacks, UseDragGestureResult } from '../../types';
1
+ import type { UseDragGestureConfig, UseDragGestureCallbacks, UseDragGestureResult } from "../../types";
2
2
  /**
3
3
  * Hook that encapsulates all drag gesture logic.
4
4
  *
@@ -1 +1 @@
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,CA8ctB"}
1
+ {"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"AAoBA,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,CAybtB"}
@@ -37,11 +37,13 @@ const DragStateContext_1 = require("../../contexts/DragStateContext");
37
37
  function useDragGesture(config, callbacks) {
38
38
  const { itemId, index, totalItems, enabled, containerRef } = config;
39
39
  const { onReorderByDelta, onHapticFeedback } = callbacks;
40
+ // Dev-only guard to warn once if measurement is missing
41
+ const hasWarnedMissingHeight = (0, react_1.useRef)(false);
40
42
  // Local drag state for this item's animation
41
43
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
42
44
  const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
43
45
  // Global drag state for coordinating animations across all items
44
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, itemIndexRegistry, dragStartIndexSnapshot, dragSequence, markRegistryUpdated, } = (0, DragStateContext_1.useDragState)();
46
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, prepareForLayoutAnimationRender, config: dragConfig, itemIndexRegistry, dragStartIndexSnapshot, dragSequence, markRegistryUpdated, } = (0, DragStateContext_1.useDragState)();
45
47
  // Performance optimization: Track last significant Y position and scroll time
46
48
  // to avoid updating on every pixel movement
47
49
  const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
@@ -66,7 +68,15 @@ function useDragGesture(config, callbacks) {
66
68
  currentIndex.value = index;
67
69
  setPrevItemId(itemId);
68
70
  }
69
- }, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
71
+ }, [
72
+ itemId,
73
+ prevItemId,
74
+ setPrevItemId,
75
+ translateY,
76
+ isDragging,
77
+ currentIndex,
78
+ index,
79
+ ]);
70
80
  // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
71
81
  (0, react_1.useEffect)(() => {
72
82
  currentIndex.value = index;
@@ -98,6 +108,16 @@ function useDragGesture(config, callbacks) {
98
108
  const itemHeight = measuredItemHeight.value > 0
99
109
  ? measuredItemHeight.value
100
110
  : dragConfig.itemHeight;
111
+ if (__DEV__ &&
112
+ measuredItemHeight.value <= 0 &&
113
+ !hasWarnedMissingHeight.current) {
114
+ console.warn("[AnimatedFlashList] Falling back to configured itemHeight because measurement was unavailable", {
115
+ itemId,
116
+ index,
117
+ fallbackHeight: dragConfig.itemHeight,
118
+ });
119
+ hasWarnedMissingHeight.current = true;
120
+ }
101
121
  // Calculate how many positions to move based on drag offset
102
122
  const positionDelta = Math.round(finalTranslateY / itemHeight);
103
123
  // Calculate if position actually changes
@@ -109,7 +129,9 @@ function useDragGesture(config, callbacks) {
109
129
  // but we also animate here as a fallback since FlashList's virtualization
110
130
  // may not trigger re-renders when data changes.
111
131
  isDropping.value = true;
112
- onHapticFeedback?.('medium');
132
+ onHapticFeedback?.("medium");
133
+ // Ask FlashList to prepare cells for layout animation on next render
134
+ prepareForLayoutAnimationRender();
113
135
  reorder(currentItemId, positionDelta);
114
136
  // Mark registry as recently updated to protect against stale React prop overwrites
115
137
  // The onEnd callback already updated the registry on the UI thread - this timestamp
@@ -118,71 +140,15 @@ function useDragGesture(config, callbacks) {
118
140
  // Note: Registry is now updated in onEnd on UI thread to prevent race conditions
119
141
  // where a new drag starts before this JS callback runs.
120
142
  // Reset currentTranslateY to stop shift calculations for other items
143
+ // This triggers all shifted items to animate their shiftY back to 0
121
144
  currentTranslateY.value = 0;
122
- // Trigger a micro-scroll to force FlashList to refresh its views
123
- // This helps FlashList notice the data change and re-render
124
- const currentOffset = scrollOffset.value;
125
- (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, currentOffset + 1, false);
126
- // Scroll back after a short delay
127
- setTimeout(() => {
128
- scrollToOffset(currentOffset, false);
129
- }, 16);
130
- // Fallback: If useDropCompensation doesn't run (no re-render from FlashList),
131
- // we need to animate the item to its new position and reset state.
132
- //
133
- // Since FlashList hasn't re-rendered, the item's base position is still at the
134
- // old index. To make it appear at the new index, we calculate the target translateY
135
- // that positions the item correctly.
136
- //
137
- // When FlashList eventually re-renders (on scroll or interaction), useDropCompensation
138
- // will see the index change and animate translateY back to 0.
139
- (0, react_native_reanimated_1.cancelAnimation)(translateY);
140
- // Calculate the target translateY to position item at its new index
141
- // Since FlashList hasn't updated base position, we need:
142
- // targetTranslateY = (newIndex - currentIndex) * itemHeight
143
- const targetTranslateY = (newIndex - currentIndex) * itemHeight;
144
- // Capture current sequence to guard callbacks against race conditions
145
- // If a new drag starts before these animations complete, the sequence
146
- // will have incremented and we should skip the state reset
147
- const currentSequence = dragSequence.value;
148
- // Use a delayed animation to give useDropCompensation a chance to run first
149
- translateY.value = (0, react_native_reanimated_1.withDelay)(150, // Delay to let React re-render and useDropCompensation run first
150
- (0, react_native_reanimated_1.withTiming)(targetTranslateY, // Animate to the new position
151
- { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
152
- 'worklet';
153
- // Guard: Only proceed if this is still the same drag operation
154
- if (dragSequence.value !== currentSequence)
155
- return;
156
- // Only reset state if still in dropping state (useDropCompensation didn't handle it)
157
- if (finished && isDropping.value) {
158
- // FlashList doesn't re-render visible items after data changes.
159
- // Since useDropCompensation won't run, we need to animate translateY
160
- // back to 0 ourselves.
161
- //
162
- // CRITICAL: Do NOT reset state here! We must wait until translateY
163
- // finishes animating to 0. Otherwise, if a new drag starts during
164
- // this 200ms animation, the item will have stale translateY values
165
- // that get applied incorrectly with the new drag's scroll delta.
166
- translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
167
- duration: 200,
168
- easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
169
- }, settleFinished => {
170
- 'worklet';
171
- // Guard: Check sequence again before resetting state
172
- if (dragSequence.value !== currentSequence)
173
- return;
174
- if (settleFinished) {
175
- // NOW reset all state - after translateY is fully 0
176
- globalIsDragging.value = false;
177
- isDropping.value = false;
178
- draggedIndex.value = -1;
179
- draggedItemId.value = '';
180
- measuredItemHeight.value = 0;
181
- dragStartScrollOffset.value = 0;
182
- }
183
- });
184
- }
185
- }));
145
+ // CRITICAL: Store the current translateY value before any animations start
146
+ // This represents where the item visually is at the moment of drop
147
+ const dropTranslateY = translateY.value;
148
+ // Freeze the visual position at the drop point; do not animate toward 0 here
149
+ // because FlashList has not yet updated base layouts. Animating now would
150
+ // create a visible bounce (the issue you are seeing).
151
+ translateY.value = dropTranslateY;
186
152
  }
187
153
  else {
188
154
  // Same position - animate back and reset state
@@ -190,15 +156,15 @@ function useDragGesture(config, callbacks) {
190
156
  const currentSequence = dragSequence.value;
191
157
  // Cancel any existing animation before starting the return animation
192
158
  (0, react_native_reanimated_1.cancelAnimation)(translateY);
193
- translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, finished => {
194
- 'worklet';
159
+ translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150, easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease) }, (finished) => {
160
+ "worklet";
195
161
  // Guard: Only reset if this is still the same drag operation
196
162
  if (dragSequence.value !== currentSequence)
197
163
  return;
198
164
  if (finished) {
199
165
  globalIsDragging.value = false;
200
166
  draggedIndex.value = -1;
201
- draggedItemId.value = '';
167
+ draggedItemId.value = "";
202
168
  currentTranslateY.value = 0;
203
169
  dragStartScrollOffset.value = 0;
204
170
  measuredItemHeight.value = 0;
@@ -221,14 +187,14 @@ function useDragGesture(config, callbacks) {
221
187
  ]);
222
188
  // Stable haptic callback for drag start
223
189
  const triggerLightHaptic = (0, react_1.useCallback)(() => {
224
- onHapticFeedback?.('light');
190
+ onHapticFeedback?.("light");
225
191
  }, [onHapticFeedback]);
226
192
  // Pan gesture for drag-to-reorder
227
193
  const panGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan()
228
194
  .activateAfterLongPress(dragConfig.longPressDuration)
229
195
  .enabled(enabled)
230
196
  .onStart(() => {
231
- 'worklet';
197
+ "worklet";
232
198
  // Reset any pending drop state from previous drag
233
199
  // This is critical: if a new drag starts before the previous drop's
234
200
  // fallback completed, isDropping would still be true, causing shift
@@ -279,8 +245,8 @@ function useDragGesture(config, callbacks) {
279
245
  });
280
246
  (0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
281
247
  })
282
- .onUpdate(event => {
283
- 'worklet';
248
+ .onUpdate((event) => {
249
+ "worklet";
284
250
  // Always update translateY for smooth visual feedback
285
251
  translateY.value = event.translationY;
286
252
  // Performance optimization: Only update currentTranslateY (which triggers
@@ -300,7 +266,7 @@ function useDragGesture(config, callbacks) {
300
266
  const topEdge = dragConfig.edgeThreshold;
301
267
  const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
302
268
  if (fingerInList < topEdge && scrollOffset.value > 0) {
303
- const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], 'clamp');
269
+ const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], "clamp");
304
270
  const newOffset = Math.max(0, scrollOffset.value - speed);
305
271
  scrollOffset.value = newOffset;
306
272
  // Throttle actual scroll calls to reduce JS thread pressure
@@ -312,7 +278,7 @@ function useDragGesture(config, callbacks) {
312
278
  else if (fingerInList > bottomEdge) {
313
279
  const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
314
280
  if (scrollOffset.value < maxOffset) {
315
- const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], 'clamp');
281
+ const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], "clamp");
316
282
  const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
317
283
  scrollOffset.value = newOffset;
318
284
  // Throttle actual scroll calls to reduce JS thread pressure
@@ -323,8 +289,8 @@ function useDragGesture(config, callbacks) {
323
289
  }
324
290
  }
325
291
  })
326
- .onEnd(event => {
327
- 'worklet';
292
+ .onEnd((event) => {
293
+ "worklet";
328
294
  isDragging.value = false;
329
295
  // Include scroll compensation in final position calculation
330
296
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
@@ -344,6 +310,9 @@ function useDragGesture(config, callbacks) {
344
310
  const positionDelta = Math.round(finalY / itemHeight);
345
311
  const newIdx = Math.max(0, Math.min(total - 1, currentIdx + positionDelta));
346
312
  if (positionDelta !== 0 && newIdx !== currentIdx) {
313
+ // Mark drop phase immediately on UI thread to prevent non-dragged items
314
+ // from briefly animating back to 0 before the JS drop handler runs.
315
+ isDropping.value = true;
347
316
  // Update registry directly on UI thread
348
317
  const registry = itemIndexRegistry.value;
349
318
  const newRegistry = {};
@@ -378,14 +347,14 @@ function useDragGesture(config, callbacks) {
378
347
  (0, react_native_worklets_1.scheduleOnRN)(handleDragEnd, finalY);
379
348
  })
380
349
  .onFinalize((_event, success) => {
381
- 'worklet';
350
+ "worklet";
382
351
  if (!success) {
383
352
  isDragging.value = false;
384
353
  translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150 });
385
354
  isDropping.value = false;
386
355
  globalIsDragging.value = false;
387
356
  draggedIndex.value = -1;
388
- draggedItemId.value = '';
357
+ draggedItemId.value = "";
389
358
  currentTranslateY.value = 0;
390
359
  draggedScale.value = 1;
391
360
  dragStartScrollOffset.value = 0;
@@ -393,6 +362,15 @@ function useDragGesture(config, callbacks) {
393
362
  }
394
363
  }),
395
364
  // eslint-disable-next-line react-hooks/exhaustive-deps
396
- [enabled, containerRef, triggerLightHaptic, handleDragEnd, index, itemId, dragStartIndexSnapshot, itemIndexRegistry]);
365
+ [
366
+ enabled,
367
+ containerRef,
368
+ triggerLightHaptic,
369
+ handleDragEnd,
370
+ index,
371
+ itemId,
372
+ dragStartIndexSnapshot,
373
+ itemIndexRegistry,
374
+ ]);
397
375
  return { panGesture, isDragging, translateY };
398
376
  }
@@ -1,4 +1,4 @@
1
- import type { UseDragShiftConfig, UseDragShiftResult } from '../../types';
1
+ import type { UseDragShiftConfig, UseDragShiftResult } from "../../types";
2
2
  /**
3
3
  * Hook that calculates shift animation for non-dragged items.
4
4
  *
@@ -1 +1 @@
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,CAqO3E"}
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,CAmP3E"}
@@ -60,7 +60,7 @@ function useDragShift(config) {
60
60
  // so we don't need a manual trigger counter. When any read SharedValue changes,
61
61
  // Reanimated will re-execute this derived value.
62
62
  const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
63
- 'worklet';
63
+ "worklet";
64
64
  // Read dragSequence to force recalculation when a new drag starts
65
65
  // This ensures stale shiftY values from previous drags are recalculated
66
66
  const _sequence = dragSequence.value;
@@ -85,10 +85,9 @@ function useDragShift(config) {
85
85
  // Dragged item doesn't need shift (it uses translateY)
86
86
  if (currentDraggedItemId === itemId)
87
87
  return 0;
88
- // During drop: return frozen shiftY.value to preserve visual spacing
89
- // Let useDropCompensation handle the reset when FlashList re-renders with correct indices
88
+ // During drop: force shift to 0 to avoid any extra wobble when FlashList re-renders
90
89
  if (isDropping.value) {
91
- return shiftY.value;
90
+ return 0;
92
91
  }
93
92
  // Not dragging: return 0 (no shift needed)
94
93
  // IMPORTANT: We previously returned flashListMismatch here, but that caused permanent
@@ -149,7 +148,7 @@ function useDragShift(config) {
149
148
  target: targetShiftY.value,
150
149
  sequence: dragSequence.value,
151
150
  }), (state, prev) => {
152
- 'worklet';
151
+ "worklet";
153
152
  const targetChanged = state.target !== prev?.target;
154
153
  const newDragStarted = prev !== null && state.sequence !== prev.sequence;
155
154
  // Cancel any in-flight animation when new drag starts and reset to clean baseline
@@ -176,6 +175,20 @@ function useDragShift(config) {
176
175
  });
177
176
  }
178
177
  });
178
+ // Freeze any in-flight shift animation as soon as drop starts to prevent
179
+ // a “double slide” while isDropping holds the visual spacing.
180
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => isDropping.value, (dropping, prev) => {
181
+ "worklet";
182
+ if (dropping && !prev) {
183
+ (0, react_native_reanimated_1.cancelAnimation)(shiftY);
184
+ shiftY.value = 0;
185
+ }
186
+ // When drop finishes, snap shiftY to 0 (no animation) to avoid a final wobble
187
+ if (!dropping && prev) {
188
+ (0, react_native_reanimated_1.cancelAnimation)(shiftY);
189
+ shiftY.value = 0;
190
+ }
191
+ });
179
192
  // CRITICAL: Explicit reset when drag ends (both isDragging and isDropping are false)
180
193
  // This serves as a safeguard against race conditions where the target-based animation
181
194
  // above doesn't fire properly. When drag fully completes, all items should have shiftY = 0.
@@ -183,21 +196,17 @@ function useDragShift(config) {
183
196
  isDragging: globalIsDragging.value,
184
197
  isDropping: isDropping.value,
185
198
  }), (current, prev) => {
186
- 'worklet';
199
+ "worklet";
187
200
  if (prev === null)
188
201
  return;
189
202
  // Detect when drag fully ends: was dragging/dropping, now neither
190
203
  const wasActive = prev.isDragging || prev.isDropping;
191
204
  const isNowIdle = !current.isDragging && !current.isDropping;
192
205
  if (wasActive && isNowIdle) {
193
- // Force reset shiftY to 0 when drag fully completes
194
- // This catches any items that didn't get reset through the normal target animation
206
+ // Force reset shiftY to 0 when drag fully completes (snap, no animation)
195
207
  if (Math.abs(shiftY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
196
208
  (0, react_native_reanimated_1.cancelAnimation)(shiftY);
197
- shiftY.value = (0, react_native_reanimated_1.withTiming)(0, {
198
- duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
199
- easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
200
- });
209
+ shiftY.value = 0;
201
210
  }
202
211
  }
203
212
  });
@@ -1,4 +1,4 @@
1
- import type { UseDropCompensationConfig } from '../../types';
1
+ import type { UseDropCompensationConfig } from "../../types";
2
2
  /**
3
3
  * Hook that handles index change compensation after drag reorder.
4
4
  *
@@ -1 +1 @@
1
- {"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CA8I3E"}
1
+ {"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CA6I3E"}