@souscheflabs/reanimated-flashlist 0.2.0 → 0.2.3

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.
@@ -33,8 +33,6 @@ export interface DragStateContextValue {
33
33
  visibleHeight: SharedValue<number>;
34
34
  /** Y position of FlashList top on screen (for autoscroll coordinate conversion) */
35
35
  listTopY: SharedValue<number>;
36
- /** Counter incremented on every drag state change to force useDerivedValue re-evaluation */
37
- dragUpdateTrigger: SharedValue<number>;
38
36
  /** Measured height of the dragged item (for dynamic height calculations) */
39
37
  measuredItemHeight: SharedValue<number>;
40
38
  /** Flag to freeze shift values during drop transition */
@@ -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,4FAA4F;IAC5F,iBAAiB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,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,CA2G9D,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,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"}
@@ -102,8 +102,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
102
102
  const contentHeight = (0, react_native_reanimated_1.useSharedValue)(0);
103
103
  const visibleHeight = (0, react_native_reanimated_1.useSharedValue)(0);
104
104
  const listTopY = (0, react_native_reanimated_1.useSharedValue)(0);
105
- // Counter to force useDerivedValue re-evaluation on every drag state change
106
- const dragUpdateTrigger = (0, react_native_reanimated_1.useSharedValue)(0);
107
105
  // Measured height of dragged item (0 = use fallback from config)
108
106
  const measuredItemHeight = (0, react_native_reanimated_1.useSharedValue)(0);
109
107
  // Flag to freeze shift values during drop transition
@@ -142,7 +140,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
142
140
  contentHeight,
143
141
  visibleHeight,
144
142
  listTopY,
145
- dragUpdateTrigger,
146
143
  measuredItemHeight,
147
144
  isDropping,
148
145
  setListRef,
@@ -160,7 +157,6 @@ const DragStateProvider = ({ children, config: configOverrides, }) => {
160
157
  contentHeight,
161
158
  visibleHeight,
162
159
  listTopY,
163
- dragUpdateTrigger,
164
160
  measuredItemHeight,
165
161
  isDropping,
166
162
  setListRef,
@@ -1 +1 @@
1
- {"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE3E;;GAEG;AACH,UAAU,0BAA0B;IAClC,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,SAAS,0BAA0B;;;;;;;;;;;6BAyEpB,kBAAkB,cACjB,MAAM,IAAI,WACd,mBAAmB;;CAqDhC,CAAC"}
1
+ {"version":3,"file":"useListExitAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListExitAnimation.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE3E;;GAEG;AACH,UAAU,0BAA0B;IAClC,uDAAuD;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qDAAqD;IACrD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAQD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,oBAAoB,GAC/B,QAAQ,MAAM,EACd,SAAS,0BAA0B;;;;;;;;;;;6BAyEpB,kBAAkB,cACjB,MAAM,IAAI,WACd,mBAAmB;;CAwDhC,CAAC"}
@@ -126,6 +126,8 @@ const useListExitAnimation = (itemId, config) => {
126
126
  // Set animation config SharedValues BEFORE starting animation
127
127
  slideDistance.value = animConfig.slide.distance;
128
128
  scaleToValue.value = animConfig.scale.toValue;
129
+ // Cancel any existing animation before starting the exit animation
130
+ (0, react_native_reanimated_1.cancelAnimation)(exitDirection);
129
131
  // Animate exitDirection.value from 0 to 1 (or -1)
130
132
  exitDirection.value = (0, react_native_reanimated_1.withTiming)(direction, { duration: animConfig.slide.duration, easing: animations_1.standardEasing }, finished => {
131
133
  'worklet';
@@ -1 +1 @@
1
- {"version":3,"file":"useDragGesture.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragGesture.ts"],"names":[],"mappings":"AAYA,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,CA0NtB"}
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,CAuPtB"}
@@ -40,7 +40,11 @@ function useDragGesture(config, callbacks) {
40
40
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
41
41
  const translateY = (0, react_native_reanimated_1.useSharedValue)(0);
42
42
  // Global drag state for coordinating animations across all items
43
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, dragUpdateTrigger, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
43
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, draggedScale, scrollOffset, dragStartScrollOffset, contentHeight, visibleHeight, listTopY, measuredItemHeight, isDropping, scrollToOffset, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
44
+ // Performance optimization: Track last significant Y position and scroll time
45
+ // to avoid updating on every pixel movement
46
+ const lastSignificantY = (0, react_native_reanimated_1.useSharedValue)(0);
47
+ const lastScrollTime = (0, react_native_reanimated_1.useSharedValue)(0);
44
48
  // Store current values in refs for stable gesture callbacks
45
49
  const dragContextRef = (0, react_1.useRef)({
46
50
  index,
@@ -78,6 +82,8 @@ function useDragGesture(config, callbacks) {
78
82
  }
79
83
  else {
80
84
  // Same position - animate back and reset state
85
+ // Cancel any existing animation before starting the return animation
86
+ (0, react_native_reanimated_1.cancelAnimation)(translateY);
81
87
  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 => {
82
88
  'worklet';
83
89
  if (finished) {
@@ -87,7 +93,6 @@ function useDragGesture(config, callbacks) {
87
93
  currentTranslateY.value = 0;
88
94
  dragStartScrollOffset.value = 0;
89
95
  measuredItemHeight.value = 0;
90
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
91
96
  }
92
97
  });
93
98
  }
@@ -100,7 +105,6 @@ function useDragGesture(config, callbacks) {
100
105
  draggedItemId,
101
106
  currentTranslateY,
102
107
  dragStartScrollOffset,
103
- dragUpdateTrigger,
104
108
  isDropping,
105
109
  onHapticFeedback,
106
110
  ]);
@@ -127,19 +131,32 @@ function useDragGesture(config, callbacks) {
127
131
  draggedItemId.value = itemId;
128
132
  dragStartScrollOffset.value = scrollOffset.value;
129
133
  currentTranslateY.value = 0;
134
+ // Reset performance tracking values
135
+ lastSignificantY.value = 0;
136
+ lastScrollTime.value = Date.now();
130
137
  draggedScale.value = (0, react_native_reanimated_1.withSpring)(dragConfig.dragScale, {
131
138
  damping: 15,
132
139
  stiffness: 400,
133
140
  });
134
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
135
141
  (0, react_native_worklets_1.scheduleOnRN)(triggerLightHaptic);
136
142
  })
137
143
  .onUpdate(event => {
138
144
  'worklet';
145
+ // Always update translateY for smooth visual feedback
139
146
  translateY.value = event.translationY;
140
- currentTranslateY.value = event.translationY;
141
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
147
+ // Performance optimization: Only update currentTranslateY (which triggers
148
+ // shift calculations in other items) when movement is significant (>5px)
149
+ const POSITION_THRESHOLD = 5;
150
+ const deltaFromLast = Math.abs(event.translationY - lastSignificantY.value);
151
+ if (deltaFromLast >= POSITION_THRESHOLD) {
152
+ currentTranslateY.value = event.translationY;
153
+ lastSignificantY.value = event.translationY;
154
+ }
142
155
  // Autoscroll when dragging near edges
156
+ // Performance optimization: Throttle to ~30fps (33ms interval)
157
+ const SCROLL_THROTTLE_MS = 33;
158
+ const now = Date.now();
159
+ const timeSinceLastScroll = now - lastScrollTime.value;
143
160
  const fingerInList = event.absoluteY - listTopY.value;
144
161
  const topEdge = dragConfig.edgeThreshold;
145
162
  const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
@@ -147,7 +164,11 @@ function useDragGesture(config, callbacks) {
147
164
  const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [0, topEdge], [dragConfig.maxScrollSpeed, 0], 'clamp');
148
165
  const newOffset = Math.max(0, scrollOffset.value - speed);
149
166
  scrollOffset.value = newOffset;
150
- (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
167
+ // Throttle actual scroll calls to reduce JS thread pressure
168
+ if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
169
+ lastScrollTime.value = now;
170
+ (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
171
+ }
151
172
  }
152
173
  else if (fingerInList > bottomEdge) {
153
174
  const maxOffset = Math.max(0, contentHeight.value - visibleHeight.value);
@@ -155,7 +176,11 @@ function useDragGesture(config, callbacks) {
155
176
  const speed = (0, react_native_reanimated_1.interpolate)(fingerInList, [bottomEdge, visibleHeight.value], [0, dragConfig.maxScrollSpeed], 'clamp');
156
177
  const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
157
178
  scrollOffset.value = newOffset;
158
- (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
179
+ // Throttle actual scroll calls to reduce JS thread pressure
180
+ if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
181
+ lastScrollTime.value = now;
182
+ (0, react_native_worklets_1.scheduleOnRN)(scrollToOffset, newOffset);
183
+ }
159
184
  }
160
185
  }
161
186
  })
@@ -1 +1 @@
1
- {"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CA8G3E"}
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"}
@@ -25,29 +25,15 @@ const constants_1 = require("../../constants");
25
25
  function useDragShift(config) {
26
26
  const { itemId, index } = config;
27
27
  // Global drag state for coordinating animations across all items
28
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, dragUpdateTrigger, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
28
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, currentTranslateY, scrollOffset, dragStartScrollOffset, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
29
29
  // Shift animation SharedValue
30
30
  const shiftY = (0, react_native_reanimated_1.useSharedValue)(0);
31
31
  // Calculate target shift using useDerivedValue
32
+ // Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
33
+ // so we don't need a manual trigger counter. When any read SharedValue changes,
34
+ // Reanimated will re-execute this derived value.
32
35
  const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
33
36
  'worklet';
34
- /**
35
- * FORCE RE-EVALUATION PATTERN:
36
- * useDerivedValue only re-runs when its dependencies change. However, with
37
- * SharedValues from context, React can't track changes automatically.
38
- *
39
- * By reading dragUpdateTrigger.value (which is incremented on every drag
40
- * state change), we force this derived value to recompute. This pattern
41
- * is necessary because:
42
- * 1. SharedValue changes don't trigger React re-renders
43
- * 2. useDerivedValue caches its result based on tracked values
44
- * 3. We need fresh calculations on every drag position update
45
- *
46
- * The ESLint disable is required because the value appears "unused" but
47
- * reading it creates a dependency that Reanimated tracks.
48
- */
49
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
50
- dragUpdateTrigger.value;
51
37
  // During drop transition, freeze shift values
52
38
  if (isDropping.value) {
53
39
  return shiftY.value;
@@ -77,7 +63,16 @@ function useDragShift(config) {
77
63
  : effectiveTranslateY < 0
78
64
  ? -constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
79
65
  : 0;
80
- const hoveredIndex = currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
66
+ const hoveredIndex = currentDraggedIndex +
67
+ Math.round(effectiveTranslateY / itemHeight + offset);
68
+ // Performance optimization: Spatial filtering
69
+ // Only items in the affected range need to calculate shift
70
+ // Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
71
+ const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
72
+ const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
73
+ if (index < minAffectedIndex || index > maxAffectedIndex) {
74
+ return 0;
75
+ }
81
76
  // Moving DOWN: items between original and hovered positions shift UP
82
77
  if (hoveredIndex > currentDraggedIndex) {
83
78
  if (index > currentDraggedIndex && index <= hoveredIndex) {
@@ -96,6 +91,9 @@ function useDragShift(config) {
96
91
  (0, react_native_reanimated_1.useAnimatedReaction)(() => targetShiftY.value, (target, prev) => {
97
92
  'worklet';
98
93
  if (target !== prev) {
94
+ // Cancel any in-flight animation before starting a new one
95
+ // This prevents competing animations and visual glitches
96
+ (0, react_native_reanimated_1.cancelAnimation)(shiftY);
99
97
  shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
100
98
  duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
101
99
  easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
@@ -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,CAyG3E"}
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,CAqG3E"}
@@ -35,7 +35,7 @@ function useDropCompensation(config) {
35
35
  // Track index on UI thread for synchronized shift reset
36
36
  const indexShared = (0, react_native_reanimated_1.useSharedValue)(index);
37
37
  // Global drag state
38
- const { isDragging: globalIsDragging, draggedIndex, draggedItemId, measuredItemHeight, isDropping, dragUpdateTrigger, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
38
+ const { isDragging: globalIsDragging, draggedIndex, draggedItemId, measuredItemHeight, isDropping, config: dragConfig, } = (0, DragStateContext_1.useDragState)();
39
39
  // Use FlashList's useRecyclingState for automatic reset on view recycling
40
40
  const [prevIndex, setPrevIndex] = (0, flash_list_1.useRecyclingState)(index, [itemId]);
41
41
  // Handle index changes after data updates (drop compensation)
@@ -74,9 +74,6 @@ function useDropCompensation(config) {
74
74
  draggedIndex.value = -1;
75
75
  draggedItemId.value = '';
76
76
  measuredItemHeight.value = 0;
77
- // Issue 5 Fix: Use direct assignment instead of withTiming for counter
78
- // withTiming on a counter can cause timing issues and potential double-increment
79
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
80
77
  }
81
78
  });
82
79
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@souscheflabs/reanimated-flashlist",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
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",
@@ -42,8 +42,6 @@ export interface DragStateContextValue {
42
42
  visibleHeight: SharedValue<number>;
43
43
  /** Y position of FlashList top on screen (for autoscroll coordinate conversion) */
44
44
  listTopY: SharedValue<number>;
45
- /** Counter incremented on every drag state change to force useDerivedValue re-evaluation */
46
- dragUpdateTrigger: SharedValue<number>;
47
45
  /** Measured height of the dragged item (for dynamic height calculations) */
48
46
  measuredItemHeight: SharedValue<number>;
49
47
  /** Flag to freeze shift values during drop transition */
@@ -152,9 +150,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
152
150
  const visibleHeight = useSharedValue(0);
153
151
  const listTopY = useSharedValue(0);
154
152
 
155
- // Counter to force useDerivedValue re-evaluation on every drag state change
156
- const dragUpdateTrigger = useSharedValue(0);
157
-
158
153
  // Measured height of dragged item (0 = use fallback from config)
159
154
  const measuredItemHeight = useSharedValue(0);
160
155
 
@@ -200,7 +195,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
200
195
  contentHeight,
201
196
  visibleHeight,
202
197
  listTopY,
203
- dragUpdateTrigger,
204
198
  measuredItemHeight,
205
199
  isDropping,
206
200
  setListRef,
@@ -219,7 +213,6 @@ export const DragStateProvider: React.FC<DragStateProviderProps> = ({
219
213
  contentHeight,
220
214
  visibleHeight,
221
215
  listTopY,
222
- dragUpdateTrigger,
223
216
  measuredItemHeight,
224
217
  isDropping,
225
218
  setListRef,
@@ -3,6 +3,7 @@ import {
3
3
  useAnimatedStyle,
4
4
  useSharedValue,
5
5
  withTiming,
6
+ cancelAnimation,
6
7
  } from 'react-native-reanimated';
7
8
  import { scheduleOnRN } from 'react-native-worklets';
8
9
  import {
@@ -171,6 +172,9 @@ export const useListExitAnimation = (
171
172
  slideDistance.value = animConfig.slide.distance;
172
173
  scaleToValue.value = animConfig.scale.toValue;
173
174
 
175
+ // Cancel any existing animation before starting the exit animation
176
+ cancelAnimation(exitDirection);
177
+
174
178
  // Animate exitDirection.value from 0 to 1 (or -1)
175
179
  exitDirection.value = withTiming(
176
180
  direction,
@@ -4,6 +4,7 @@ import {
4
4
  measure,
5
5
  withSpring,
6
6
  withTiming,
7
+ cancelAnimation,
7
8
  Easing,
8
9
  interpolate,
9
10
  } from 'react-native-reanimated';
@@ -66,13 +67,17 @@ export function useDragGesture(
66
67
  contentHeight,
67
68
  visibleHeight,
68
69
  listTopY,
69
- dragUpdateTrigger,
70
70
  measuredItemHeight,
71
71
  isDropping,
72
72
  scrollToOffset,
73
73
  config: dragConfig,
74
74
  } = useDragState();
75
75
 
76
+ // Performance optimization: Track last significant Y position and scroll time
77
+ // to avoid updating on every pixel movement
78
+ const lastSignificantY = useSharedValue(0);
79
+ const lastScrollTime = useSharedValue(0);
80
+
76
81
  // Store current values in refs for stable gesture callbacks
77
82
  const dragContextRef = useRef({
78
83
  index,
@@ -127,6 +132,8 @@ export function useDragGesture(
127
132
  dragStartScrollOffset.value = 0;
128
133
  } else {
129
134
  // Same position - animate back and reset state
135
+ // Cancel any existing animation before starting the return animation
136
+ cancelAnimation(translateY);
130
137
  translateY.value = withTiming(
131
138
  0,
132
139
  { duration: 150, easing: Easing.out(Easing.ease) },
@@ -139,7 +146,6 @@ export function useDragGesture(
139
146
  currentTranslateY.value = 0;
140
147
  dragStartScrollOffset.value = 0;
141
148
  measuredItemHeight.value = 0;
142
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
143
149
  }
144
150
  },
145
151
  );
@@ -154,7 +160,6 @@ export function useDragGesture(
154
160
  draggedItemId,
155
161
  currentTranslateY,
156
162
  dragStartScrollOffset,
157
- dragUpdateTrigger,
158
163
  isDropping,
159
164
  onHapticFeedback,
160
165
  ],
@@ -187,20 +192,37 @@ export function useDragGesture(
187
192
  draggedItemId.value = itemId;
188
193
  dragStartScrollOffset.value = scrollOffset.value;
189
194
  currentTranslateY.value = 0;
195
+ // Reset performance tracking values
196
+ lastSignificantY.value = 0;
197
+ lastScrollTime.value = Date.now();
190
198
  draggedScale.value = withSpring(dragConfig.dragScale, {
191
199
  damping: 15,
192
200
  stiffness: 400,
193
201
  });
194
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
195
202
  scheduleOnRN(triggerLightHaptic);
196
203
  })
197
204
  .onUpdate(event => {
198
205
  'worklet';
206
+ // Always update translateY for smooth visual feedback
199
207
  translateY.value = event.translationY;
200
- currentTranslateY.value = event.translationY;
201
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
208
+
209
+ // Performance optimization: Only update currentTranslateY (which triggers
210
+ // shift calculations in other items) when movement is significant (>5px)
211
+ const POSITION_THRESHOLD = 5;
212
+ const deltaFromLast = Math.abs(
213
+ event.translationY - lastSignificantY.value,
214
+ );
215
+ if (deltaFromLast >= POSITION_THRESHOLD) {
216
+ currentTranslateY.value = event.translationY;
217
+ lastSignificantY.value = event.translationY;
218
+ }
202
219
 
203
220
  // Autoscroll when dragging near edges
221
+ // Performance optimization: Throttle to ~30fps (33ms interval)
222
+ const SCROLL_THROTTLE_MS = 33;
223
+ const now = Date.now();
224
+ const timeSinceLastScroll = now - lastScrollTime.value;
225
+
204
226
  const fingerInList = event.absoluteY - listTopY.value;
205
227
  const topEdge = dragConfig.edgeThreshold;
206
228
  const bottomEdge = visibleHeight.value - dragConfig.edgeThreshold;
@@ -214,7 +236,11 @@ export function useDragGesture(
214
236
  );
215
237
  const newOffset = Math.max(0, scrollOffset.value - speed);
216
238
  scrollOffset.value = newOffset;
217
- scheduleOnRN(scrollToOffset, newOffset);
239
+ // Throttle actual scroll calls to reduce JS thread pressure
240
+ if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
241
+ lastScrollTime.value = now;
242
+ scheduleOnRN(scrollToOffset, newOffset);
243
+ }
218
244
  } else if (fingerInList > bottomEdge) {
219
245
  const maxOffset = Math.max(
220
246
  0,
@@ -229,7 +255,11 @@ export function useDragGesture(
229
255
  );
230
256
  const newOffset = Math.min(maxOffset, scrollOffset.value + speed);
231
257
  scrollOffset.value = newOffset;
232
- scheduleOnRN(scrollToOffset, newOffset);
258
+ // Throttle actual scroll calls to reduce JS thread pressure
259
+ if (timeSinceLastScroll >= SCROLL_THROTTLE_MS) {
260
+ lastScrollTime.value = now;
261
+ scheduleOnRN(scrollToOffset, newOffset);
262
+ }
233
263
  }
234
264
  }
235
265
  })
@@ -3,6 +3,7 @@ import {
3
3
  useDerivedValue,
4
4
  useAnimatedReaction,
5
5
  withTiming,
6
+ cancelAnimation,
6
7
  Easing,
7
8
  } from 'react-native-reanimated';
8
9
  import { useDragState } from '../../contexts/DragStateContext';
@@ -38,7 +39,6 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
38
39
  currentTranslateY,
39
40
  scrollOffset,
40
41
  dragStartScrollOffset,
41
- dragUpdateTrigger,
42
42
  measuredItemHeight,
43
43
  isDropping,
44
44
  config: dragConfig,
@@ -48,26 +48,11 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
48
48
  const shiftY = useSharedValue(0);
49
49
 
50
50
  // Calculate target shift using useDerivedValue
51
+ // Note: useDerivedValue automatically tracks SharedValue dependencies on UI thread,
52
+ // so we don't need a manual trigger counter. When any read SharedValue changes,
53
+ // Reanimated will re-execute this derived value.
51
54
  const targetShiftY = useDerivedValue(() => {
52
55
  'worklet';
53
- /**
54
- * FORCE RE-EVALUATION PATTERN:
55
- * useDerivedValue only re-runs when its dependencies change. However, with
56
- * SharedValues from context, React can't track changes automatically.
57
- *
58
- * By reading dragUpdateTrigger.value (which is incremented on every drag
59
- * state change), we force this derived value to recompute. This pattern
60
- * is necessary because:
61
- * 1. SharedValue changes don't trigger React re-renders
62
- * 2. useDerivedValue caches its result based on tracked values
63
- * 3. We need fresh calculations on every drag position update
64
- *
65
- * The ESLint disable is required because the value appears "unused" but
66
- * reading it creates a dependency that Reanimated tracks.
67
- */
68
- // eslint-disable-next-line @typescript-eslint/no-unused-expressions
69
- dragUpdateTrigger.value;
70
-
71
56
  // During drop transition, freeze shift values
72
57
  if (isDropping.value) {
73
58
  return shiftY.value;
@@ -104,7 +89,17 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
104
89
  ? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
105
90
  : 0;
106
91
  const hoveredIndex =
107
- currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
92
+ currentDraggedIndex +
93
+ Math.round(effectiveTranslateY / itemHeight + offset);
94
+
95
+ // Performance optimization: Spatial filtering
96
+ // Only items in the affected range need to calculate shift
97
+ // Items outside [min(dragIdx, hoverIdx), max(dragIdx, hoverIdx)] can bail early
98
+ const minAffectedIndex = Math.min(currentDraggedIndex, hoveredIndex);
99
+ const maxAffectedIndex = Math.max(currentDraggedIndex, hoveredIndex);
100
+ if (index < minAffectedIndex || index > maxAffectedIndex) {
101
+ return 0;
102
+ }
108
103
 
109
104
  // Moving DOWN: items between original and hovered positions shift UP
110
105
  if (hoveredIndex > currentDraggedIndex) {
@@ -128,6 +123,9 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
128
123
  (target, prev) => {
129
124
  'worklet';
130
125
  if (target !== prev) {
126
+ // Cancel any in-flight animation before starting a new one
127
+ // This prevents competing animations and visual glitches
128
+ cancelAnimation(shiftY);
131
129
  shiftY.value = withTiming(target, {
132
130
  duration: ANIMATION_TIMING.SHIFT_DURATION,
133
131
  easing: Easing.out(Easing.ease),
@@ -47,7 +47,6 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
47
47
  draggedItemId,
48
48
  measuredItemHeight,
49
49
  isDropping,
50
- dragUpdateTrigger,
51
50
  config: dragConfig,
52
51
  } = useDragState();
53
52
 
@@ -99,9 +98,6 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
99
98
  draggedIndex.value = -1;
100
99
  draggedItemId.value = '';
101
100
  measuredItemHeight.value = 0;
102
- // Issue 5 Fix: Use direct assignment instead of withTiming for counter
103
- // withTiming on a counter can cause timing issues and potential double-increment
104
- dragUpdateTrigger.value = dragUpdateTrigger.value + 1;
105
101
  }
106
102
  },
107
103
  );