react-native-drax 0.11.0-alpha.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +390 -227
  3. package/lib/module/DebugOverlay.js +121 -0
  4. package/lib/module/DebugOverlay.js.map +1 -0
  5. package/lib/module/Drax.js +36 -0
  6. package/lib/module/Drax.js.map +1 -0
  7. package/lib/module/DraxContext.js +6 -0
  8. package/lib/module/DraxContext.js.map +1 -0
  9. package/lib/module/DraxHandle.js +47 -0
  10. package/lib/module/DraxHandle.js.map +1 -0
  11. package/lib/module/DraxHandleContext.js +11 -0
  12. package/lib/module/DraxHandleContext.js.map +1 -0
  13. package/lib/module/DraxList.js +108 -0
  14. package/lib/module/DraxList.js.map +1 -0
  15. package/lib/module/DraxProvider.js +203 -0
  16. package/lib/module/DraxProvider.js.map +1 -0
  17. package/lib/module/DraxScrollView.js +167 -0
  18. package/lib/module/DraxScrollView.js.map +1 -0
  19. package/lib/module/DraxSubprovider.js +21 -0
  20. package/lib/module/DraxSubprovider.js.map +1 -0
  21. package/lib/module/DraxView.js +348 -0
  22. package/lib/module/DraxView.js.map +1 -0
  23. package/lib/module/HoverLayer.js +152 -0
  24. package/lib/module/HoverLayer.js.map +1 -0
  25. package/lib/module/SortableBoardContainer.js +386 -0
  26. package/lib/module/SortableBoardContainer.js.map +1 -0
  27. package/lib/module/SortableBoardContext.js +6 -0
  28. package/lib/module/SortableBoardContext.js.map +1 -0
  29. package/lib/module/SortableContainer.js +571 -0
  30. package/lib/module/SortableContainer.js.map +1 -0
  31. package/lib/module/SortableItem.js +226 -0
  32. package/lib/module/SortableItem.js.map +1 -0
  33. package/lib/module/SortableItemContext.js +38 -0
  34. package/lib/module/SortableItemContext.js.map +1 -0
  35. package/lib/module/compat/detectVersion.js +19 -0
  36. package/lib/module/compat/detectVersion.js.map +1 -0
  37. package/lib/module/compat/index.js +5 -0
  38. package/lib/module/compat/index.js.map +1 -0
  39. package/lib/module/compat/types.js +4 -0
  40. package/lib/module/compat/types.js.map +1 -0
  41. package/lib/module/compat/useDraxPanGesture.js +94 -0
  42. package/lib/module/compat/useDraxPanGesture.js.map +1 -0
  43. package/lib/module/hooks/index.js +5 -0
  44. package/lib/module/hooks/index.js.map +1 -0
  45. package/lib/module/hooks/useCallbackDispatch.js +688 -0
  46. package/lib/module/hooks/useCallbackDispatch.js.map +1 -0
  47. package/lib/module/hooks/useDragGesture.js +240 -0
  48. package/lib/module/hooks/useDragGesture.js.map +1 -0
  49. package/lib/module/hooks/useDraxContext.js +12 -0
  50. package/lib/module/hooks/useDraxContext.js.map +1 -0
  51. package/lib/module/hooks/useDraxId.js +13 -0
  52. package/lib/module/hooks/useDraxId.js.map +1 -0
  53. package/lib/module/hooks/useDraxMethods.js +73 -0
  54. package/lib/module/hooks/useDraxMethods.js.map +1 -0
  55. package/lib/module/hooks/useDraxScrollHandler.js +97 -0
  56. package/lib/module/hooks/useDraxScrollHandler.js.map +1 -0
  57. package/lib/module/hooks/useSortableBoard.js +37 -0
  58. package/lib/module/hooks/useSortableBoard.js.map +1 -0
  59. package/lib/module/hooks/useSortableList.js +988 -0
  60. package/lib/module/hooks/useSortableList.js.map +1 -0
  61. package/lib/module/hooks/useSpatialIndex.js +283 -0
  62. package/lib/module/hooks/useSpatialIndex.js.map +1 -0
  63. package/lib/module/hooks/useViewStyles.js +158 -0
  64. package/lib/module/hooks/useViewStyles.js.map +1 -0
  65. package/lib/module/hooks/useWebScrollFreeze.js +52 -0
  66. package/lib/module/hooks/useWebScrollFreeze.js.map +1 -0
  67. package/lib/module/index.js +37 -0
  68. package/lib/module/index.js.map +1 -0
  69. package/lib/module/math.js +294 -0
  70. package/lib/module/math.js.map +1 -0
  71. package/lib/module/package.json +1 -0
  72. package/lib/module/params.js +88 -0
  73. package/lib/module/params.js.map +1 -0
  74. package/lib/module/types.js +215 -0
  75. package/lib/module/types.js.map +1 -0
  76. package/lib/typescript/package.json +1 -0
  77. package/lib/typescript/src/DebugOverlay.d.ts +17 -0
  78. package/lib/typescript/src/DebugOverlay.d.ts.map +1 -0
  79. package/lib/typescript/src/Drax.d.ts +28 -0
  80. package/lib/typescript/src/Drax.d.ts.map +1 -0
  81. package/lib/typescript/src/DraxContext.d.ts +3 -0
  82. package/lib/typescript/src/DraxContext.d.ts.map +1 -0
  83. package/lib/typescript/src/DraxHandle.d.ts +25 -0
  84. package/lib/typescript/src/DraxHandle.d.ts.map +1 -0
  85. package/lib/typescript/src/DraxHandleContext.d.ts +12 -0
  86. package/lib/typescript/src/DraxHandleContext.d.ts.map +1 -0
  87. package/lib/typescript/src/DraxList.d.ts +66 -0
  88. package/lib/typescript/src/DraxList.d.ts.map +1 -0
  89. package/lib/typescript/src/DraxProvider.d.ts +4 -0
  90. package/lib/typescript/src/DraxProvider.d.ts.map +1 -0
  91. package/lib/typescript/src/DraxScrollView.d.ts +7 -0
  92. package/lib/typescript/src/DraxScrollView.d.ts.map +1 -0
  93. package/lib/typescript/src/DraxSubprovider.d.ts +4 -0
  94. package/lib/typescript/src/DraxSubprovider.d.ts.map +1 -0
  95. package/lib/typescript/src/DraxView.d.ts +4 -0
  96. package/lib/typescript/src/DraxView.d.ts.map +1 -0
  97. package/lib/typescript/src/HoverLayer.d.ts +38 -0
  98. package/lib/typescript/src/HoverLayer.d.ts.map +1 -0
  99. package/lib/typescript/src/SortableBoardContainer.d.ts +11 -0
  100. package/lib/typescript/src/SortableBoardContainer.d.ts.map +1 -0
  101. package/lib/typescript/src/SortableBoardContext.d.ts +4 -0
  102. package/lib/typescript/src/SortableBoardContext.d.ts.map +1 -0
  103. package/lib/typescript/src/SortableContainer.d.ts +13 -0
  104. package/lib/typescript/src/SortableContainer.d.ts.map +1 -0
  105. package/lib/typescript/src/SortableItem.d.ts +14 -0
  106. package/lib/typescript/src/SortableItem.d.ts.map +1 -0
  107. package/lib/typescript/src/SortableItemContext.d.ts +37 -0
  108. package/lib/typescript/src/SortableItemContext.d.ts.map +1 -0
  109. package/lib/typescript/src/compat/detectVersion.d.ts +2 -0
  110. package/lib/typescript/src/compat/detectVersion.d.ts.map +1 -0
  111. package/lib/typescript/src/compat/index.d.ts +4 -0
  112. package/lib/typescript/src/compat/index.d.ts.map +1 -0
  113. package/lib/typescript/src/compat/types.d.ts +33 -0
  114. package/lib/typescript/src/compat/types.d.ts.map +1 -0
  115. package/lib/typescript/src/compat/useDraxPanGesture.d.ts +8 -0
  116. package/lib/typescript/src/compat/useDraxPanGesture.d.ts.map +1 -0
  117. package/lib/typescript/src/hooks/index.d.ts +3 -0
  118. package/lib/typescript/src/hooks/index.d.ts.map +1 -0
  119. package/lib/typescript/src/hooks/useCallbackDispatch.d.ts +40 -0
  120. package/lib/typescript/src/hooks/useCallbackDispatch.d.ts.map +1 -0
  121. package/lib/typescript/src/hooks/useDragGesture.d.ts +17 -0
  122. package/lib/typescript/src/hooks/useDragGesture.d.ts.map +1 -0
  123. package/lib/typescript/src/hooks/useDraxContext.d.ts +2 -0
  124. package/lib/typescript/src/hooks/useDraxContext.d.ts.map +1 -0
  125. package/{build → lib/typescript/src}/hooks/useDraxId.d.ts +1 -0
  126. package/lib/typescript/src/hooks/useDraxId.d.ts.map +1 -0
  127. package/lib/typescript/src/hooks/useDraxMethods.d.ts +13 -0
  128. package/lib/typescript/src/hooks/useDraxMethods.d.ts.map +1 -0
  129. package/lib/typescript/src/hooks/useDraxScrollHandler.d.ts +27 -0
  130. package/lib/typescript/src/hooks/useDraxScrollHandler.d.ts.map +1 -0
  131. package/lib/typescript/src/hooks/useSortableBoard.d.ts +10 -0
  132. package/lib/typescript/src/hooks/useSortableBoard.d.ts.map +1 -0
  133. package/lib/typescript/src/hooks/useSortableList.d.ts +11 -0
  134. package/lib/typescript/src/hooks/useSortableList.d.ts.map +1 -0
  135. package/lib/typescript/src/hooks/useSpatialIndex.d.ts +22 -0
  136. package/lib/typescript/src/hooks/useSpatialIndex.d.ts.map +1 -0
  137. package/lib/typescript/src/hooks/useViewStyles.d.ts +183 -0
  138. package/lib/typescript/src/hooks/useViewStyles.d.ts.map +1 -0
  139. package/lib/typescript/src/hooks/useWebScrollFreeze.d.ts +14 -0
  140. package/lib/typescript/src/hooks/useWebScrollFreeze.d.ts.map +1 -0
  141. package/lib/typescript/src/index.d.ts +25 -0
  142. package/lib/typescript/src/index.d.ts.map +1 -0
  143. package/lib/typescript/src/math.d.ts +76 -0
  144. package/lib/typescript/src/math.d.ts.map +1 -0
  145. package/{build → lib/typescript/src}/params.d.ts +13 -9
  146. package/lib/typescript/src/params.d.ts.map +1 -0
  147. package/lib/typescript/src/types.d.ts +756 -0
  148. package/lib/typescript/src/types.d.ts.map +1 -0
  149. package/package.json +164 -34
  150. package/src/DebugOverlay.tsx +140 -0
  151. package/src/Drax.ts +33 -0
  152. package/src/DraxContext.ts +8 -0
  153. package/src/DraxHandle.tsx +52 -0
  154. package/src/DraxHandleContext.ts +15 -0
  155. package/src/DraxList.tsx +181 -0
  156. package/src/DraxProvider.tsx +224 -0
  157. package/src/DraxScrollView.tsx +180 -0
  158. package/src/DraxSubprovider.tsx +22 -0
  159. package/src/DraxView.tsx +430 -0
  160. package/src/HoverLayer.tsx +167 -0
  161. package/src/SortableBoardContainer.tsx +439 -0
  162. package/src/SortableBoardContext.ts +6 -0
  163. package/src/SortableContainer.tsx +650 -0
  164. package/src/SortableItem.tsx +264 -0
  165. package/src/SortableItemContext.ts +46 -0
  166. package/src/compat/detectVersion.ts +17 -0
  167. package/src/compat/index.ts +7 -0
  168. package/src/compat/types.ts +35 -0
  169. package/src/compat/useDraxPanGesture.ts +112 -0
  170. package/src/hooks/index.ts +2 -0
  171. package/src/hooks/useCallbackDispatch.tsx +830 -0
  172. package/src/hooks/useDragGesture.ts +273 -0
  173. package/src/hooks/useDraxContext.ts +11 -0
  174. package/src/hooks/useDraxId.ts +11 -0
  175. package/src/hooks/useDraxMethods.ts +71 -0
  176. package/src/hooks/useDraxScrollHandler.ts +121 -0
  177. package/src/hooks/useSortableBoard.ts +44 -0
  178. package/src/hooks/useSortableList.ts +1063 -0
  179. package/src/hooks/useSpatialIndex.ts +336 -0
  180. package/src/hooks/useViewStyles.ts +180 -0
  181. package/src/hooks/useWebScrollFreeze.ts +60 -0
  182. package/src/index.ts +111 -0
  183. package/src/math.ts +333 -0
  184. package/src/params.ts +74 -0
  185. package/src/types.ts +933 -0
  186. package/.editorconfig +0 -15
  187. package/.eslintrc.js +0 -4
  188. package/.prettierrc +0 -16
  189. package/CHANGELOG.md +0 -270
  190. package/CODE-OF-CONDUCT.md +0 -85
  191. package/CONTRIBUTING.md +0 -15
  192. package/FUNDING.yml +0 -4
  193. package/build/AllHoverViews.d.ts +0 -0
  194. package/build/AllHoverViews.js +0 -30
  195. package/build/DraxContext.d.ts +0 -2
  196. package/build/DraxContext.js +0 -6
  197. package/build/DraxList.d.ts +0 -8
  198. package/build/DraxList.js +0 -512
  199. package/build/DraxListItem.d.ts +0 -7
  200. package/build/DraxListItem.js +0 -121
  201. package/build/DraxProvider.d.ts +0 -2
  202. package/build/DraxProvider.js +0 -704
  203. package/build/DraxScrollView.d.ts +0 -6
  204. package/build/DraxScrollView.js +0 -136
  205. package/build/DraxSubprovider.d.ts +0 -3
  206. package/build/DraxSubprovider.js +0 -18
  207. package/build/DraxView.d.ts +0 -8
  208. package/build/DraxView.js +0 -93
  209. package/build/HoverView.d.ts +0 -8
  210. package/build/HoverView.js +0 -40
  211. package/build/PanGestureDetector.d.ts +0 -3
  212. package/build/PanGestureDetector.js +0 -49
  213. package/build/hooks/index.d.ts +0 -4
  214. package/build/hooks/index.js +0 -11
  215. package/build/hooks/useContent.d.ts +0 -23
  216. package/build/hooks/useContent.js +0 -212
  217. package/build/hooks/useDraxContext.d.ts +0 -1
  218. package/build/hooks/useDraxContext.js +0 -13
  219. package/build/hooks/useDraxId.js +0 -13
  220. package/build/hooks/useDraxProtocol.d.ts +0 -5
  221. package/build/hooks/useDraxProtocol.js +0 -32
  222. package/build/hooks/useDraxRegistry.d.ts +0 -78
  223. package/build/hooks/useDraxRegistry.js +0 -714
  224. package/build/hooks/useDraxScrollHandler.d.ts +0 -25
  225. package/build/hooks/useDraxScrollHandler.js +0 -89
  226. package/build/hooks/useDraxState.d.ts +0 -10
  227. package/build/hooks/useDraxState.js +0 -132
  228. package/build/hooks/useMeasurements.d.ts +0 -9
  229. package/build/hooks/useMeasurements.js +0 -119
  230. package/build/hooks/useStatus.d.ts +0 -11
  231. package/build/hooks/useStatus.js +0 -96
  232. package/build/index.d.ts +0 -9
  233. package/build/index.js +0 -33
  234. package/build/math.d.ts +0 -22
  235. package/build/math.js +0 -68
  236. package/build/params.js +0 -27
  237. package/build/transform.d.ts +0 -11
  238. package/build/transform.js +0 -59
  239. package/build/types.d.ts +0 -807
  240. package/build/types.js +0 -46
  241. package/docs/concept.md +0 -79
  242. package/docs/images/color-drag-drop.gif +0 -0
  243. package/docs/images/deck-cards.gif +0 -0
  244. package/docs/images/drag-drop-events.jpg +0 -0
  245. package/docs/images/knight-moves.gif +0 -0
  246. package/docs/images/reorderable-list.gif +0 -0
