@souscheflabs/reanimated-flashlist 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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;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,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,CA+N9D,CAAC"}
@@ -44,7 +44,7 @@ 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 };
@@ -58,7 +58,7 @@ const DragStateContext = (0, react_1.createContext)(null);
58
58
  const useDragState = () => {
59
59
  const context = (0, react_1.useContext)(DragStateContext);
60
60
  if (!context) {
61
- throw new Error('useDragState must be used within DragStateProvider');
61
+ throw new Error("useDragState must be used within DragStateProvider");
62
62
  }
63
63
  return context;
64
64
  };
@@ -80,19 +80,19 @@ exports.useDragState = useDragState;
80
80
  function validateConfig(config) {
81
81
  if (__DEV__) {
82
82
  if (config.itemHeight <= 0) {
83
- console.warn('[AnimatedFlashList] Invalid itemHeight: must be positive. Got:', config.itemHeight);
83
+ console.warn("[AnimatedFlashList] Invalid itemHeight: must be positive. Got:", config.itemHeight);
84
84
  }
85
85
  if (config.longPressDuration <= 0) {
86
- console.warn('[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:', config.longPressDuration);
86
+ console.warn("[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:", config.longPressDuration);
87
87
  }
88
88
  if (config.dragScale <= 0) {
89
- console.warn('[AnimatedFlashList] Invalid dragScale: must be positive. Got:', config.dragScale);
89
+ console.warn("[AnimatedFlashList] Invalid dragScale: must be positive. Got:", config.dragScale);
90
90
  }
91
91
  if (config.edgeThreshold < 0) {
92
- console.warn('[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:', config.edgeThreshold);
92
+ console.warn("[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:", config.edgeThreshold);
93
93
  }
94
94
  if (config.maxScrollSpeed <= 0) {
95
- console.warn('[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:', config.maxScrollSpeed);
95
+ console.warn("[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:", config.maxScrollSpeed);
96
96
  }
97
97
  }
98
98
  }
@@ -106,7 +106,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
106
106
  // Shared values are created once and persist for the lifetime of the provider
107
107
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
108
108
  const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
109
- const draggedItemId = (0, react_native_reanimated_1.useSharedValue)('');
109
+ const draggedItemId = (0, react_native_reanimated_1.useSharedValue)("");
110
110
  const currentTranslateY = (0, react_native_reanimated_1.useSharedValue)(0);
111
111
  const draggedScale = (0, react_native_reanimated_1.useSharedValue)(1);
112
112
  // Scroll state for viewport-aware calculations and autoscroll
@@ -139,6 +139,10 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
139
139
  const scrollToOffset = (0, react_1.useCallback)((offset, animated = false) => {
140
140
  listRef.current?.scrollToOffset({ offset, animated });
141
141
  }, []);
142
+ // Request FlashList to prepare cells for layout animation on next render
143
+ const prepareForLayoutAnimationRender = (0, react_1.useCallback)(() => {
144
+ listRef.current?.prepareForLayoutAnimationRender?.();
145
+ }, []);
142
146
  // Update an item's index in the registry
143
147
  // Uses scheduleOnUI to ensure SharedValue modifications are immediately visible
144
148
  // to UI thread worklets (fixing the registry sync bug).
@@ -210,7 +214,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
210
214
  const resetDragState = (0, react_1.useCallback)(() => {
211
215
  isDragging.value = false;
212
216
  draggedIndex.value = -1;
213
- draggedItemId.value = '';
217
+ draggedItemId.value = "";
214
218
  currentTranslateY.value = 0;
215
219
  draggedScale.value = 1;
216
220
  dragStartScrollOffset.value = 0;
@@ -235,6 +239,7 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
235
239
  dragSequence,
236
240
  setListRef,
237
241
  scrollToOffset,
242
+ prepareForLayoutAnimationRender,
238
243
  resetDragState,
239
244
  config,
240
245
  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":"AAqBA,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,CA2atB"}
@@ -41,7 +41,7 @@ function useDragGesture(config, callbacks) {
41
41
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
42
42
  const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
43
43
  // 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)();
44
+ 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
45
  // Performance optimization: Track last significant Y position and scroll time
46
46
  // to avoid updating on every pixel movement
47
47
  const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
@@ -66,7 +66,15 @@ function useDragGesture(config, callbacks) {
66
66
  currentIndex.value = index;
67
67
  setPrevItemId(itemId);
68
68
  }
69
- }, [itemId, prevItemId, setPrevItemId, translateY, isDragging, currentIndex, index]);
69
+ }, [
70
+ itemId,
71
+ prevItemId,
72
+ setPrevItemId,
73
+ translateY,
74
+ isDragging,
75
+ currentIndex,
76
+ index,
77
+ ]);
70
78
  // Sync index to SharedValue in useEffect (not during render per Reanimated docs)
