@souscheflabs/reanimated-flashlist 0.1.7 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +6 -6
  2. package/lib/AnimatedFlashListItem.d.ts.map +1 -1
  3. package/lib/AnimatedFlashListItem.js +3 -8
  4. package/lib/constants/animations.d.ts.map +1 -1
  5. package/lib/constants/animations.js +4 -2
  6. package/lib/constants/drag.d.ts +38 -0
  7. package/lib/constants/drag.d.ts.map +1 -1
  8. package/lib/constants/drag.js +48 -1
  9. package/lib/contexts/DragStateContext.d.ts +0 -10
  10. package/lib/contexts/DragStateContext.d.ts.map +1 -1
  11. package/lib/contexts/DragStateContext.js +29 -2
  12. package/lib/contexts/ListAnimationContext.d.ts +5 -2
  13. package/lib/contexts/ListAnimationContext.d.ts.map +1 -1
  14. package/lib/contexts/ListAnimationContext.js +8 -9
  15. package/lib/hooks/animations/useListEntryAnimation.d.ts.map +1 -1
  16. package/lib/hooks/animations/useListEntryAnimation.js +3 -3
  17. package/lib/hooks/animations/useListExitAnimation.d.ts.map +1 -1
  18. package/lib/hooks/animations/useListExitAnimation.js +5 -1
  19. package/lib/hooks/drag/useDragAnimatedStyle.d.ts.map +1 -1
  20. package/lib/hooks/drag/useDragAnimatedStyle.js +31 -4
  21. package/lib/hooks/drag/useDragShift.d.ts.map +1 -1
  22. package/lib/hooks/drag/useDragShift.js +24 -3
  23. package/lib/hooks/drag/useDropCompensation.d.ts.map +1 -1
  24. package/lib/hooks/drag/useDropCompensation.js +28 -9
  25. package/lib/index.d.ts +2 -2
  26. package/lib/index.js +2 -2
  27. package/package.json +1 -1
  28. package/src/AnimatedFlashListItem.tsx +4 -10
  29. package/src/__tests__/contexts/ListAnimationContext.test.tsx +3 -3
  30. package/src/constants/animations.ts +4 -2
  31. package/src/constants/drag.ts +52 -0
  32. package/src/contexts/DragStateContext.tsx +45 -5
  33. package/src/contexts/ListAnimationContext.tsx +18 -11
  34. package/src/hooks/animations/useListEntryAnimation.ts +9 -6
  35. package/src/hooks/animations/useListExitAnimation.ts +6 -1
  36. package/src/hooks/drag/useDragAnimatedStyle.ts +45 -10
  37. package/src/hooks/drag/useDragShift.ts +24 -3
  38. package/src/hooks/drag/useDropCompensation.ts +31 -9
  39. package/src/index.ts +2 -2
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @souschef/reanimated-flashlist
1
+ # @souscheflabs/reanimated-flashlist
2
2
 
3
3
  A high-performance animated FlashList with drag-to-reorder and entry/exit animations for React Native.
4
4
 
@@ -15,7 +15,7 @@ A high-performance animated FlashList with drag-to-reorder and entry/exit animat
15
15
 
16
16
  ### From npm (when published)
17
17
  ```bash
18
- npm install @souschef/reanimated-flashlist
18
+ npm install @souscheflabs/reanimated-flashlist
19
19
  ```
20
20
 
21
21
  ### From GitHub (private repo)
@@ -34,7 +34,7 @@ Or add to `package.json`:
34
34
  ```json
35
35
  {
36
36
  "dependencies": {
37
- "@souschef/reanimated-flashlist": "github:SousChefLabs/reanimated-flashlist#v0.1.1"
37
+ "@souscheflabs/reanimated-flashlist": "github:SousChefLabs/reanimated-flashlist#v0.1.1"
38
38
  }
39
39
  }
40
40
  ```
@@ -57,7 +57,7 @@ npm install @shopify/flash-list react-native-reanimated react-native-gesture-han
57
57
  ## Quick Start
58
58
 
59
59
  ```tsx
60
- import { AnimatedFlashList, type AnimatedListItem } from '@souschef/reanimated-flashlist';
60
+ import { AnimatedFlashList, type AnimatedListItem } from '@souscheflabs/reanimated-flashlist';
61
61
  import { GestureDetector } from 'react-native-gesture-handler';
62
62
 
63
63
  interface MyItem extends AnimatedListItem {
@@ -261,7 +261,7 @@ import {
261
261
  useDragAnimatedStyle,
262
262
  useListExitAnimation,
263
263
  useListEntryAnimation,
264
- } from '@souschef/reanimated-flashlist';
264
+ } from '@souscheflabs/reanimated-flashlist';
265
265
  ```
266
266
 
267
267
  ### Context Providers