@@ -0,0 +1,650 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import { useEffect, useLayoutEffect, useRef } from 'react';
3
+ import { Platform, StyleSheet } from 'react-native';
4
+ import type { StyleProp, ViewStyle } from 'react-native';
5
+ import type { SharedValue } from 'react-native-reanimated';
6
+ import Reanimated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';
7
+ import { runOnJS, runOnUI } from 'react-native-worklets';
8
+ import { DraxView } from './DraxView';
9
+ import { useDraxContext } from './hooks/useDraxContext';
10
+ import { useWebScrollFreeze } from './hooks/useWebScrollFreeze';
11
+ import { defaultAutoScrollIntervalLength, ITEM_SHIFT_ANIMATION_DURATION } from './params';
12
+ import { useSortableBoardContext } from './SortableBoardContext';
13
+ import type {
14
+ DropIndicatorProps,
15
+ Position,
16
+ DraxDragEventData,
17
+ DraxMonitorDragDropEventData,
18
+ DraxMonitorEndEventData,
19
+ DraxMonitorEventData,
20
+ DraxProtocolDragEndResponse,
21
+ DraxViewProps,
22
+ SortableListHandle,
23
+ } from './types';
24
+ import {
25
+ AutoScrollDirection,
26
+ isSortableItemPayload,
27
+ isWithCancelledFlag,
28
+ } from './types';
29
+
30
+ /**
31
+ * Touch sensor jitter threshold in pixels.
32
+ * Computes actual finger displacement from drag start and ignores
33
+ * reorder when the finger hasn't meaningfully moved.
34
+ */
35
+ const FINGER_JITTER_THRESHOLD = 5;
36
+
37
+ function computeFingerDisplacement(eventData: DraxDragEventData): number {
38
+ const { grabOffset, measurements } = eventData.dragged;
39
+ if (!measurements) return Infinity;
40
+ const dx =
41
+ grabOffset.x - measurements.width / 2 + eventData.dragTranslation.x;
42
+ const dy =
43
+ grabOffset.y - measurements.height / 2 + eventData.dragTranslation.y;
44
+ return Math.abs(dx) + Math.abs(dy);
45
+ }
46
+
47
+ export interface SortableContainerProps {
48
+ sortable: SortableListHandle<any>;
49
+ scrollRef: RefObject<any>;
50
+ style?: StyleProp<ViewStyle>;
51
+ children: ReactNode;
52
+ draxViewProps?: Partial<DraxViewProps>;
53
+ renderDropIndicator?: (props: DropIndicatorProps) => ReactNode;
54
+ }
55
+
56
+ export const SortableContainer = ({
57
+ sortable,
58
+ scrollRef,
59
+ style,
60
+ children,
61
+ draxViewProps,
62
+ renderDropIndicator,
63
+ }: SortableContainerProps) => {
64
+ const {
65
+ id,
66
+ horizontal,
67
+ draggedItem,
68
+ rawData,
69
+ moveDraggedItem,
70
+ getSnapbackTarget,
71
+ setDraggedItem,
72
+ resetDraggedItem,
73
+ scrollPosition,
74
+ containerMeasurementsRef,
75
+ contentSizeRef,
76
+ autoScrollJumpRatio,
77
+ autoScrollBackThreshold,
78
+ autoScrollForwardThreshold,
79
+ onDragStart: onDragStartCallback,
80
+ onDragPositionChange: onDragPositionChangeCallback,
81
+ onDragEnd: onDragEndCallback,
82
+ onReorder,
83
+ getMeasurementByOriginalIndex,
84
+ dropTargetPositionSV,
85
+ dropTargetVisibleSV,
86
+ draggedDisplayIndexRef,
87
+ dragStartIndexRef,
88
+ initPendingOrder,
89
+ commitVisualOrder,
90
+ computeShiftsForOrder,
91
+ pendingOrderRef,
92
+ committedOrderRef,
93
+ cancelDrag,
94
+ shiftsRef,
95
+ instantClearSV,
96
+ shiftsValidSV,
97
+ getSlotFromPosition,
98
+ } = sortable._internal;
99
+
100
+ // Access hover SharedValues from DraxContext for deferred clearing.
101
+ const {
102
+ hoverReadySV,
103
+ dragPhaseSV,
104
+ draggedIdSV,
105
+ hoverPositionSV,
106
+ hoverClearDeferredRef,
107
+ setHoverContent,
108
+ } = useDraxContext();
109
+
110
+ const boardContext = useSortableBoardContext();
111
+
112
+ useEffect(() => {
113
+ if (!boardContext) return;
114
+ boardContext.registerColumn(id, sortable._internal);
115
+ return () => boardContext.unregisterColumn(id);
116
+ }, [boardContext, id, sortable._internal]);
117
+
118
+ const itemCount = rawData.length;
119
+ const scrollStateRef = useRef(AutoScrollDirection.None);
120
+ const scrollIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(
121
+ undefined
122
+ );
123
+ const draggedToIndex = useRef<number | undefined>(undefined);
124
+ const jitterExceededRef = useRef(false);
125
+ // Track last receiver that triggered a reorder to prevent oscillation.
126
+ // After moveDraggedItem inserts at position R, the receiver shifts to R-1.
127
+ // Next frame, same receiver at R-1 would move backward — skip it.
128
+ const lastMoveReceiverRef = useRef<number | undefined>(undefined);
129
+ const lastMoveDirectionRef = useRef<number>(0); // +1 forward, -1 backward
130
+
131
+
132
+ const { freeze: freezeScroll, unfreeze: unfreezeScroll } = useWebScrollFreeze(scrollRef);
133
+
134
+ // ── Finalize drag (called after snap animation completes) ──────────
135
+
136
+ const finalizeDrag = () => {
137
+ unfreezeScroll();
138
+
139
+ if (boardContext?.boardInternal.transferState.current?.targetId) {
140
+ boardContext.boardInternal.finalizeTransfer?.();
141
+ return;
142
+ }
143
+
144
+ const startIdx = dragStartIndexRef.current;
145
+ const endIdx = draggedDisplayIndexRef.current;
146
+
147
+ const pending = pendingOrderRef.current;
148
+ const didReorder = startIdx !== undefined && endIdx !== undefined
149
+ && startIdx !== endIdx && pending.length > 0;
150
+
151
+ if (didReorder) {
152
+ // Build final data BEFORE clearing refs
153
+ const finalData = pending
154
+ .map((idx) => rawData[idx])
155
+ .filter((item): item is any => item !== undefined);
156
+ const draggedOrigIdx = pending[endIdx];
157
+ const displacedOrigIdx = pending[startIdx];
158
+
159
+ const reorderEvent = {
160
+ data: finalData,
161
+ fromIndex: startIdx,
162
+ toIndex: endIdx,
163
+ fromItem: draggedOrigIdx !== undefined ? rawData[draggedOrigIdx] as any : undefined as any,
164
+ toItem: displacedOrigIdx !== undefined ? rawData[displacedOrigIdx] as any : undefined as any,
165
+ isExternalDrag: false,
166
+ };
167
+
168
+ {
169
+ // ── PERMANENT SHIFTS: blink-free for all contexts ──
170
+ const finalShifts = computeShiftsForOrder(pending) ?? {};
171
+ commitVisualOrder();
172
+
173
+ // Clear JS-thread refs BEFORE the runOnUI block.
174
+ draggedDisplayIndexRef.current = undefined;
175
+ dragStartIndexRef.current = undefined;
176
+ pendingOrderRef.current = [];
177
+
178
+ hoverClearDeferredRef.current = true;
179
+ runOnUI((_shifts: Record<string, Position>) => {
180
+ 'worklet';
181
+ instantClearSV.value = true;
182
+ shiftsValidSV.value = true;
183
+ shiftsRef.value = _shifts;
184
+ draggedItem.value = -1;
185
+ hoverReadySV.value = false;
186
+ dragPhaseSV.value = 'idle';
187
+ draggedIdSV.value = '';
188
+ hoverPositionSV.value = { x: 0, y: 0 };
189
+ runOnJS(setHoverContent)(null);
190
+ })(finalShifts);
191
+
192
+ requestAnimationFrame(() => {
193
+ onReorder(reorderEvent);
194
+ if (boardContext) {
195
+ setTimeout(() => {
196
+ sortable._internal.flushVisualOrder();
197
+ }, 300);
198
+ } else if (Platform.OS === 'web') {
199
+ // On web, flush synchronously after onReorder so FlatList cells
200
+ // move to correct positions immediately. The delayed flush caused
201
+ // races when the user grabbed another item before it fired.
202
+ sortable._internal.flushVisualOrder();
203
+ }
204
+ });
205
+ }
206
+ } else {
207
+ // No reorder — cancel drag: revert to committed shifts + make item visible.
208
+ cancelDrag();
209
+ resetDraggedItem();
210
+ }
211
+ };
212
+
213
+ // Register finalizeDrag via the stable ref so SortableItem always
214
+ // calls the latest version, even if it has a stale _internal reference
215
+ // (e.g., after MATCH path skips FlatList re-render).
216
+ useLayoutEffect(() => {
217
+ sortable._internal.onItemSnapEnd = finalizeDrag;
218
+ }, [sortable._internal, finalizeDrag]);
219
+
220
+ // ── Auto-scroll ─────────────────────────────────────────────────────
221
+
222
+ const doScroll = () => {
223
+ const containerMeasurements = containerMeasurementsRef.current;
224
+ const contentSize = contentSizeRef.current;
225
+ if (!scrollRef.current || !containerMeasurements || !contentSize) return;
226
+
227
+ let containerLength: number;
228
+ let contentLength: number;
229
+ let prevOffset: number;
230
+ if (horizontal) {
231
+ containerLength = containerMeasurements.width;
232
+ contentLength = contentSize.x;
233
+ prevOffset = scrollPosition.value.x;
234
+ } else {
235
+ containerLength = containerMeasurements.height;
236
+ contentLength = contentSize.y;
237
+ prevOffset = scrollPosition.value.y;
238
+ }
239
+
240
+ const jumpLength = containerLength * autoScrollJumpRatio;
241
+ let offset: number | undefined;
242
+ if (scrollStateRef.current === AutoScrollDirection.Forward) {
243
+ const maxOffset = contentLength - containerLength;
244
+ if (prevOffset < maxOffset) {
245
+ offset = Math.min(prevOffset + jumpLength, maxOffset);
246
+ }
247
+ } else if (scrollStateRef.current === AutoScrollDirection.Back) {
248
+ if (prevOffset > 0) {
249
+ offset = Math.max(prevOffset - jumpLength, 0);
250
+ }
251
+ }
252
+
253
+ if (offset !== undefined) {
254
+ if (scrollRef.current.scrollToOffset) {
255
+ // FlatList / FlashList / LegendList
256
+ scrollRef.current.scrollToOffset({ offset });
257
+ } else if (scrollRef.current.scrollTo) {
258
+ // ScrollView
259
+ scrollRef.current.scrollTo(
260
+ horizontal ? { x: offset } : { y: offset },
261
+ );
262
+ }
263
+ if (scrollRef.current.flashScrollIndicators) {
264
+ scrollRef.current.flashScrollIndicators();
265
+ }
266
+ }
267
+ };
268
+
269
+ const startScroll = () => {
270
+ if (scrollIntervalRef.current) return;
271
+ doScroll();
272
+ scrollIntervalRef.current = setInterval(
273
+ doScroll,
274
+ defaultAutoScrollIntervalLength
275
+ );
276
+ };
277
+
278
+ const stopScroll = () => {
279
+ if (scrollIntervalRef.current) {
280
+ clearInterval(scrollIntervalRef.current);
281
+ scrollIntervalRef.current = undefined;
282
+ }
283
+ };
284
+
285
+ // ── Internal drag end handler ───────────────────────────────────────
286
+
287
+ const handleInternalDragEnd = (
288
+ eventData:
289
+ | DraxMonitorEventData
290
+ | DraxMonitorEndEventData
291
+ | DraxMonitorDragDropEventData,
292
+ totalDragEnd: boolean
293
+ ): DraxProtocolDragEndResponse => {
294
+ scrollStateRef.current = AutoScrollDirection.None;
295
+ stopScroll();
296
+ unfreezeScroll();
297
+ dropTargetVisibleSV.value = false;
298
+
299
+ const { dragged, receiver } = eventData;
300
+ const draggedPayload = isSortableItemPayload(dragged.payload)
301
+ ? dragged.payload
302
+ : undefined;
303
+ const externalDrag = dragged.parentId !== id || !draggedPayload;
304
+
305
+ const fromIndex = dragStartIndexRef.current ?? draggedPayload?.index ?? 0;
306
+ const fromOriginalIndex = draggedPayload?.originalIndex ?? fromIndex;
307
+ const fromItem = externalDrag ? undefined : rawData[fromOriginalIndex];
308
+
309
+ const receiverPayload = isSortableItemPayload(receiver?.payload)
310
+ ? receiver?.payload
311
+ : undefined;
312
+ const toPayload =
313
+ receiver?.parentId === id ? receiverPayload : undefined;
314
+
315
+ if (totalDragEnd) {
316
+ onDragEndCallback?.({
317
+ index: fromIndex,
318
+ item: fromItem as any,
319
+ toIndex: draggedDisplayIndexRef.current,
320
+ cancelled: isWithCancelledFlag(eventData)
321
+ ? eventData.cancelled
322
+ : false,
323
+ });
324
+ }
325
+
326
+ // Reset drag position tracking
327
+ if (draggedToIndex.current !== undefined) {
328
+ if (!totalDragEnd) {
329
+ onDragPositionChangeCallback?.({
330
+ index: fromIndex,
331
+ item: fromItem as any,
332
+ toIndex: undefined,
333
+ previousIndex: draggedToIndex.current,
334
+ });
335
+ }
336
+ draggedToIndex.current = undefined;
337
+ }
338
+
339
+ // User hasn't moved — skip reorder, snap back to origin.
340
+ // Don't clean up here — finalizeDrag handles it after snap completes.
341
+ if (
342
+ toPayload !== undefined &&
343
+ computeFingerDisplacement(eventData) < FINGER_JITTER_THRESHOLD
344
+ ) {
345
+ return undefined;
346
+ }
347
+
348
+ // Reorder happened — return snap target. Don't commit yet;
349
+ // finalizeDrag commits after the snap animation completes so
350
+ // the hover covers any FlatList re-render.
351
+ if (totalDragEnd && draggedDisplayIndexRef.current !== undefined) {
352
+ // Shifts stay active during snap animation — items remain at their
353
+ // shifted positions. finalizeDrag will set permanent shifts after snap.
354
+ const snapbackTarget = getSnapbackTarget();
355
+ return snapbackTarget;
356
+ }
357
+
358
+ // Dropped on a receiver outside this sortable list
359
+ if (receiver && receiver.parentId !== id) {
360
+ return undefined;
361
+ }
362
+
363
+ // External drag (item from another container) with no reorder — snap back
364
+ // to original position. Without this, getMeasurementByOriginalIndex would
365
+ // look up the wrong item in this container's data by the source index.
366
+ if (externalDrag) {
367
+ return undefined;
368
+ }
369
+
370
+ // No receiver — snap back to the dragged item's current position
371
+ const containerMeasurements = containerMeasurementsRef.current;
372
+ const fromMeas = getMeasurementByOriginalIndex(fromOriginalIndex);
373
+ if (fromMeas && containerMeasurements) {
374
+ return {
375
+ x: containerMeasurements.x + fromMeas.x - scrollPosition.value.x,
376
+ y: containerMeasurements.y + fromMeas.y - scrollPosition.value.y,
377
+ };
378
+ }
379
+ return undefined;
380
+ };
381
+
382
+ // ── Monitor callbacks ───────────────────────────────────────────────
383
+
384
+ const onMonitorDragStart = (eventData: DraxMonitorEventData) => {
385
+ draxViewProps?.onMonitorDragStart?.(eventData);
386
+ jitterExceededRef.current = false;
387
+ lastMoveReceiverRef.current = undefined;
388
+ lastMoveDirectionRef.current = 0;
389
+ // Clear any stale freeze from a previous drag that failed to unfreeze
390
+ // (e.g., fast cross-container gesture where onMonitorDragExit was skipped).
391
+ unfreezeScroll();
392
+ freezeScroll();
393
+
394
+ const { dragged } = eventData;
395
+
396
+ // No guard on draggedItem.value — Reanimated 4 doesn't reliably sync
397
+ // SharedValue writes from runOnUI worklets, so the value may be stale.
398
+ // onMonitorDragStart only fires at the start of a new gesture, so
399
+ // setDraggedItem + initPendingOrder safely overwrite any stale state.
400
+
401
+ if (
402
+ dragged.parentId === id &&
403
+ isSortableItemPayload(dragged.payload)
404
+ ) {
405
+ const { index, originalIndex } = dragged.payload;
406
+ setDraggedItem(originalIndex);
407
+ // Initialize pending order BEFORE setting display index.
408
+ // initPendingOrder copies the committed visual order into pendingOrderRef.
409
+ initPendingOrder();
410
+ // Map FlatList index to committed visual order position.
411
+ // With stableData, FlatList renders original data and permanent shifts
412
+ // handle visual order. The dragged item at FlatList cell `index`
413
+ // may be at a different visual position in the committed order.
414
+ const committed = committedOrderRef.current;
415
+ let displayIndex = index;
416
+ if (committed.length > 0) {
417
+ const pos = committed.indexOf(originalIndex);
418
+ if (pos >= 0) displayIndex = pos;
419
+ }
420
+ draggedDisplayIndexRef.current = displayIndex;
421
+ dragStartIndexRef.current = displayIndex;
422
+ // Item visibility is controlled by hoverReadySV from DraxContext.
423
+ onDragStartCallback?.({
424
+ index: displayIndex,
425
+ item: rawData[originalIndex] as any,
426
+ });
427
+ }
428
+ };
429
+
430
+ const onMonitorDragOver = (eventData: DraxMonitorEventData) => {
431
+ const displacement = computeFingerDisplacement(eventData);
432
+ if (!jitterExceededRef.current) {
433
+ if (displacement < FINGER_JITTER_THRESHOLD) {
434
+ draxViewProps?.onMonitorDragOver?.(eventData);
435
+ return;
436
+ }
437
+ jitterExceededRef.current = true;
438
+ // Item visibility is now controlled by hoverReadySV from DraxContext —
439
+ // SortableItem hides when hoverReadySV && draggedIdSV match.
440
+ // No need for setDraggedKey here.
441
+ }
442
+
443
+ draxViewProps?.onMonitorDragOver?.(eventData);
444
+
445
+ const { dragged, monitorOffset, monitorOffsetRatio } = eventData;
446
+ const draggedPayload = isSortableItemPayload(dragged.payload)
447
+ ? dragged.payload
448
+ : undefined;
449
+ const externalDrag = dragged.parentId !== id || !draggedPayload;
450
+ const fromIndex = dragStartIndexRef.current ?? draggedPayload?.index ?? 0;
451
+ const fromItem = externalDrag
452
+ ? undefined
453
+ : rawData[draggedPayload?.originalIndex ?? fromIndex];
454
+
455
+ if (typeof draggedItem.value !== 'number' || draggedItem.value < 0) {
456
+ setDraggedItem(itemCount);
457
+ }
458
+
459
+ // ── Position-based slot detection ──────────────────────────────────
460
+ // Use the hover center's content position. Slot boundaries are based
461
+ // on original layout positions (stable, never shift during drag).
462
+ const contentPos = {
463
+ x: monitorOffset.x + scrollPosition.value.x,
464
+ y: monitorOffset.y + scrollPosition.value.y,
465
+ };
466
+ const targetSlot = getSlotFromPosition(contentPos);
467
+
468
+ // Track drag position changes (log only on slot change to avoid per-frame noise)
469
+ if (targetSlot !== draggedToIndex.current) {
470
+ onDragPositionChangeCallback?.({
471
+ toIndex: targetSlot,
472
+ index: fromIndex,
473
+ item: fromItem as any,
474
+ previousIndex: draggedToIndex.current,
475
+ });
476
+ draggedToIndex.current = targetSlot;
477
+
478
+ // Update drop indicator
479
+ if (renderDropIndicator) {
480
+ const pending = pendingOrderRef.current;
481
+ const slotOrigIdx = pending.length > targetSlot ? pending[targetSlot] : undefined;
482
+ const toMeas = slotOrigIdx !== undefined
483
+ ? getMeasurementByOriginalIndex(slotOrigIdx)
484
+ : undefined;
485
+ if (toMeas) {
486
+ const currentDragIdx = draggedDisplayIndexRef.current ?? fromIndex;
487
+ const isForward = currentDragIdx < targetSlot;
488
+ if (horizontal) {
489
+ dropTargetPositionSV.value = {
490
+ x: isForward ? toMeas.x + toMeas.width : toMeas.x,
491
+ y: toMeas.y,
492
+ };
493
+ } else {
494
+ dropTargetPositionSV.value = {
495
+ x: toMeas.x,
496
+ y: isForward ? toMeas.y + toMeas.height : toMeas.y,
497
+ };
498
+ }
499
+ dropTargetVisibleSV.value = true;
500
+ }
501
+ } else {
502
+ dropTargetVisibleSV.value = false;
503
+ }
504
+ }
505
+
506
+ // Reorder via position-based slot (not receiver-based).
507
+ // Receiver detection uses the spatial index which stores FlatList layout
508
+ // positions. With stableData, these become stale after the first reorder
509
+ // because shifts move items visually but don't update the spatial index.
510
+ const currentDragIdx = draggedDisplayIndexRef.current;
511
+ if (currentDragIdx !== undefined && targetSlot !== currentDragIdx) {
512
+ const direction = Math.sign(targetSlot - currentDragIdx);
513
+ const sameTarget = lastMoveReceiverRef.current === targetSlot;
514
+ const wouldReverse = sameTarget && direction !== 0
515
+ && direction !== lastMoveDirectionRef.current;
516
+
517
+ if (!wouldReverse) {
518
+ if (direction !== 0) {
519
+ lastMoveReceiverRef.current = targetSlot;
520
+ lastMoveDirectionRef.current = direction;
521
+ }
522
+ moveDraggedItem(targetSlot);
523
+ }
524
+ }
525
+
526
+ // Auto-scroll
527
+ const ratio = horizontal ? monitorOffsetRatio.x : monitorOffsetRatio.y;
528
+ if (ratio > autoScrollBackThreshold && ratio < autoScrollForwardThreshold) {
529
+ scrollStateRef.current = AutoScrollDirection.None;
530
+ stopScroll();
531
+ } else {
532
+ if (ratio >= autoScrollForwardThreshold) {
533
+ scrollStateRef.current = AutoScrollDirection.Forward;
534
+ } else if (ratio <= autoScrollBackThreshold) {
535
+ scrollStateRef.current = AutoScrollDirection.Back;
536
+ }
537
+ startScroll();
538
+ }
539
+ };
540
+
541
+ const onMonitorDragExit = (eventData: DraxMonitorEventData) => {
542
+ stopScroll();
543
+ if (scrollIntervalRef.current) {
544
+ draxViewProps?.onMonitorDragExit?.(eventData);
545
+ return;
546
+ }
547
+ handleInternalDragEnd(eventData, false);
548
+ draxViewProps?.onMonitorDragExit?.(eventData);
549
+ };
550
+
551
+ const onMonitorDragEnd = (eventData: DraxMonitorEndEventData) => {
552
+ if (boardContext?.boardInternal.transferState.current?.targetId) {
553
+ unfreezeScroll();
554
+ draxViewProps?.onMonitorDragEnd?.(eventData);
555
+ return undefined;
556
+ }
557
+ const defaultSnapbackTarget = handleInternalDragEnd(eventData, true);
558
+ const providedSnapTarget =
559
+ draxViewProps?.onMonitorDragEnd?.(eventData);
560
+
561
+ return providedSnapTarget ?? defaultSnapbackTarget;
562
+ };
563
+
564
+ const onMonitorDragDrop = (eventData: DraxMonitorDragDropEventData) => {
565
+ if (boardContext?.boardInternal.transferState.current?.targetId) {
566
+ unfreezeScroll();
567
+ draxViewProps?.onMonitorDragDrop?.(eventData);
568
+ return undefined;
569
+ }
570
+ const defaultSnapbackTarget = handleInternalDragEnd(eventData, true);
571
+ const providedSnapTarget =
572
+ draxViewProps?.onMonitorDragDrop?.(eventData);
573
+
574
+ return providedSnapTarget ?? defaultSnapbackTarget;
575
+ };
576
+
577
+ const handleMeasure = (event: any) => {
578
+ draxViewProps?.onMeasure?.(event);
579
+ containerMeasurementsRef.current = event;
580
+ };
581
+
582
+ return (
583
+ <DraxView
584
+ {...draxViewProps}
585
+ style={[draxViewProps?.style, style]}
586
+ id={id}
587
+ isParent
588
+ scrollPosition={scrollPosition}
589
+ monitoring
590
+ onMeasure={handleMeasure}
591
+ onMonitorDragStart={onMonitorDragStart}
592
+ onMonitorDragOver={onMonitorDragOver}
593
+ onMonitorDragExit={onMonitorDragExit}
594
+ onMonitorDragEnd={onMonitorDragEnd}
595
+ onMonitorDragDrop={onMonitorDragDrop}
596
+ >
597
+ {children}
598
+ {renderDropIndicator && (
599
+ <DropIndicatorOverlay
600
+ dropTargetPositionSV={dropTargetPositionSV}
601
+ dropTargetVisibleSV={dropTargetVisibleSV}
602
+ horizontal={horizontal}
603
+ renderDropIndicator={renderDropIndicator}
604
+ />
605
+ )}
606
+ </DraxView>
607
+ );
608
+ };
609
+
610
+ /** Extracted so useAnimatedStyle is always called when the component mounts. */
611
+ const DropIndicatorOverlay = ({
612
+ dropTargetPositionSV,
613
+ dropTargetVisibleSV,
614
+ horizontal,
615
+ renderDropIndicator,
616
+ }: {
617
+ dropTargetPositionSV: SharedValue<Position>;
618
+ dropTargetVisibleSV: SharedValue<boolean>;
619
+ horizontal: boolean;
620
+ renderDropIndicator: (props: DropIndicatorProps) => ReactNode;
621
+ }) => {
622
+ const indicatorStyle = useAnimatedStyle(() => {
623
+ const pos = dropTargetPositionSV.value;
624
+ const visible = dropTargetVisibleSV.value;
625
+ return {
626
+ opacity: visible ? 1 : 0,
627
+ transform: [
628
+ { translateX: withTiming(pos.x, { duration: ITEM_SHIFT_ANIMATION_DURATION }) },
629
+ { translateY: withTiming(pos.y, { duration: ITEM_SHIFT_ANIMATION_DURATION }) },
630
+ ] as const,
631
+ };
632
+ });
633
+
634
+ return (
635
+ <Reanimated.View
636
+ style={[dropIndicatorStyles.container, indicatorStyle]}
637
+ pointerEvents="none"
638
+ >
639
+ {renderDropIndicator({ visible: true, horizontal })}
640
+ </Reanimated.View>
641
+ );
642
+ };
643
+
644
+ const dropIndicatorStyles = StyleSheet.create({
645
+ container: {
646
+ position: 'absolute',
647
+ top: 0,
648
+ left: 0,
649
+ },
650
+ });