71
79
  (0, react_1.useEffect)(() => {
72
80
  currentIndex.value = index;
@@ -109,7 +117,9 @@ function useDragGesture(config, callbacks) {
109
117
  // but we also animate here as a fallback since FlashList's virtualization
110
118
  // may not trigger re-renders when data changes.
111
119
  isDropping.value = true;
112
- onHapticFeedback?.('medium');
120
+ onHapticFeedback?.("medium");
121
+ // Ask FlashList to prepare cells for layout animation on next render
122
+ prepareForLayoutAnimationRender();
113
123
  reorder(currentItemId, positionDelta);
114
124
  // Mark registry as recently updated to protect against stale React prop overwrites
115
125
  // The onEnd callback already updated the registry on the UI thread - this timestamp
@@ -118,71 +128,20 @@ function useDragGesture(config, callbacks) {
118
128
  // Note: Registry is now updated in onEnd on UI thread to prevent race conditions
119
129
  // where a new drag starts before this JS callback runs.
120
130
  // Reset currentTranslateY to stop shift calculations for other items
131
+ // This triggers all shifted items to animate their shiftY back to 0
121
132
  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
- }));
133
+ // CRITICAL: Store the current translateY value before any animations start
134
+ // This represents where the item visually is at the moment of drop
135
+ const dropTranslateY = translateY.value;
136
+ // Freeze the visual position at the drop point; do not animate toward 0 here
137
+ // because FlashList has not yet updated base layouts. Animating now would
138
+ // create a visible bounce (the issue you are seeing).
139
+ translateY.value = dropTranslateY;
140
+ console.log("[DROP] Captured translateY at drop:", {
141
+ dropTranslateY,
142
+ currentIndex,
143
+ newIndex,
144
+ });
186
145
  }