@@ -274,7 +274,7 @@ import {
274
274
  ListAnimationProvider,
275
275
  useDragState,
276
276
  useListAnimation,
277
- } from '@souschef/reanimated-flashlist';
277
+ } from '@souscheflabs/reanimated-flashlist';
278
278
  ```
279
279
 
280
280
  ## License
@@ -1 +1 @@
1
- {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAwJ3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
1
+ {"version":3,"file":"AnimatedFlashListItem.d.ts","sourceRoot":"","sources":["../src/AnimatedFlashListItem.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAwD,MAAM,OAAO,CAAC;AAY7E,OAAO,KAAK,EACV,gBAAgB,EAChB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,SAAS,CAAC;AAEjB,UAAU,0BAA0B,CAAC,CAAC,SAAS,gBAAgB;IAC7D,oBAAoB;IACpB,IAAI,EAAE,CAAC,CAAC;IACR,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,4BAA4B;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,4CAA4C;IAC5C,aAAa,EAAE,OAAO,CAAC;IACvB,kCAAkC;IAClC,UAAU,EAAE,CAAC,IAAI,EAAE,sBAAsB,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,YAAY,CAAC;IACpE,mCAAmC;IACnC,gBAAgB,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3D,wCAAwC;IACxC,gBAAgB,CAAC,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,IAAI,CAAC;CACvD;AAED;;;;;;;;;;GAUG;AACH,iBAAS,0BAA0B,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC9D,IAAI,EACJ,KAAK,EACL,UAAU,EACV,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,GAAG,IAAI,CAkJ3D;AAGD,eAAO,MAAM,qBAAqB,EAE7B,OAAO,0BAA0B,CAAC"}
@@ -102,12 +102,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
102
102
  const handleLayout = (0, react_1.useCallback)((event) => {
103
103
  measuredHeightRef.current = event.nativeEvent.layout.height;
104
104
  }, []);
105
- // Create combined animated style
106
- const combinedAnimatedStyle = (0, react_1.useMemo)(() => {
107
- // We can't directly combine animated styles here since they're worklet-based
108
- // Instead, we'll let the consumer apply them via the render prop
109
- return {};
110
- }, []);
111
105
  // Create drag handle props
112
106
  const dragHandleProps = (0, react_1.useMemo)(() => isDragEnabled
113
107
  ? {
@@ -120,11 +114,13 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
120
114
  triggerExit(direction, onComplete, preset);
121
115
  }, [triggerExit]);
122
116
  // Create render info
117
+ // Note: animatedStyle is empty because animations are applied to the wrapper View automatically.
118
+ // This field is kept for backward compatibility with the AnimatedRenderItemInfo interface.
123
119
  const renderInfo = (0, react_1.useMemo)(() => ({
124
120
  item,
125
121
  index,
126
122
  totalItems,
127
- animatedStyle: combinedAnimatedStyle,
123
+ animatedStyle: {},
128
124
  dragHandleProps,
129
125
  isDragging: false, // This is a SharedValue, consumer should use dragHandleProps.isDragging
130
126
  isDragEnabled,
@@ -134,7 +130,6 @@ function AnimatedFlashListItemInner({ item, index, totalItems, isDragEnabled, re
134
130
  item,
135
131
  index,
136
132
  totalItems,
137
- combinedAnimatedStyle,
138
133
  dragHandleProps,
139
134
  isDragEnabled,
140
135
  triggerExitAnimation,
@@ -1 +1 @@
1
- {"version":3,"file":"animations.d.ts","sourceRoot":"","sources":["../../src/constants/animations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,cAAc,yDAAoC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,mBAkBpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,mBAkBjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,oBAGrC,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,GAAE,SAAS,GAAG,MAAe,EACnC,SAAS,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACvC,mBAAmB,CAWrB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACxC,oBAAoB,CAOtB"}
1
+ {"version":3,"file":"animations.d.ts","sourceRoot":"","sources":["../../src/constants/animations.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAE1E;;;GAGG;AACH,eAAO,MAAM,cAAc,yDAAoC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,EAAE,mBAkBpC,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,mBAAmB,EAAE,mBAoBjC,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,EAAE,oBAGrC,CAAC;AAEF;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,GAAE,SAAS,GAAG,MAAe,EACnC,SAAS,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,GACvC,mBAAmB,CAWrB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CACxC,SAAS,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC,GACxC,oBAAoB,CAOtB"}
@@ -34,7 +34,7 @@ exports.DEFAULT_EXIT_ANIMATION = {
34
34
  },
35
35
  removalDelay: 300,
36
36
  layoutAnimation: {
37
- duration: 200,
37
+ duration: 300,
38
38
  },
39
39
  };
40
40
  /**
@@ -61,7 +61,9 @@ exports.FAST_EXIT_ANIMATION = {
61
61
  },
62
62
  removalDelay: 200,
63
63
  layoutAnimation: {
64
- duration: 150,
64
+ // Slightly longer than the exit animation for a smoother overall effect
65
+ // Items below start moving up as the exiting item is sliding out
66
+ duration: 250,
65
67
  },
66
68
  };
67
69
  /**
@@ -1,4 +1,42 @@
1
1
  import type { DragConfig } from '../types';
2
+ /**
3
+ * Unified animation timing configuration for consistency across all drag hooks.
4
+ * Using consistent timing prevents visual "mismatches" during complex interactions.
5
+ */
6
+ export declare const ANIMATION_TIMING: {
7
+ /** Duration for item shift animations (when items move to make room) */
8
+ readonly SHIFT_DURATION: 100;
9
+ /** Duration for drop settle animation (when dragged item animates to final position) */
10
+ readonly DROP_SETTLE_DURATION: 150;
11
+ /** Duration for shadow opacity fade transition */
12
+ readonly SHADOW_FADE_DURATION: 100;
13
+ /** Duration for non-dragged item compensation animations */
14
+ readonly COMPENSATION_DURATION: 100;
15
+ };
16
+ /**
17
+ * Configurable thresholds for drag calculations.
18
+ * These values control sensitivity and visual behavior.
19
+ */
20
+ export declare const DRAG_THRESHOLDS: {
21
+ /**
22
+ * Offset factor for hover position calculation.
23
+ * This adds a bias when determining which index is being hovered over,
24
+ * making it slightly easier to "cross" into the next/prev position.
25
+ * Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
26
+ */
27
+ readonly HOVER_OFFSET_FACTOR: 0.2;
28
+ /**
29
+ * Z-index for elevated (dragged) items.
30
+ * Should be high enough to appear above other UI but not conflict
31
+ * with modals or overlays (which typically use 1000+).
32
+ */
33
+ readonly ELEVATED_Z_INDEX: 999;
34
+ /**
35
+ * Minimum translateY/shiftY value to consider "significant".
36
+ * Values below this are treated as zero for optimization.
37
+ */
38
+ readonly SHIFT_SIGNIFICANCE_THRESHOLD: 1;
39
+ };
2
40
  /**
3
41
  * Default drag configuration
4
42
  * All values can be overridden via AnimatedFlashList config prop
@@ -1 +1 @@
1
- {"version":3,"file":"drag.d.ts","sourceRoot":"","sources":["../../src/constants/drag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAE3C;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAoCjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAG5E"}
1
+ {"version":3,"file":"drag.d.ts","sourceRoot":"","sources":["../../src/constants/drag.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAM3C;;;GAGG;AACH,eAAO,MAAM,gBAAgB;IAC3B,wEAAwE;;IAExE,wFAAwF;;IAExF,kDAAkD;;IAElD,4DAA4D;;CAEpD,CAAC;AAMX;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;;OAKG;;IAEH;;;;OAIG;;IAEH;;;OAGG;;CAEK,CAAC;AAMX;;;GAGG;AACH,eAAO,MAAM,mBAAmB,EAAE,UAoCjC,CAAC;AAEF;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAG5E"}
@@ -1,7 +1,54 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_DRAG_CONFIG = void 0;
3
+ exports.DEFAULT_DRAG_CONFIG = exports.DRAG_THRESHOLDS = exports.ANIMATION_TIMING = void 0;
4
4
  exports.createDragConfig = createDragConfig;
5
+ // ============================================================================
6
+ // Animation Timing & Easing Constants
7
+ // ============================================================================
8
+ /**
9
+ * Unified animation timing configuration for consistency across all drag hooks.
10
+ * Using consistent timing prevents visual "mismatches" during complex interactions.
11
+ */
12
+ exports.ANIMATION_TIMING = {
13
+ /** Duration for item shift animations (when items move to make room) */
14
+ SHIFT_DURATION: 100,
15
+ /** Duration for drop settle animation (when dragged item animates to final position) */
16
+ DROP_SETTLE_DURATION: 150,
17
+ /** Duration for shadow opacity fade transition */
18
+ SHADOW_FADE_DURATION: 100,
19
+ /** Duration for non-dragged item compensation animations */
20
+ COMPENSATION_DURATION: 100,
21
+ };
22
+ // ============================================================================
23
+ // Threshold Constants
24
+ // ============================================================================
25
+ /**
26
+ * Configurable thresholds for drag calculations.
27
+ * These values control sensitivity and visual behavior.
28
+ */
29
+ exports.DRAG_THRESHOLDS = {
30
+ /**
31
+ * Offset factor for hover position calculation.
32
+ * This adds a bias when determining which index is being hovered over,
33
+ * making it slightly easier to "cross" into the next/prev position.
34
+ * Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
35
+ */
36
+ HOVER_OFFSET_FACTOR: 0.2,
37
+ /**
38
+ * Z-index for elevated (dragged) items.
39
+ * Should be high enough to appear above other UI but not conflict
40
+ * with modals or overlays (which typically use 1000+).
41
+ */
42
+ ELEVATED_Z_INDEX: 999,
43
+ /**
44
+ * Minimum translateY/shiftY value to consider "significant".
45
+ * Values below this are treated as zero for optimization.
46
+ */
47
+ SHIFT_SIGNIFICANCE_THRESHOLD: 1,
48
+ };
49
+ // ============================================================================
50
+ // Default Drag Configuration
51
+ // ============================================================================
5
52
  /**
6
53
  * Default drag configuration
7
54
  * All values can be overridden via AnimatedFlashList config prop
@@ -58,16 +58,6 @@ interface DragStateProviderProps {
58
58
  /** Optional drag configuration overrides */
59
59
  config?: Partial<DragConfig>;
60
60
  }
61
- /**
62
- * Provider that creates shared Reanimated values for drag state.
63
- *
64
- * These values are shared across all list items:
65
- * - The dragged item writes to them during drag gestures
66
- * - Non-dragged items read them to calculate their shift offset
67
- * - Scroll state enables viewport-aware hover calculations
68
- *
69
- * Using SharedValues ensures animations run on the UI thread at 60fps.
70
- */
71
61
  export declare const DragStateProvider: React.FC<DragStateProviderProps>;
72
62
  export {};
73
63
  //# sourceMappingURL=DragStateContext.d.ts.map
@@ -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;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC,sBAAsB,CA0G9D,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,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"}
@@ -60,9 +60,36 @@ exports.useDragState = useDragState;
60
60
  *
61
61
  * Using SharedValues ensures animations run on the UI thread at 60fps.
62
62
  */
63
+ /**
64
+ * Validates drag configuration values to prevent runtime errors.
65
+ * Logs warnings in development for invalid configurations.
66
+ */
67
+ function validateConfig(config) {
68
+ if (__DEV__) {
69
+ if (config.itemHeight <= 0) {
70
+ console.warn('[AnimatedFlashList] Invalid itemHeight: must be positive. Got:', config.itemHeight);
71
+ }
72
+ if (config.longPressDuration <= 0) {
73
+ console.warn('[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:', config.longPressDuration);
74
+ }
75
+ if (config.dragScale <= 0) {
76
+ console.warn('[AnimatedFlashList] Invalid dragScale: must be positive. Got:', config.dragScale);
77
+ }
78
+ if (config.edgeThreshold < 0) {
79
+ console.warn('[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:', config.edgeThreshold);
80
+ }
81
+ if (config.maxScrollSpeed <= 0) {
82
+ console.warn('[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:', config.maxScrollSpeed);
83
+ }
84
+ }
85
+ }
63
86
  const DragStateProvider = ({ children, config: configOverrides, }) => {
64
- // Merge config with defaults
65
- const config = (0, react_1.useMemo)(() => ({ ...constants_1.DEFAULT_DRAG_CONFIG, ...configOverrides }), [configOverrides]);
87
+ // Merge config with defaults and validate
88
+ const config = (0, react_1.useMemo)(() => {
89
+ const mergedConfig = { ...constants_1.DEFAULT_DRAG_CONFIG, ...configOverrides };
90
+ validateConfig(mergedConfig);
91
+ return mergedConfig;
92
+ }, [configOverrides]);
66
93
  // Shared values are created once and persist for the lifetime of the provider
67
94
  const isDragging = (0, react_native_reanimated_1.useSharedValue)(false);
68
95
  const draggedIndex = (0, react_native_reanimated_1.useSharedValue)(-1);
@@ -80,8 +80,11 @@ interface ListAnimationProviderProps {
80
80
  */
81
81
  entryAnimationTimeout?: number;
82
82
  /**
83
- * Duration for layout animations when items are removed (ms)
84
- * @default 200
83
+ * Duration for layout animations when items are removed (ms).
84
+ * This controls how fast remaining items animate into their new positions
85
+ * after an item is removed from the list.
86
+ * Lower values = faster/snappier, higher values = smoother/more gradual
87
+ * @default 300
85
88
  */
86
89
  layoutAnimationDuration?: number;
87
90
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ListAnimationContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ListAnimationContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAElF;;;OAGG;IACH,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,oBAAoB,EAAE,CACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,kBAAkB,EAC7B,UAAU,EAAE,MAAM,IAAI,KACnB,OAAO,CAAC;IAEb;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAE7E;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,qBAAqB,GAAG,IAAI,CAAC;IAEtE;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7E;;OAEG;IACH,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;OAGG;IACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjD;;;OAGG;IACH,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC;;OAEG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAID;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAO,yBAMnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAO,yBAAyB,GAAG,IAEvE,CAAC;AAEF,UAAU,0BAA0B;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CA+JtE,CAAC"}
1
+ {"version":3,"file":"ListAnimationContext.d.ts","sourceRoot":"","sources":["../../src/contexts/ListAnimationContext.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAkB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3E,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EAEtB,MAAM,UAAU,CAAC;AAElB;;;;;;;;GAQG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;OAGG;IACH,wBAAwB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAElF;;;OAGG;IACH,0BAA0B,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAErD;;;OAGG;IACH,oBAAoB,EAAE,CACpB,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,kBAAkB,EAC7B,UAAU,EAAE,MAAM,IAAI,KACnB,OAAO,CAAC;IAEb;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAE7E;;;;OAIG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,qBAAqB,GAAG,IAAI,CAAC;IAEtE;;;OAGG;IACH,mBAAmB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7E;;OAEG;IACH,qBAAqB,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAEhD;;;OAGG;IACH,qBAAqB,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAEjD;;;OAGG;IACH,mBAAmB,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAEzC;;OAEG;IACH,uBAAuB,EAAE,MAAM,CAAC;CACjC;AAID;;;GAGG;AACH,eAAO,MAAM,gBAAgB,QAAO,yBAMnC,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,QAAO,yBAAyB,GAAG,IAEvE,CAAC;AAEF,UAAU,0BAA0B;IAClC,QAAQ,EAAE,SAAS,CAAC;IACpB;;;OAGG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,KAAK,CAAC,EAAE,CAAC,0BAA0B,CAmKtE,CAAC"}
@@ -68,7 +68,7 @@ exports.useListAnimationOptional = useListAnimationOptional;
68
68
  * - Layout animation coordination when items are removed
69
69
  * - Decoupled from React render cycle for performance
70
70
  */
71
- const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutAnimationDuration = 200, enableLayoutAnimation = true, }) => {
71
+ const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutAnimationDuration = 300, enableLayoutAnimation = true, }) => {
72
72
  // Map of itemId -> exit animation trigger function
73
73
  const animationTriggersRef = (0, react_1.useRef)(new Map());
74
74
  // Map of itemId -> pending entry animation
@@ -129,14 +129,13 @@ const ListAnimationProvider = ({ children, entryAnimationTimeout = 5000, layoutA
129
129
  exitingItemsVersion.value = exitingItemsVersion.value + 1;
130
130
  // Configure native LayoutAnimation for remaining items
131
131
  if (enableLayoutAnimation) {
132
- // Use custom config for smoother animation
133
- react_native_1.LayoutAnimation.configureNext({
134
- duration: layoutAnimationDuration,
135
- update: {
136
- type: react_native_1.LayoutAnimation.Types.easeInEaseOut,
137
- property: react_native_1.LayoutAnimation.Properties.scaleY,
138
- },
139
- });
132
+ // Use spring animation for smoother, more natural item transitions
133
+ // Spring animations better handle position changes than timed animations
134
+ // and provide a more "organic" feel to remaining items moving up
135
+ react_native_1.LayoutAnimation.configureNext(react_native_1.LayoutAnimation.create(layoutAnimationDuration, react_native_1.LayoutAnimation.Types.easeOut,
136
+ // Use opacity as the trigger property - this works better for
137
+ // position-based animations than scaleY which can cause visual artifacts
138
+ react_native_1.LayoutAnimation.Properties.opacity));
140
139
  }
141
140
  }, [enableLayoutAnimation, layoutAnimationDuration, exitingItemsVersion]);
142
141
  // Unregister an exiting item
@@ -1 +1 @@
1
- {"version":3,"file":"useListEntryAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListEntryAnimation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,kBAAkB,OAAO,CAAC,oBAAoB,CAAC;;;;;;;;;;CAmEhD,CAAC"}
1
+ {"version":3,"file":"useListEntryAnimation.d.ts","sourceRoot":"","sources":["../../../src/hooks/animations/useListEntryAnimation.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAExD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,qBAAqB,GAChC,QAAQ,MAAM,EACd,kBAAkB,OAAO,CAAC,oBAAoB,CAAC;;;;;;;;;;CAsEhD,CAAC"}
@@ -32,11 +32,11 @@ const ListAnimationContext_1 = require("../../contexts/ListAnimationContext");
32
32
  */
33
33
  const useListEntryAnimation = (itemId, configOverrides) => {
34
34
  const animationContext = (0, ListAnimationContext_1.useListAnimationOptional)();
35
- // Merge config with defaults
36
- const config = {
35
+ // Memoize merged config to prevent unnecessary re-renders and effect re-runs
36
+ const config = (0, react_1.useMemo)(() => ({
37
37
  fade: { ...animations_1.DEFAULT_ENTRY_ANIMATION.fade, ...configOverrides?.fade },
38
38
  slide: { ...animations_1.DEFAULT_ENTRY_ANIMATION.slide, ...configOverrides?.slide },
39
- };
39
+ }), [configOverrides]);
40
40
  // Shared values for entry animation - start at final position (no animation by default)
41
41
  const translateX = (0, react_native_reanimated_1.useSharedValue)(0);
42
42
  const opacity = (0, react_native_reanimated_1.useSharedValue)(1);
@@ -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;;CAgDhC,CAAC"}
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"}
@@ -116,7 +116,11 @@ const useListExitAnimation = (itemId, config) => {
116
116
  const animConfig = animationConfigs[preset];
117
117
  // Register exiting item for layout compensation (if configured)
118
118
  if (config?.onExitStart && config.index !== undefined) {
119
- const height = config.measuredHeight ?? animConfig.slide.distance;
119
+ // Use measured height if available and valid (> 0), otherwise use animation distance
120
+ // This handles the case where layout hasn't been measured yet (height = 0)
121
+ const height = config.measuredHeight && config.measuredHeight > 0
122
+ ? config.measuredHeight
123
+ : animConfig.slide.distance;
120
124
  config.onExitStart(config.index, height);
121
125
  }
122
126
  // Set animation config SharedValues BEFORE starting animation
@@ -1 +1 @@
1
- {"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAG3D,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,CAuC5B"}
1
+ {"version":3,"file":"useDragAnimatedStyle.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragAnimatedStyle.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AAE9D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,WAAW,CAAC,OAAO,CAAC,EAChC,UAAU,EAAE,WAAW,CAAC,MAAM,CAAC,EAC/B,MAAM,EAAE,WAAW,CAAC,MAAM,CAAC,GAC1B,0BAA0B,CAmE5B"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useDragAnimatedStyle = useDragAnimatedStyle;
4
4
  const react_native_reanimated_1 = require("react-native-reanimated");
5
5
  const DragStateContext_1 = require("../../contexts/DragStateContext");
6
+ const constants_1 = require("../../constants");
6
7
  /**
7
8
  * Hook that creates animated styles for drag operations.
8
9
  *
@@ -35,12 +36,37 @@ const DragStateContext_1 = require("../../contexts/DragStateContext");
35
36
  function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
36
37
  // Global drag state for scale, identity check, and scroll compensation
37
38
  const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset } = (0, DragStateContext_1.useDragState)();
39
+ // Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
40
+ // When drag ends, shadow opacity should fade smoothly instead of snapping
41
+ const animatedShadowOpacity = (0, react_native_reanimated_1.useSharedValue)(0.1);
42
+ // React to drag state changes and animate shadow opacity
43
+ (0, react_native_reanimated_1.useAnimatedReaction)(() => ({
44
+ isThisItemDragged: draggedItemId.value === itemId,
45
+ scale: draggedScale.value,
46
+ }), (current, previous) => {
47
+ 'worklet';
48
+ if (current.isThisItemDragged) {
49
+ // When dragged, interpolate shadow opacity based on scale
50
+ const targetOpacity = (0, react_native_reanimated_1.interpolate)(current.scale, [1, config.dragScale], [0.1, config.dragShadowOpacity]);
51
+ animatedShadowOpacity.value = targetOpacity;
52
+ }
53
+ else if (previous?.isThisItemDragged && !current.isThisItemDragged) {
54
+ // Transition from dragged to not-dragged: fade shadow smoothly
55
+ animatedShadowOpacity.value = (0, react_native_reanimated_1.withTiming)(0.1, {
56
+ duration: constants_1.ANIMATION_TIMING.SHADOW_FADE_DURATION,
57
+ });
58
+ }
59
+ else {
60
+ // Non-dragged item: keep at base opacity
61
+ animatedShadowOpacity.value = 0.1;
62
+ }
63
+ });
38
64
  // Animated style for drag offset with scale and shadow
39
65
  const dragAnimatedStyle = (0, react_native_reanimated_1.useAnimatedStyle)(() => {
40
66
  const isThisItemDragged = draggedItemId.value === itemId;
41
67
  // Keep elevated if: actively dragging OR has offset (animating back)
42
- const shouldBeElevated = isDragging.value || Math.abs(translateY.value) > 1;
43
- const shadowOpacity = (0, react_native_reanimated_1.interpolate)(draggedScale.value, [1, config.dragScale], [0.1, config.dragShadowOpacity]);
68
+ const shouldBeElevated = isDragging.value ||
69
+ Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
44
70
  // Calculate scroll delta for position compensation during autoscroll
45
71
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
46
72
  // Use drag translateY + scroll compensation for dragged item, shiftY for others
@@ -52,8 +78,9 @@ function useDragAnimatedStyle(itemId, isDragging, translateY, shiftY) {
52
78
  { translateY: yOffset },
53
79
  { scale: isThisItemDragged ? draggedScale.value : 1 },
54
80
  ],
55
- zIndex: shouldBeElevated ? 999 : 0,
56
- shadowOpacity: isThisItemDragged ? shadowOpacity : 0.1,
81
+ zIndex: shouldBeElevated ? constants_1.DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
82
+ // Use animated shadow opacity for smooth transitions
83
+ shadowOpacity: animatedShadowOpacity.value,
57
84
  elevation: isDragging.value ? 12 : 4,
58
85
  };
59
86
  });
@@ -1 +1 @@
1
- {"version":3,"file":"useDragShift.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDragShift.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAE1E;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,kBAAkB,GAAG,kBAAkB,CA0F3E"}
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"}
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.useDragShift = useDragShift;
4
4
  const react_native_reanimated_1 = require("react-native-reanimated");
5
5
  const DragStateContext_1 = require("../../contexts/DragStateContext");
6
+ const constants_1 = require("../../constants");
6
7
  /**
7
8
  * Hook that calculates shift animation for non-dragged items.
8
9
  *
@@ -30,7 +31,21 @@ function useDragShift(config) {
30
31
  // Calculate target shift using useDerivedValue
31
32
  const targetShiftY = (0, react_native_reanimated_1.useDerivedValue)(() => {
32
33
  'worklet';
33
- // Force re-evaluation on every drag state change
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
+ */
34
49
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
35
50
  dragUpdateTrigger.value;
36
51
  // During drop transition, freeze shift values
@@ -55,7 +70,13 @@ function useDragShift(config) {
55
70
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
56
71
  const effectiveTranslateY = translateYNow + scrollDelta;
57
72
  // Calculate which index the dragged item is hovering over
58
- const offset = effectiveTranslateY > 0 ? 0.2 : effectiveTranslateY < 0 ? -0.2 : 0;
73
+ // The HOVER_OFFSET_FACTOR adds a small bias to make crossing into
74
+ // the next position slightly easier (prevents "sticky" hover positions)
75
+ const offset = effectiveTranslateY > 0
76
+ ? constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
77
+ : effectiveTranslateY < 0
78
+ ? -constants_1.DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
79
+ : 0;
59
80
  const hoveredIndex = currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
60
81
  // Moving DOWN: items between original and hovered positions shift UP
61
82
  if (hoveredIndex > currentDraggedIndex) {
@@ -76,7 +97,7 @@ function useDragShift(config) {
76
97
  'worklet';
77
98
  if (target !== prev) {
78
99
  shiftY.value = (0, react_native_reanimated_1.withTiming)(target, {
79
- duration: 100,
100
+ duration: constants_1.ANIMATION_TIMING.SHIFT_DURATION,
80
101
  easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
81
102
  });
82
103
  }
@@ -1 +1 @@
1
- {"version":3,"file":"useDropCompensation.d.ts","sourceRoot":"","sources":["../../../src/hooks/drag/useDropCompensation.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAoF3E"}
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"}
@@ -5,6 +5,7 @@ const react_1 = require("react");
5
5
  const react_native_reanimated_1 = require("react-native-reanimated");
6
6
  const flash_list_1 = require("@shopify/flash-list");
7
7
  const DragStateContext_1 = require("../../contexts/DragStateContext");
8
+ const constants_1 = require("../../constants");
8
9
  /**
9
10
  * Hook that handles index change compensation after drag reorder.
10
11
  *
@@ -40,9 +41,20 @@ function useDropCompensation(config) {
40
41
  // Handle index changes after data updates (drop compensation)
41
42
  (0, react_1.useLayoutEffect)(() => {
42
43
  if (index !== prevIndex) {
44
+ // Guard: Skip compensation during active drop transition to prevent double-compensation
45
+ // The isDropping flag is set when drop begins and cleared after settle animation
46
+ if (isDropping.value && draggedItemId.value !== itemId) {
47
+ // Non-dragged items should wait for drop to complete
48
+ setPrevIndex(index);
49
+ return;
50
+ }
51
+ // Issue 1 Fix: Use dynamically measured height when available, matching useDragShift behavior
52
+ const itemHeight = measuredItemHeight.value > 0
53
+ ? measuredItemHeight.value + dragConfig.itemVerticalMargin
54
+ : dragConfig.itemHeight;
43
55
  // Compensate for index change by adjusting translateY
44
56
  const indexDelta = index - prevIndex;
45
- const heightDelta = indexDelta * dragConfig.itemHeight;
57
+ const heightDelta = indexDelta * itemHeight;
46
58
  // Check if this is the dragged item completing its drop
47
59
  const isTheDraggedItem = draggedItemId.value === itemId;
48
60
  // Compensate translateY for position change
@@ -50,7 +62,10 @@ function useDropCompensation(config) {
50
62
  if (isTheDraggedItem) {
51
63
  // Dragged item: animate smoothly to 0 from compensated position
52
64
  translateY.value = compensatedY;
53
- 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 => {
65
+ translateY.value = (0, react_native_reanimated_1.withTiming)(0, {
66
+ duration: constants_1.ANIMATION_TIMING.DROP_SETTLE_DURATION,
67
+ easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
68
+ }, finished => {
54
69
  'worklet';
55
70
  if (finished) {
56
71
  // Reset ALL global state after settle animation
@@ -59,15 +74,19 @@ function useDropCompensation(config) {
59
74
  draggedIndex.value = -1;
60
75
  draggedItemId.value = '';
61
76
  measuredItemHeight.value = 0;
62
- dragUpdateTrigger.value = (0, react_native_reanimated_1.withTiming)(dragUpdateTrigger.value + 1, {
63
- duration: 1,
64
- });
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;
65
80
  }
66
81
  });
67
82
  }
68
- else if (Math.abs(translateY.value) > 1) {
69
- // Non-dragged item: just compensate without animation
70
- translateY.value = compensatedY;
83
+ else if (Math.abs(translateY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
84
+ // Issue 3 Fix: Non-dragged items now animate their compensation for visual consistency
85
+ // This prevents the jarring "jump" when only the dragged item animates smoothly
86
+ translateY.value = (0, react_native_reanimated_1.withTiming)(compensatedY, {
87
+ duration: constants_1.ANIMATION_TIMING.COMPENSATION_DURATION,
88
+ easing: react_native_reanimated_1.Easing.out(react_native_reanimated_1.Easing.ease),
89
+ });
71
90
  }
72
91
  // Update tracked index
73
92
  setPrevIndex(index);
@@ -83,7 +102,7 @@ function useDropCompensation(config) {
83
102
  'worklet';
84
103
  if (prevIdx !== null &&
85
104
  currentIndex !== prevIdx &&
86
- Math.abs(shiftY.value) > 1) {
105
+ Math.abs(shiftY.value) > constants_1.DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
87
106
  shiftY.value = 0;
88
107
  }
89
108
  });
package/lib/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @souschef/reanimated-flashlist
2
+ * @souscheflabs/reanimated-flashlist
3
3
  *
4
4
  * A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
5
5
  *
@@ -8,7 +8,7 @@
8
8
  * import {
9
9
  * AnimatedFlashList,
10
10
  * type AnimatedListItem,
11
- * } from '@souschef/reanimated-flashlist';
11
+ * } from '@souscheflabs/reanimated-flashlist';
12
12
  *
13
13
  * interface MyItem extends AnimatedListItem {
14
14
  * title: string;
package/lib/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  /**
3
- * @souschef/reanimated-flashlist
3
+ * @souscheflabs/reanimated-flashlist
4
4
  *
5
5
  * A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
6
6
  *
@@ -9,7 +9,7 @@
9
9
  * import {
10
10
  * AnimatedFlashList,
11
11
  * type AnimatedListItem,
12
- * } from '@souschef/reanimated-flashlist';
12
+ * } from '@souscheflabs/reanimated-flashlist';
13
13
  *
14
14
  * interface MyItem extends AnimatedListItem {
15
15
  * title: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@souscheflabs/reanimated-flashlist",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
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",
@@ -1,6 +1,6 @@
1
1
  import React, { useLayoutEffect, useMemo, useCallback, useRef } from 'react';
2
2
  import Animated, { useAnimatedRef } from 'react-native-reanimated';
3
- import type { ViewStyle, LayoutChangeEvent } from 'react-native';
3
+ import type { LayoutChangeEvent } from 'react-native';
4
4
  import {
5
5
  useDragGesture,
6
6
  useDragShift,
@@ -131,13 +131,6 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
131
131
  measuredHeightRef.current = event.nativeEvent.layout.height;
132
132
  }, []);
133
133
 
134
- // Create combined animated style
135
- const combinedAnimatedStyle = useMemo<ViewStyle>(() => {
136
- // We can't directly combine animated styles here since they're worklet-based
137
- // Instead, we'll let the consumer apply them via the render prop
138
- return {};
139
- }, []);
140
-
141
134
  // Create drag handle props
142
135
  const dragHandleProps = useMemo(
143
136
  () =>
@@ -163,12 +156,14 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
163
156
  );
164
157
 
165
158
  // Create render info
159
+ // Note: animatedStyle is empty because animations are applied to the wrapper View automatically.
160
+ // This field is kept for backward compatibility with the AnimatedRenderItemInfo interface.
166
161
  const renderInfo = useMemo<AnimatedRenderItemInfo<T>>(
167
162
  () => ({
168
163
  item,
169
164
  index,
170
165
  totalItems,
171
- animatedStyle: combinedAnimatedStyle,
166
+ animatedStyle: {},
172
167
  dragHandleProps,
173
168
  isDragging: false, // This is a SharedValue, consumer should use dragHandleProps.isDragging
174
169
  isDragEnabled,
@@ -179,7 +174,6 @@ function AnimatedFlashListItemInner<T extends AnimatedListItem>({
179
174
  item,
180
175
  index,
181
176
  totalItems,
182
- combinedAnimatedStyle,
183
177
  dragHandleProps,
184
178
  isDragEnabled,
185
179
  triggerExitAnimation,
@@ -309,15 +309,15 @@ describe('ListAnimationContext', () => {
309
309
  wrapper: createWrapper(),
310
310
  });
311
311
 
312
- expect(result.current.layoutAnimationDuration).toBe(200);
312
+ expect(result.current.layoutAnimationDuration).toBe(300);
313
313
  });
314
314
 
315
315
  it('uses custom layoutAnimationDuration', () => {
316
316
  const { result } = renderHook(() => useListAnimation(), {
317
- wrapper: createWrapper({ layoutAnimationDuration: 300 }),
317
+ wrapper: createWrapper({ layoutAnimationDuration: 400 }),
318
318
  });
319
319
 
320
- expect(result.current.layoutAnimationDuration).toBe(300);
320
+ expect(result.current.layoutAnimationDuration).toBe(400);
321
321
  });
322
322
  });
323
323
  });
@@ -32,7 +32,7 @@ export const DEFAULT_EXIT_ANIMATION: ExitAnimationConfig = {
32
32
  },
33
33
  removalDelay: 300,
34
34
  layoutAnimation: {
35
- duration: 200,
35
+ duration: 300,
36
36
  },
37
37
  };
38
38
 
@@ -60,7 +60,9 @@ export const FAST_EXIT_ANIMATION: ExitAnimationConfig = {
60
60
  },
61
61
  removalDelay: 200,
62
62
  layoutAnimation: {
63
- duration: 150,
63
+ // Slightly longer than the exit animation for a smoother overall effect
64
+ // Items below start moving up as the exiting item is sliding out
65
+ duration: 250,
64
66
  },
65
67
  };
66
68
 
@@ -1,5 +1,57 @@
1
1
  import type { DragConfig } from '../types';
2
2
 
3
+ // ============================================================================
4
+ // Animation Timing & Easing Constants
5
+ // ============================================================================
6
+
7
+ /**
8
+ * Unified animation timing configuration for consistency across all drag hooks.
9
+ * Using consistent timing prevents visual "mismatches" during complex interactions.
10
+ */
11
+ export const ANIMATION_TIMING = {
12
+ /** Duration for item shift animations (when items move to make room) */
13
+ SHIFT_DURATION: 100,
14
+ /** Duration for drop settle animation (when dragged item animates to final position) */
15
+ DROP_SETTLE_DURATION: 150,
16
+ /** Duration for shadow opacity fade transition */
17
+ SHADOW_FADE_DURATION: 100,
18
+ /** Duration for non-dragged item compensation animations */
19
+ COMPENSATION_DURATION: 100,
20
+ } as const;
21
+
22
+ // ============================================================================
23
+ // Threshold Constants
24
+ // ============================================================================
25
+
26
+ /**
27
+ * Configurable thresholds for drag calculations.
28
+ * These values control sensitivity and visual behavior.
29
+ */
30
+ export const DRAG_THRESHOLDS = {
31
+ /**
32
+ * Offset factor for hover position calculation.
33
+ * This adds a bias when determining which index is being hovered over,
34
+ * making it slightly easier to "cross" into the next/prev position.
35
+ * Range: 0-0.5, where 0 = exact center, 0.5 = edge of item
36
+ */
37
+ HOVER_OFFSET_FACTOR: 0.2,
38
+ /**
39
+ * Z-index for elevated (dragged) items.
40
+ * Should be high enough to appear above other UI but not conflict
41
+ * with modals or overlays (which typically use 1000+).
42
+ */
43
+ ELEVATED_Z_INDEX: 999,
44
+ /**
45
+ * Minimum translateY/shiftY value to consider "significant".
46
+ * Values below this are treated as zero for optimization.
47
+ */
48
+ SHIFT_SIGNIFICANCE_THRESHOLD: 1,
49
+ } as const;
50
+
51
+ // ============================================================================
52
+ // Default Drag Configuration
53
+ // ============================================================================
54
+
3
55
  /**
4
56
  * Default drag configuration
5
57
  * All values can be overridden via AnimatedFlashList config prop
@@ -88,15 +88,55 @@ interface DragStateProviderProps {
88
88
  *
89
89
  * Using SharedValues ensures animations run on the UI thread at 60fps.
90
90
  */
91
+ /**
92
+ * Validates drag configuration values to prevent runtime errors.
93
+ * Logs warnings in development for invalid configurations.
94
+ */
95
+ function validateConfig(config: DragConfig): void {
96
+ if (__DEV__) {
97
+ if (config.itemHeight <= 0) {
98
+ console.warn(
99
+ '[AnimatedFlashList] Invalid itemHeight: must be positive. Got:',
100
+ config.itemHeight,
101
+ );
102
+ }
103
+ if (config.longPressDuration <= 0) {
104
+ console.warn(
105
+ '[AnimatedFlashList] Invalid longPressDuration: must be positive. Got:',
106
+ config.longPressDuration,
107
+ );
108
+ }
109
+ if (config.dragScale <= 0) {
110
+ console.warn(
111
+ '[AnimatedFlashList] Invalid dragScale: must be positive. Got:',
112
+ config.dragScale,
113
+ );
114
+ }
115
+ if (config.edgeThreshold < 0) {
116
+ console.warn(
117
+ '[AnimatedFlashList] Invalid edgeThreshold: must be non-negative. Got:',
118
+ config.edgeThreshold,
119
+ );
120
+ }
121
+ if (config.maxScrollSpeed <= 0) {
122
+ console.warn(
123
+ '[AnimatedFlashList] Invalid maxScrollSpeed: must be positive. Got:',
124
+ config.maxScrollSpeed,
125
+ );
126
+ }
127
+ }
128
+ }
129
+
91
130
  export const DragStateProvider: React.FC<DragStateProviderProps> = ({
92
131
  children,
93
132
  config: configOverrides,
94
133
  }) => {
95
- // Merge config with defaults
96
- const config = useMemo<DragConfig>(
97
- () => ({ ...DEFAULT_DRAG_CONFIG, ...configOverrides }),
98
- [configOverrides],
99
- );
134
+ // Merge config with defaults and validate
135
+ const config = useMemo<DragConfig>(() => {
136
+ const mergedConfig = { ...DEFAULT_DRAG_CONFIG, ...configOverrides };
137
+ validateConfig(mergedConfig);
138
+ return mergedConfig;
139
+ }, [configOverrides]);
100
140
 
101
141
  // Shared values are created once and persist for the lifetime of the provider
102
142
  const isDragging = useSharedValue(false);
@@ -120,8 +120,11 @@ interface ListAnimationProviderProps {
120
120
  */
121
121
  entryAnimationTimeout?: number;
122
122
  /**
123
- * Duration for layout animations when items are removed (ms)
124
- * @default 200
123
+ * Duration for layout animations when items are removed (ms).
124
+ * This controls how fast remaining items animate into their new positions
125
+ * after an item is removed from the list.
126
+ * Lower values = faster/snappier, higher values = smoother/more gradual
127
+ * @default 300
125
128
  */
126
129
  layoutAnimationDuration?: number;
127
130
  /**
@@ -143,7 +146,7 @@ interface ListAnimationProviderProps {
143
146
  export const ListAnimationProvider: React.FC<ListAnimationProviderProps> = ({
144
147
  children,
145
148
  entryAnimationTimeout = 5000,
146
- layoutAnimationDuration = 200,
149
+ layoutAnimationDuration = 300,
147
150
  enableLayoutAnimation = true,
148
151
  }) => {
149
152
  // Map of itemId -> exit animation trigger function
@@ -234,14 +237,18 @@ export const ListAnimationProvider: React.FC<ListAnimationProviderProps> = ({
234
237
 
235
238
  // Configure native LayoutAnimation for remaining items
236
239
  if (enableLayoutAnimation) {
237
- // Use custom config for smoother animation
238
- LayoutAnimation.configureNext({
239
- duration: layoutAnimationDuration,
240
- update: {
241
- type: LayoutAnimation.Types.easeInEaseOut,
242
- property: LayoutAnimation.Properties.scaleY,
243
- },
244
- });
240
+ // Use spring animation for smoother, more natural item transitions
241
+ // Spring animations better handle position changes than timed animations
242
+ // and provide a more "organic" feel to remaining items moving up
243
+ LayoutAnimation.configureNext(
244
+ LayoutAnimation.create(
245
+ layoutAnimationDuration,
246
+ LayoutAnimation.Types.easeOut,
247
+ // Use opacity as the trigger property - this works better for
248
+ // position-based animations than scaleY which can cause visual artifacts
249
+ LayoutAnimation.Properties.opacity,
250
+ ),
251
+ );
245
252
  }
246
253
  },
247
254
  [enableLayoutAnimation, layoutAnimationDuration, exitingItemsVersion],
@@ -1,4 +1,4 @@
1
- import { useEffect, useRef } from 'react';
1
+ import { useEffect, useRef, useMemo } from 'react';
2
2
  import {
3
3
  useAnimatedStyle,
4
4
  useSharedValue,
@@ -42,11 +42,14 @@ export const useListEntryAnimation = (
42
42
  ) => {
43
43
  const animationContext = useListAnimationOptional();
44
44
 
45
- // Merge config with defaults
46
- const config = {
47
- fade: { ...DEFAULT_ENTRY_ANIMATION.fade, ...configOverrides?.fade },
48
- slide: { ...DEFAULT_ENTRY_ANIMATION.slide, ...configOverrides?.slide },
49
- };
45
+ // Memoize merged config to prevent unnecessary re-renders and effect re-runs
46
+ const config = useMemo(
47
+ () => ({
48
+ fade: { ...DEFAULT_ENTRY_ANIMATION.fade, ...configOverrides?.fade },
49
+ slide: { ...DEFAULT_ENTRY_ANIMATION.slide, ...configOverrides?.slide },
50
+ }),
51
+ [configOverrides],
52
+ );
50
53
 
51
54
  // Shared values for entry animation - start at final position (no animation by default)
52
55
  const translateX = useSharedValue(0);
@@ -158,7 +158,12 @@ export const useListExitAnimation = (
158
158
 
159
159
  // Register exiting item for layout compensation (if configured)
160
160
  if (config?.onExitStart && config.index !== undefined) {
161
- const height = config.measuredHeight ?? animConfig.slide.distance;
161
+ // Use measured height if available and valid (> 0), otherwise use animation distance
162
+ // This handles the case where layout hasn't been measured yet (height = 0)
163
+ const height =
164
+ config.measuredHeight && config.measuredHeight > 0
165
+ ? config.measuredHeight
166
+ : animConfig.slide.distance;
162
167
  config.onExitStart(config.index, height);
163
168
  }
164
169
 
@@ -1,7 +1,14 @@
1
- import { useAnimatedStyle, interpolate } from 'react-native-reanimated';
1
+ import {
2
+ useAnimatedStyle,
3
+ interpolate,
4
+ useSharedValue,
5
+ useAnimatedReaction,
6
+ withTiming,
7
+ } from 'react-native-reanimated';
2
8
  import type { SharedValue } from 'react-native-reanimated';
3
9
  import type { ViewStyle } from 'react-native';
4
10
  import { useDragState } from '../../contexts/DragStateContext';
11
+ import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
5
12
  import type { UseDragAnimatedStyleResult } from '../../types';
6
13
 
7
14
  /**
@@ -43,19 +50,46 @@ export function useDragAnimatedStyle(
43
50
  const { draggedItemId, draggedScale, config, scrollOffset, dragStartScrollOffset } =
44
51
  useDragState();
45
52
 
53
+ // Issue 2 Fix: Track shadow opacity with smooth transitions to prevent "flash"
54
+ // When drag ends, shadow opacity should fade smoothly instead of snapping
55
+ const animatedShadowOpacity = useSharedValue(0.1);
56
+
57
+ // React to drag state changes and animate shadow opacity
58
+ useAnimatedReaction(
59
+ () => ({
60
+ isThisItemDragged: draggedItemId.value === itemId,
61
+ scale: draggedScale.value,
62
+ }),
63
+ (current, previous) => {
64
+ 'worklet';
65
+ if (current.isThisItemDragged) {
66
+ // When dragged, interpolate shadow opacity based on scale
67
+ const targetOpacity = interpolate(
68
+ current.scale,
69
+ [1, config.dragScale],
70
+ [0.1, config.dragShadowOpacity],
71
+ );
72
+ animatedShadowOpacity.value = targetOpacity;
73
+ } else if (previous?.isThisItemDragged && !current.isThisItemDragged) {
74
+ // Transition from dragged to not-dragged: fade shadow smoothly
75
+ animatedShadowOpacity.value = withTiming(0.1, {
76
+ duration: ANIMATION_TIMING.SHADOW_FADE_DURATION,
77
+ });
78
+ } else {
79
+ // Non-dragged item: keep at base opacity
80
+ animatedShadowOpacity.value = 0.1;
81
+ }
82
+ },
83
+ );
84
+
46
85
  // Animated style for drag offset with scale and shadow
47
86
  const dragAnimatedStyle = useAnimatedStyle(() => {
48
87
  const isThisItemDragged = draggedItemId.value === itemId;
49
88
 
50
89
  // Keep elevated if: actively dragging OR has offset (animating back)
51
90
  const shouldBeElevated =
52
- isDragging.value || Math.abs(translateY.value) > 1;
53
-
54
- const shadowOpacity = interpolate(
55
- draggedScale.value,
56
- [1, config.dragScale],
57
- [0.1, config.dragShadowOpacity],
58
- );
91
+ isDragging.value ||
92
+ Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD;
59
93
 
60
94
  // Calculate scroll delta for position compensation during autoscroll
61
95
  const scrollDelta = scrollOffset.value - dragStartScrollOffset.value;
@@ -70,8 +104,9 @@ export function useDragAnimatedStyle(
70
104
  { translateY: yOffset },
71
105
  { scale: isThisItemDragged ? draggedScale.value : 1 },
72
106
  ],
73
- zIndex: shouldBeElevated ? 999 : 0,
74
- shadowOpacity: isThisItemDragged ? shadowOpacity : 0.1,
107
+ zIndex: shouldBeElevated ? DRAG_THRESHOLDS.ELEVATED_Z_INDEX : 0,
108
+ // Use animated shadow opacity for smooth transitions
109
+ shadowOpacity: animatedShadowOpacity.value,
75
110
  elevation: isDragging.value ? 12 : 4,
76
111
  } as ViewStyle;
77
112
  });
@@ -6,6 +6,7 @@ import {
6
6
  Easing,
7
7
  } from 'react-native-reanimated';
8
8
  import { useDragState } from '../../contexts/DragStateContext';
9
+ import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
9
10
  import type { UseDragShiftConfig, UseDragShiftResult } from '../../types';
10
11
 
11
12
  /**
@@ -49,7 +50,21 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
49
50
  // Calculate target shift using useDerivedValue
50
51
  const targetShiftY = useDerivedValue(() => {
51
52
  'worklet';
52
- // Force re-evaluation on every drag state change
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
+ */
53
68
  // eslint-disable-next-line @typescript-eslint/no-unused-expressions
54
69
  dragUpdateTrigger.value;
55
70
 
@@ -80,8 +95,14 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
80
95
  const effectiveTranslateY = translateYNow + scrollDelta;
81
96
 
82
97
  // Calculate which index the dragged item is hovering over
98
+ // The HOVER_OFFSET_FACTOR adds a small bias to make crossing into
99
+ // the next position slightly easier (prevents "sticky" hover positions)
83
100
  const offset =
84
- effectiveTranslateY > 0 ? 0.2 : effectiveTranslateY < 0 ? -0.2 : 0;
101
+ effectiveTranslateY > 0
102
+ ? DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
103
+ : effectiveTranslateY < 0
104
+ ? -DRAG_THRESHOLDS.HOVER_OFFSET_FACTOR
105
+ : 0;
85
106
  const hoveredIndex =
86
107
  currentDraggedIndex + Math.round(effectiveTranslateY / itemHeight + offset);
87
108
 
@@ -108,7 +129,7 @@ export function useDragShift(config: UseDragShiftConfig): UseDragShiftResult {
108
129
  'worklet';
109
130
  if (target !== prev) {
110
131
  shiftY.value = withTiming(target, {
111
- duration: 100,
132
+ duration: ANIMATION_TIMING.SHIFT_DURATION,
112
133
  easing: Easing.out(Easing.ease),
113
134
  });
114
135
  }
@@ -7,6 +7,7 @@ import {
7
7
  } from 'react-native-reanimated';
8
8
  import { useRecyclingState } from '@shopify/flash-list';
9
9
  import { useDragState } from '../../contexts/DragStateContext';
10
+ import { ANIMATION_TIMING, DRAG_THRESHOLDS } from '../../constants';
10
11
  import type { UseDropCompensationConfig } from '../../types';
11
12
 
12
13
  /**
@@ -56,9 +57,23 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
56
57
  // Handle index changes after data updates (drop compensation)
57
58
  useLayoutEffect(() => {
58
59
  if (index !== prevIndex) {
60
+ // Guard: Skip compensation during active drop transition to prevent double-compensation
61
+ // The isDropping flag is set when drop begins and cleared after settle animation
62
+ if (isDropping.value && draggedItemId.value !== itemId) {
63
+ // Non-dragged items should wait for drop to complete
64
+ setPrevIndex(index);
65
+ return;
66
+ }
67
+
68
+ // Issue 1 Fix: Use dynamically measured height when available, matching useDragShift behavior
69
+ const itemHeight =
70
+ measuredItemHeight.value > 0
71
+ ? measuredItemHeight.value + dragConfig.itemVerticalMargin
72
+ : dragConfig.itemHeight;
73
+
59
74
  // Compensate for index change by adjusting translateY
60
75
  const indexDelta = index - prevIndex;
61
- const heightDelta = indexDelta * dragConfig.itemHeight;
76
+ const heightDelta = indexDelta * itemHeight;
62
77
 
63
78
  // Check if this is the dragged item completing its drop
64
79
  const isTheDraggedItem = draggedItemId.value === itemId;
@@ -71,7 +86,10 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
71
86
  translateY.value = compensatedY;
72
87
  translateY.value = withTiming(
73
88
  0,
74
- { duration: 150, easing: Easing.out(Easing.ease) },
89
+ {
90
+ duration: ANIMATION_TIMING.DROP_SETTLE_DURATION,
91
+ easing: Easing.out(Easing.ease),
92
+ },
75
93
  finished => {
76
94
  'worklet';
77
95
  if (finished) {
@@ -81,15 +99,19 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
81
99
  draggedIndex.value = -1;
82
100
  draggedItemId.value = '';
83
101
  measuredItemHeight.value = 0;
84
- dragUpdateTrigger.value = withTiming(dragUpdateTrigger.value + 1, {
85
- duration: 1,
86
- });
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;
87
105
  }
88
106
  },
89
107
  );
90
- } else if (Math.abs(translateY.value) > 1) {
91
- // Non-dragged item: just compensate without animation
92
- translateY.value = compensatedY;
108
+ } else if (Math.abs(translateY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD) {
109
+ // Issue 3 Fix: Non-dragged items now animate their compensation for visual consistency
110
+ // This prevents the jarring "jump" when only the dragged item animates smoothly
111
+ translateY.value = withTiming(compensatedY, {
112
+ duration: ANIMATION_TIMING.COMPENSATION_DURATION,
113
+ easing: Easing.out(Easing.ease),
114
+ });
93
115
  }
94
116
 
95
117
  // Update tracked index
@@ -111,7 +133,7 @@ export function useDropCompensation(config: UseDropCompensationConfig): void {
111
133
  if (
112
134
  prevIdx !== null &&
113
135
  currentIndex !== prevIdx &&
114
- Math.abs(shiftY.value) > 1
136
+ Math.abs(shiftY.value) > DRAG_THRESHOLDS.SHIFT_SIGNIFICANCE_THRESHOLD
115
137
  ) {
116
138
  shiftY.value = 0;
117
139
  }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @souschef/reanimated-flashlist
2
+ * @souscheflabs/reanimated-flashlist
3
3
  *
4
4
  * A high-performance animated FlashList with drag-to-reorder and entry/exit animations.
5
5
  *
@@ -8,7 +8,7 @@
8
8
  * import {
9
9
  * AnimatedFlashList,
10
10
  * type AnimatedListItem,
11
- * } from '@souschef/reanimated-flashlist';
11
+ * } from '@souscheflabs/reanimated-flashlist';
12
12
  *
13
13
  * interface MyItem extends AnimatedListItem {
14
14
  * title: string;