187
146
  else {
188
147
  // Same position - animate back and reset state
@@ -190,15 +149,15 @@ function useDragGesture(config, callbacks) {
190
149
  const currentSequence = dragSequence.value;
191
150
  // Cancel any existing animation before starting the return animation
192
151
  (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';
152
+ 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) => {
153
+ "worklet";
195
154
  // Guard: Only reset if this is still the same drag operation
196
155
  if (dragSequence.value !== currentSequence)
197
156
  return;
198
157
  if (finished) {
199
158
  globalIsDragging.value = false;
200
159
  draggedIndex.value = -1;
201
- draggedItemId.value = '';
160
+ draggedItemId.value = "";
202
161
  currentTranslateY.value = 0;
203
162
  dragStartScrollOffset.value = 0;
204
163
  measuredItemHeight.value = 0;
@@ -221,14 +180,14 @@ function useDragGesture(config, callbacks) {
221
180
  ]);
222
181
  // Stable haptic callback for drag start
223
182
  const triggerLightHaptic = (0, react_1.useCallback)(() => {
224
- onHapticFeedback?.('light');
183
+ onHapticFeedback?.("light");
225
184
  }, [onHapticFeedback]);
226
185
  // Pan gesture for drag-to-reorder
227
186
  const panGesture = (0, react_1.useMemo)(() => react_native_gesture_handler_1.Gesture.Pan()
228
187
  .activateAfterLongPress(dragConfig.longPressDuration)
229
188
  .enabled(enabled)
230
189
  .onStart(() => {
231
- 'worklet';
190
+ "worklet";
232
191
  // Reset any pending drop state from previous drag
233
192
  // This is critical: if a new drag starts before the previous drop's
234
193
  // fallback completed, isDropping would still be true, causing shift
@@ -279,8 +238,8 @@ function useDragGesture(config, callbacks) {
279
238
  });
280
239
  (0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
281
240
  })
282
- .onUpdate(event => {
283
- 'worklet';
241
+ .onUpdate((event) => {
242
+ "worklet";
284
243
  // Always update translateY for smooth visual feedback
285
244
  translateY.value = event.translationY;
286
245
  // Performance optimization: Only update currentTranslateY (which triggers
@@ -300,7 +259,7 @@ function useDragGesture(config, callbacks) {
300
259
  const topEdge = dragConfig.edgeThreshold;
301
260
  const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
302
261
  if (fingerInList < topEdge && scrollOffset.value > 0) {
303
- const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], 'clamp');
262
+ const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], "clamp");
304
263
  const newOffset = Math.max(0, scrollOffset.value - speed);
305
264
  scrollOffset.value = newOffset;
306
265
  // Throttle actual scroll calls to reduce JS thread pressure
@@ -312,7 +271,7 @@ function useDragGesture(config, callbacks) {
312
271
  else if (fingerInList > bottomEdge) {
313
272
  const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
314
273
  if (scrollOffset.value < maxOffset) {
315
- const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], 'clamp');
274
+ const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], "clamp");
316
275
  const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
317
276
  scrollOffset.value = newOffset;
318
277
  // Throttle actual scroll calls to reduce JS thread pressure
@@ -323,8 +282,8 @@ function useDragGesture(config, callbacks) {
323
282
  }
324
283
  }
325
284
  })
326
- .onEnd(event => {
327
- 'worklet';
285
+ .onEnd((event) => {
286
+ "worklet";
328
287
  isDragging.value = false;
329
288
  // Include scroll compensation in final position calculation
330
289
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
@@ -344,6 +303,9 @@ function useDragGesture(config, callbacks) {
344
303
  const positionDelta = Math.round(finalY / itemHeight);
345
304
  const newIdx = Math.max(0, Math.min(total - 1, currentIdx + positionDelta));
346
305
  if (positionDelta !== 0 && newIdx !== currentIdx) {
306
+ // Mark drop phase immediately on UI thread to prevent non-dragged items
307
+ // from briefly animating back to 0 before the JS drop handler runs.
308
+ isDropping.value = true;
347
309
  // Update registry directly on UI thread
348
310
  const registry = itemIndexRegistry.value;
349
311
  const newRegistry = {};
@@ -378,14 +340,14 @@ function useDragGesture(config, callbacks) {
378
340
  (0, react_native_worklets_1.scheduleOnRN)(handleDragEnd, finalY);
379
341
  })
380
342
  .onFinalize((_event, success) => {
381
- 'worklet';
343
+ "worklet";
382
344
  if (!success) {
383
345
  isDragging.value = false;
384
346
  translateY.value = (0, react_native_reanimated_1.withTiming)(0, { duration: 150 });
385
347
  isDropping.value = false;
386
348
  globalIsDragging.value = false;
387
349
  draggedIndex.value = -1;
388
- draggedItemId.value = '';
350
+ draggedItemId.value = "";
389
351
  currentTranslateY.value = 0;
390
352
  draggedScale.value = 1;
391
353
  dragStartScrollOffset.value = 0;
@@ -393,6 +355,15 @@ function useDragGesture(config, callbacks) {
393
355
  }
394
356
  }),
395
357
  // eslint-disable-next-line react-hooks/exhaustive-deps
396
- [enabled, containerRef, triggerLightHaptic, handleDragEnd, index, itemId, dragStartIndexSnapshot, itemIndexRegistry]);
358
+ [
359
+ enabled,
360
+ containerRef,
361
+ triggerLightHaptic,
362
+ handleDragEnd,
363
+ index,
364
+ itemId,
365
+ dragStartIndexSnapshot,
366
+ itemIndexRegistry,
367
+ ]);
397
368
  return { panGesture, isDragging, translateY };
398
369
  }
@@ -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,CAgK3E"}