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,988 @@
1
+ "use strict";
2
+
3
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
4
+ import { useSharedValue } from 'react-native-reanimated';
5
+ import { runOnUI } from 'react-native-worklets';
6
+ import { defaultAutoScrollBackThreshold, defaultAutoScrollForwardThreshold, defaultAutoScrollJumpRatio, defaultListItemLongPressDelay } from "../params.js";
7
+ import { packGrid } from "../math.js";
8
+ import { DraxSnapbackTargetPreset } from "../types.js";
9
+ import { useDraxId } from "./useDraxId.js";
10
+
11
+ /** Stable identity — avoids FlatList cell unmounting on data reorder. */
12
+ function useStableKeyExtractor() {
13
+ return useCallback((_item, index) => `__drax_${index}`, []);
14
+ }
15
+
16
+ /**
17
+ * Core hook for list-agnostic sortable reordering.
18
+ *
19
+ * During drag, order changes are tracked in a ref (no React re-render)
20
+ * and items are visually repositioned via shift transforms (SharedValues).
21
+ * The data reorder is committed to state only on drop, while the hover
22
+ * view covers any layout transition.
23
+ */
24
+ export const useSortableList = options => {
25
+ const {
26
+ data: rawData,
27
+ keyExtractor,
28
+ onReorder,
29
+ horizontal = false,
30
+ numColumns = 1,
31
+ reorderStrategy = 'insert',
32
+ longPressDelay = defaultListItemLongPressDelay,
33
+ lockToMainAxis = false,
34
+ autoScrollJumpRatio = defaultAutoScrollJumpRatio,
35
+ autoScrollBackThreshold = defaultAutoScrollBackThreshold,
36
+ autoScrollForwardThreshold = defaultAutoScrollForwardThreshold,
37
+ animationConfig = 'default',
38
+ getItemSpan,
39
+ inactiveItemStyle,
40
+ itemEntering,
41
+ itemExiting,
42
+ onDragStart,
43
+ onDragPositionChange,
44
+ onDragEnd
45
+ } = options;
46
+ const id = useDraxId(options.id);
47
+
48
+ // ── Fixed items tracking ────────────────────────────────────────────
49
+ const fixedKeys = useRef(new Set());
50
+
51
+ // ── SharedValues (UI-thread state) ────────────────────────────────
52
+ const draggedItem = useSharedValue(undefined);
53
+ const dropTargetPositionSV = useSharedValue({
54
+ x: 0,
55
+ y: 0
56
+ });
57
+ const dropTargetVisibleSV = useSharedValue(false);
58
+
59
+ /**
60
+ * Per-item shift transforms keyed by item key.
61
+ * Written from JS thread during drag, read on UI thread via useAnimatedStyle.
62
+ */
63
+ const shiftsRef = useSharedValue({});
64
+
65
+ /**
66
+ * When true, SortableItem applies shifts with duration 0 (instant).
67
+ * Set during reorder commit so items don't animate from old shift→0
68
+ * while the FlatList re-renders (which would cause a double-offset flash).
69
+ * Reset at the start of the next drag session.
70
+ */
71
+ const instantClearSV = useSharedValue(false);
72
+
73
+ /**
74
+ * When false, SortableItem ignores all shifts (treats them as 0).
75
+ * Written SYNCHRONOUSLY from useLayoutEffect (direct JSI write) when
76
+ * rawData changes, so the animated style picks it up in the same UI
77
+ * frame as the Fabric commit. This prevents the 1-frame blink where
78
+ * cells show new content but the animated style still has stale shifts.
79
+ */
80
+ const shiftsValidSV = useSharedValue(true);
81
+
82
+ // ── JS-thread state ───────────────────────────────────────────────
83
+ const [originalIndexes, setOriginalIndexes] = useState([]);
84
+
85
+ /**
86
+ * Buffered FlatList data. Only updated on external data changes, NOT on
87
+ * accepted reorders. This ensures FlatList never re-renders on reorder
88
+ * commit, eliminating the Fabric-vs-Reanimated race that caused the blink.
89
+ */
90
+ const [stableData, setStableData] = useState(rawData);
91
+
92
+ // Always-current rawData for deferred flushVisualOrder
93
+ const rawDataRef = useRef(rawData);
94
+ rawDataRef.current = rawData;
95
+ const itemMeasurements = useRef(new Map());
96
+ const containerMeasurementsRef = useRef(undefined);
97
+ const contentSizeRef = useRef(undefined);
98
+ const scrollPosition = useSharedValue({
99
+ x: 0,
100
+ y: 0
101
+ });
102
+
103
+ // ── Drag tracking (refs, no re-render) ────────────────────────────
104
+ const draggedDisplayIndexRef = useRef(undefined);
105
+ const dragStartIndexRef = useRef(undefined);
106
+ /**
107
+ * Pending reorder during drag. Tracks the desired display order
108
+ * as indices into rawData. Updated by moveDraggedItem (ref, not state).
109
+ */
110
+ const pendingOrderRef = useRef([]);
111
+
112
+ /**
113
+ * Committed visual order — the pending order from the last completed drag.
114
+ * FlatList data is NOT changed on reorder; items are positioned entirely
115
+ * via shifts. This ref allows the next drag to start from the visual state.
116
+ * Empty means FlatList data matches the visual order (identity).
117
+ */
118
+ const committedOrderRef = useRef([]);
119
+ /** Shifts corresponding to the committed visual order (for cancel revert). */
120
+ const committedShiftsRef = useRef({});
121
+ /** Item keys in committed visual order — detects when parent data matches. */
122
+ const committedKeyOrderRef = useRef([]);
123
+ /** Cross-container: phantom slot for incoming items */
124
+ const phantomRef = useRef(undefined);
125
+ /** Cross-container: off-screen shifts for transferred items */
126
+ const ghostShiftsRef = useRef({});
127
+ /** When true, the next useLayoutEffect RESET skips the sync shiftsValidSV=false
128
+ * write. Set by board-path finalizeDrag which keeps the hover visible to cover
129
+ * the transition — the sync write would prematurely zero shifts on other items. */
130
+ const skipShiftsInvalidationRef = useRef(false);
131
+
132
+ // ── Handle data changes ──────────────────────────────────────────────
133
+ // With permanent shifts, FlatList data is NOT changed on reorder.
134
+ // When rawData changes (parent updated state after onReorder, or external
135
+ // data change), check if it matches the committed visual order. If so,
136
+ // clear shifts (items now at correct FlatList positions). Otherwise reset.
137
+ useLayoutEffect(() => {
138
+ // Always keep originalIndexes as identity — permanent shifts handle visual order.
139
+ setOriginalIndexes(prev => {
140
+ const isIdentity = prev.length === rawData.length && prev.every((v, i) => v === i);
141
+ if (isIdentity) return prev;
142
+ return rawData.length > 0 ? [...Array(rawData.length).keys()] : [];
143
+ });
144
+ const committedKeys = committedKeyOrderRef.current;
145
+ if (committedKeys.length > 0 && committedKeys.length === rawData.length) {
146
+ // Check if new data order matches committed visual order by keys.
147
+ let matches = true;
148
+ for (let i = 0; i < rawData.length; i++) {
149
+ const item = rawData[i];
150
+ if (item === undefined || keyExtractor(item, i) !== committedKeys[i]) {
151
+ matches = false;
152
+ break;
153
+ }
154
+ }
155
+ if (matches) {
156
+ // Parent accepted our reorder — stableData stays unchanged.
157
+ // FlatList keeps rendering the original data order; permanent shifts
158
+ // handle the visual reorder. No Fabric commit → no race → no blink.
159
+ return;
160
+ }
161
+ }
162
+
163
+ // External data change or initial mount — update stableData and reset.
164
+ setStableData(rawData);
165
+ committedOrderRef.current = [];
166
+ committedKeyOrderRef.current = [];
167
+ committedShiftsRef.current = {};
168
+ ghostShiftsRef.current = {};
169
+ if (skipShiftsInvalidationRef.current) {
170
+ // Board-path reorder: hover covers the transition.
171
+ skipShiftsInvalidationRef.current = false;
172
+ instantClearSV.value = true;
173
+ shiftsRef.value = {};
174
+ } else {
175
+ // External data change: invalidate shifts immediately so the animated
176
+ // style reads zero shifts in the same frame as the Fabric commit.
177
+ shiftsValidSV.value = false;
178
+ runOnUI(() => {
179
+ 'worklet';
180
+
181
+ instantClearSV.value = true;
182
+ shiftsRef.value = {};
183
+ shiftsValidSV.value = true;
184
+ })();
185
+ }
186
+ }, [rawData, keyExtractor, shiftsRef, instantClearSV, shiftsValidSV]);
187
+
188
+ // ── Helpers ─────────────────────────────────────────────────────────
189
+
190
+ const getMeasurementByOriginalIndex = originalIndex => {
191
+ const item = stableData[originalIndex];
192
+ if (item === undefined) return undefined;
193
+ const key = keyExtractor(item, originalIndex);
194
+ return itemMeasurements.current.get(key);
195
+ };
196
+
197
+ // Alias for internal use
198
+ const getMeasForOrigIdx = getMeasurementByOriginalIndex;
199
+
200
+ /** Get the span for an item at the given original data index */
201
+ const getSpanForOrigIdx = origIdx => {
202
+ if (!getItemSpan) return {
203
+ colSpan: 1,
204
+ rowSpan: 1
205
+ };
206
+ const item = stableData[origIdx];
207
+ if (item === undefined) return {
208
+ colSpan: 1,
209
+ rowSpan: 1
210
+ };
211
+ return getItemSpan(item, origIdx);
212
+ };
213
+
214
+ /**
215
+ * Derive grid geometry (cell size + gaps) from current measurements.
216
+ * Only used when getItemSpan is provided and numColumns > 1.
217
+ */
218
+ const deriveGridGeometry = () => {
219
+ if (!getItemSpan || originalIndexes.length === 0) return undefined;
220
+ const firstOrigIdx = originalIndexes[0];
221
+ const startMeas = firstOrigIdx !== undefined ? getMeasForOrigIdx(firstOrigIdx) : undefined;
222
+ if (!startMeas) return undefined;
223
+
224
+ // Pack original order to know grid positions for gap derivation
225
+ const origPacking = packGrid(originalIndexes.length, numColumns, displayIdx => getSpanForOrigIdx(originalIndexes[displayIdx]));
226
+
227
+ // Find cell dimensions from measurements of items with span 1
228
+ let cellWidth;
229
+ let cellHeight;
230
+ for (let i = 0; i < originalIndexes.length; i++) {
231
+ const origIdx = originalIndexes[i];
232
+ const span = getSpanForOrigIdx(origIdx);
233
+ const meas = getMeasForOrigIdx(origIdx);
234
+ if (!meas) continue;
235
+ if (span.colSpan === 1 && cellWidth === undefined) cellWidth = meas.width;
236
+ if (span.rowSpan === 1 && cellHeight === undefined) cellHeight = meas.height;
237
+ if (cellWidth !== undefined && cellHeight !== undefined) break;
238
+ }
239
+
240
+ // Fallback: derive from first item divided by its span
241
+ if (cellWidth === undefined || cellHeight === undefined) {
242
+ const firstSpan = getSpanForOrigIdx(firstOrigIdx);
243
+ if (cellWidth === undefined) cellWidth = startMeas.width / firstSpan.colSpan;
244
+ if (cellHeight === undefined) cellHeight = startMeas.height / firstSpan.rowSpan;
245
+ }
246
+
247
+ // Derive column gap from two items at different grid columns
248
+ let colGap = 0;
249
+ for (let i = 0; i < origPacking.positions.length && colGap === 0; i++) {
250
+ for (let j = i + 1; j < origPacking.positions.length; j++) {
251
+ const pi = origPacking.positions[i];
252
+ const pj = origPacking.positions[j];
253
+ if (pi.col !== pj.col) {
254
+ const mi = getMeasForOrigIdx(originalIndexes[i]);
255
+ const mj = getMeasForOrigIdx(originalIndexes[j]);
256
+ if (mi && mj) {
257
+ const colDiff = Math.abs(pj.col - pi.col);
258
+ const xDiff = Math.abs(mj.x - mi.x);
259
+ colGap = xDiff / colDiff - cellWidth;
260
+ break;
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // Derive row gap from two items at different grid rows
267
+ let rowGap = 0;
268
+ for (let i = 0; i < origPacking.positions.length && rowGap === 0; i++) {
269
+ for (let j = i + 1; j < origPacking.positions.length; j++) {
270
+ const pi = origPacking.positions[i];
271
+ const pj = origPacking.positions[j];
272
+ if (pi.row !== pj.row) {
273
+ const mi = getMeasForOrigIdx(originalIndexes[i]);
274
+ const mj = getMeasForOrigIdx(originalIndexes[j]);
275
+ if (mi && mj) {
276
+ const rowDiff = Math.abs(pj.row - pi.row);
277
+ const yDiff = Math.abs(mj.y - mi.y);
278
+ rowGap = yDiff / rowDiff - cellHeight;
279
+ break;
280
+ }
281
+ }
282
+ }
283
+ }
284
+ return {
285
+ cellWidth,
286
+ cellHeight,
287
+ colGap: Math.max(colGap, 0),
288
+ rowGap: Math.max(rowGap, 0),
289
+ startX: startMeas.x,
290
+ startY: startMeas.y
291
+ };
292
+ };
293
+
294
+ // ── Shift application (merges ghost shifts for cross-container) ──
295
+
296
+ const applyShifts = shifts => {
297
+ if (!shifts) return;
298
+ const ghosts = ghostShiftsRef.current;
299
+ if (Object.keys(ghosts).length > 0) {
300
+ shiftsRef.value = {
301
+ ...shifts,
302
+ ...ghosts
303
+ };
304
+ } else {
305
+ shiftsRef.value = shifts;
306
+ }
307
+ };
308
+
309
+ // ── Shift computation ─────────────────────────────────────────────
310
+
311
+ /**
312
+ * Compute the gap between items from current FlatList measurements.
313
+ * Uses the first two items in originalIndexes to detect separator/padding.
314
+ */
315
+ const computeItemGap = () => {
316
+ if (originalIndexes.length < 2) return 0;
317
+ const meas0 = getMeasForOrigIdx(originalIndexes[0]);
318
+ const meas1 = getMeasForOrigIdx(originalIndexes[1]);
319
+ if (!meas0 || !meas1) return 0;
320
+ if (numColumns > 1) {
321
+ // Grid: gap between rows (check items in different rows)
322
+ const firstRowEnd = Math.min(numColumns, originalIndexes.length);
323
+ if (originalIndexes.length > firstRowEnd) {
324
+ const lastInRow0 = getMeasForOrigIdx(originalIndexes[firstRowEnd - 1]);
325
+ const firstInRow1 = getMeasForOrigIdx(originalIndexes[firstRowEnd]);
326
+ if (lastInRow0 && firstInRow1) {
327
+ return horizontal ? firstInRow1.x - (lastInRow0.x + lastInRow0.width) : firstInRow1.y - (lastInRow0.y + lastInRow0.height);
328
+ }
329
+ }
330
+ return 0;
331
+ }
332
+
333
+ // List: gap = nextItem.y - (thisItem.y + thisItem.height)
334
+ return horizontal ? meas1.x - (meas0.x + meas0.width) : meas1.y - (meas0.y + meas0.height);
335
+ };
336
+
337
+ /**
338
+ * Compute shifts for items in the given order. Returns a map of
339
+ * item key → {x, y} shift, or undefined if measurements are missing.
340
+ *
341
+ * @param order Array of original data indices in desired display order
342
+ * @param skipIndex Optional display index to skip (dragged item during drag)
343
+ */
344
+ const computeShiftsForOrder = (order, skipIndex, phantom) => {
345
+ if (order.length === 0) return undefined;
346
+ const measurements = order.map(origIdx => getMeasForOrigIdx(origIdx));
347
+ const missingShiftIdx = measurements.findIndex(m => !m);
348
+ if (missingShiftIdx >= 0) {
349
+ return undefined;
350
+ }
351
+ const gap = computeItemGap();
352
+ const firstOrigIdx = originalIndexes[0];
353
+ const startMeas = firstOrigIdx !== undefined ? getMeasForOrigIdx(firstOrigIdx) : undefined;
354
+ if (!startMeas) {
355
+ return undefined;
356
+ }
357
+ const targetPositions = new Map();
358
+ if (numColumns <= 1) {
359
+ let cursor = horizontal ? startMeas.x : startMeas.y;
360
+ let displaySlot = 0;
361
+ for (let i = 0; i < order.length; i++) {
362
+ // Reserve space for phantom before laying out this item
363
+ if (phantom && displaySlot === phantom.atDisplayIndex) {
364
+ cursor += (horizontal ? phantom.width : phantom.height) + gap;
365
+ displaySlot++;
366
+ }
367
+ const meas = measurements[i];
368
+ if (horizontal) {
369
+ targetPositions.set(i, {
370
+ x: cursor,
371
+ y: startMeas.y
372
+ });
373
+ cursor += meas.width + gap;
374
+ } else {
375
+ targetPositions.set(i, {
376
+ x: startMeas.x,
377
+ y: cursor
378
+ });
379
+ cursor += meas.height + gap;
380
+ }
381
+ displaySlot++;
382
+ }
383
+ } else if (getItemSpan) {
384
+ // ── Mixed-size grid: bin-pack items into a 2D occupancy grid ──
385
+ const geo = deriveGridGeometry();
386
+ if (!geo) return undefined;
387
+ const packing = packGrid(order.length, numColumns, displayIdx => getSpanForOrigIdx(order[displayIdx]));
388
+ for (let i = 0; i < order.length; i++) {
389
+ const gp = packing.positions[i];
390
+ targetPositions.set(i, {
391
+ x: geo.startX + gp.col * (geo.cellWidth + geo.colGap),
392
+ y: geo.startY + gp.row * (geo.cellHeight + geo.rowGap)
393
+ });
394
+ }
395
+ } else {
396
+ // ── Uniform grid: col = i % numColumns ──
397
+ let cursorY = startMeas.y;
398
+ const colXPositions = [];
399
+ for (let c = 0; c < numColumns && c < originalIndexes.length; c++) {
400
+ const colMeas = getMeasForOrigIdx(originalIndexes[c]);
401
+ colXPositions.push(colMeas ? colMeas.x : 0);
402
+ }
403
+ for (let i = 0; i < order.length; i++) {
404
+ const col = i % numColumns;
405
+ targetPositions.set(i, {
406
+ x: colXPositions[col] ?? 0,
407
+ y: cursorY
408
+ });
409
+ if (col === numColumns - 1 || i === order.length - 1) {
410
+ const rowStart = i - col;
411
+ let rowHeight = 0;
412
+ for (let j = rowStart; j <= i; j++) {
413
+ rowHeight = Math.max(rowHeight, measurements[j].height);
414
+ }
415
+ cursorY += rowHeight + gap;
416
+ }
417
+ }
418
+ }
419
+ const newShifts = {};
420
+ for (let i = 0; i < order.length; i++) {
421
+ if (skipIndex !== undefined && i === skipIndex) continue;
422
+ const origIdx = order[i];
423
+ const item = stableData[origIdx];
424
+ if (item === undefined) continue;
425
+ const key = keyExtractor(item, origIdx);
426
+ const currentMeas = getMeasForOrigIdx(origIdx);
427
+ if (!currentMeas) continue;
428
+ const target = targetPositions.get(i);
429
+ if (!target) continue;
430
+ const dx = target.x - currentMeas.x;
431
+ const dy = target.y - currentMeas.y;
432
+ if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
433
+ newShifts[key] = {
434
+ x: dx,
435
+ y: dy
436
+ };
437
+ }
438
+ }
439
+ return newShifts;
440
+ };
441
+
442
+ /** Compute and apply shifts during drag (skips the invisible dragged item). */
443
+ const computeShifts = () => {
444
+ const shifts = computeShiftsForOrder(pendingOrderRef.current, draggedDisplayIndexRef.current, phantomRef.current ?? undefined);
445
+ applyShifts(shifts);
446
+ };
447
+
448
+ // ── Reorder during drag (ref-only, no state) ─────────────────────
449
+
450
+ const moveDraggedItem = toDisplayIndex => {
451
+ const fromIdx = draggedDisplayIndexRef.current;
452
+ if (fromIdx === undefined || fromIdx === toDisplayIndex) return;
453
+
454
+ // Don't move to a fixed item's position
455
+ const prev = pendingOrderRef.current;
456
+ if (prev.length === 0) return;
457
+ const targetOrigIdx = prev[toDisplayIndex];
458
+ if (targetOrigIdx !== undefined) {
459
+ const targetItem = stableData[targetOrigIdx];
460
+ if (targetItem !== undefined) {
461
+ const targetKey = keyExtractor(targetItem, targetOrigIdx);
462
+ if (fixedKeys.current.has(targetKey)) return;
463
+ }
464
+ }
465
+ let newOrder;
466
+ if (reorderStrategy === 'swap') {
467
+ newOrder = [...prev];
468
+ const temp = newOrder[fromIdx];
469
+ newOrder[fromIdx] = newOrder[toDisplayIndex];
470
+ newOrder[toDisplayIndex] = temp;
471
+ } else {
472
+ newOrder = [...prev];
473
+ const [removed] = newOrder.splice(fromIdx, 1);
474
+ if (removed !== undefined) {
475
+ newOrder.splice(toDisplayIndex, 0, removed);
476
+ }
477
+ }
478
+ pendingOrderRef.current = newOrder;
479
+ draggedDisplayIndexRef.current = toDisplayIndex;
480
+
481
+ // Update visual shifts
482
+ computeShifts();
483
+ };
484
+
485
+ // ── Commit visual order (permanent shifts, no FlatList data change) ──
486
+
487
+ /**
488
+ * Store the current pending order as the committed visual order.
489
+ * Called after drag ends — FlatList data is NOT changed. Items stay
490
+ * at their FlatList positions and shifts provide the visual reorder.
491
+ */
492
+ const commitVisualOrder = () => {
493
+ const pending = pendingOrderRef.current;
494
+ if (pending.length === 0) return;
495
+ committedOrderRef.current = [...pending];
496
+ committedKeyOrderRef.current = pending.map(origIdx => {
497
+ const item = stableData[origIdx];
498
+ return item !== undefined ? keyExtractor(item, origIdx) : '';
499
+ });
500
+ // Store final shifts (all items including formerly-dragged) for cancel revert.
501
+ const finalShifts = computeShiftsForOrder(pending);
502
+ committedShiftsRef.current = finalShifts ?? {};
503
+ };
504
+
505
+ /**
506
+ * Flush permanent shifts: update stableData to match rawData and clear
507
+ * shifts. Called after a delay so both the Fabric commit and the shift
508
+ * clearing are processed on the same UI frame — no visual blink because
509
+ * items are already at the correct visual positions via permanent shifts.
510
+ * Restores touch hit testing (FlatList cells at correct Yoga positions).
511
+ */
512
+ /** Flag: next stableData change should clear shifts via runOnUI */
513
+ const pendingShiftFlushRef = useRef(false);
514
+ const flushVisualOrder = () => {
515
+ const currentRawData = rawDataRef.current;
516
+ committedOrderRef.current = [];
517
+ committedKeyOrderRef.current = [];
518
+ committedShiftsRef.current = {};
519
+ pendingShiftFlushRef.current = true;
520
+ setStableData(currentRawData);
521
+ // Shift clearing happens in the useLayoutEffect below — NOT here.
522
+ // This ensures it's queued during the same React commit as the
523
+ // Fabric update, so both land on the same UI frame.
524
+ };
525
+
526
+ // When flushVisualOrder updates stableData, clear shifts during the
527
+ // same commit phase. The runOnUI worklet and the Fabric commit are
528
+ // both queued from this useLayoutEffect — processed on the same
529
+ // UI frame, so items transition from permanent-shift positions to
530
+ // new FlatList positions atomically. No blink.
531
+ useLayoutEffect(() => {
532
+ if (pendingShiftFlushRef.current) {
533
+ pendingShiftFlushRef.current = false;
534
+ runOnUI(() => {
535
+ 'worklet';
536
+
537
+ instantClearSV.value = true;
538
+ shiftsRef.value = {};
539
+ })();
540
+ }
541
+ }, [stableData, instantClearSV, shiftsRef]);
542
+
543
+ // ── Drag state methods ─────────────────────────────────────────────
544
+
545
+ const setDraggedItem = index => {
546
+ draggedItem.value = index;
547
+ };
548
+ const resetDraggedItem = () => {
549
+ draggedItem.value = -1;
550
+ draggedDisplayIndexRef.current = undefined;
551
+ dragStartIndexRef.current = undefined;
552
+ pendingOrderRef.current = [];
553
+ };
554
+
555
+ /**
556
+ * Initialize pending order from current originalIndexes at drag start.
557
+ */
558
+ const initPendingOrder = () => {
559
+ // Start from the committed visual order (what the user sees),
560
+ // NOT originalIndexes (always identity with permanent shifts).
561
+ const committed = committedOrderRef.current;
562
+ pendingOrderRef.current = committed.length > 0 ? [...committed] : [...originalIndexes];
563
+ instantClearSV.value = false;
564
+ };
565
+
566
+ /**
567
+ * Cancel drag without reorder — clears shifts instantly and makes item visible.
568
+ * Used when the drag ends but no reorder happened (item snaps back to origin).
569
+ */
570
+ const cancelDrag = () => {
571
+ instantClearSV.value = true;
572
+ // Revert to committed shifts from the previous drag (if any).
573
+ // If no previous drag, clears to empty (identity positions).
574
+ shiftsRef.value = committedShiftsRef.current;
575
+ };
576
+
577
+ // ── Cross-container phantom slot methods ────────────────────────────
578
+
579
+ const setPhantomSlot = (atDisplayIndex, width, height) => {
580
+ if (pendingOrderRef.current.length === 0) {
581
+ const committed = committedOrderRef.current;
582
+ pendingOrderRef.current = committed.length > 0 ? [...committed] : [...originalIndexes];
583
+ }
584
+ instantClearSV.value = false;
585
+ phantomRef.current = {
586
+ atDisplayIndex,
587
+ width,
588
+ height
589
+ };
590
+ const shifts = computeShiftsForOrder(pendingOrderRef.current, undefined, phantomRef.current);
591
+ applyShifts(shifts);
592
+ };
593
+ const clearPhantomSlot = () => {
594
+ phantomRef.current = undefined;
595
+ instantClearSV.value = false;
596
+ const shifts = computeShiftsForOrder(pendingOrderRef.current);
597
+ if (shifts !== undefined) {
598
+ applyShifts(shifts);
599
+ } else {
600
+ shiftsRef.value = committedShiftsRef.current;
601
+ }
602
+ pendingOrderRef.current = [];
603
+ };
604
+ const ejectDraggedItem = () => {
605
+ const dragIdx = draggedDisplayIndexRef.current;
606
+ if (dragIdx === undefined) return;
607
+ const pending = pendingOrderRef.current;
608
+ if (pending.length === 0 || dragIdx >= pending.length) return;
609
+ const newOrder = [...pending];
610
+ newOrder.splice(dragIdx, 1);
611
+ pendingOrderRef.current = newOrder;
612
+ instantClearSV.value = false;
613
+ applyShifts(computeShiftsForOrder(newOrder));
614
+ draggedDisplayIndexRef.current = undefined;
615
+ };
616
+ const reinjectDraggedItem = (displayIndex, originalIndex) => {
617
+ const pending = pendingOrderRef.current;
618
+ if (pending.length === 0) {
619
+ const committed = committedOrderRef.current;
620
+ pendingOrderRef.current = committed.length > 0 ? [...committed] : [...originalIndexes];
621
+ }
622
+ const newOrder = [...pendingOrderRef.current];
623
+ newOrder.splice(displayIndex, 0, originalIndex);
624
+ pendingOrderRef.current = newOrder;
625
+ draggedDisplayIndexRef.current = displayIndex;
626
+ instantClearSV.value = false;
627
+ computeShifts();
628
+ };
629
+ const getPhantomSnapTarget = () => {
630
+ const containerMeasurements = containerMeasurementsRef.current;
631
+ if (!containerMeasurements) return DraxSnapbackTargetPreset.Default;
632
+ const phantom = phantomRef.current;
633
+ if (!phantom) return DraxSnapbackTargetPreset.Default;
634
+ const pending = pendingOrderRef.current;
635
+ if (pending.length === 0) {
636
+ return {
637
+ x: containerMeasurements.x - scrollPosition.value.x,
638
+ y: containerMeasurements.y - scrollPosition.value.y
639
+ };
640
+ }
641
+ const gap = computeItemGap();
642
+ const firstOrigIdx = originalIndexes[0];
643
+ const startMeas = firstOrigIdx !== undefined ? getMeasForOrigIdx(firstOrigIdx) : undefined;
644
+ if (!startMeas) return DraxSnapbackTargetPreset.Default;
645
+ let cursor = horizontal ? startMeas.x : startMeas.y;
646
+ let displaySlot = 0;
647
+ for (let i = 0; i < pending.length; i++) {
648
+ if (displaySlot === phantom.atDisplayIndex) {
649
+ const phantomPos = horizontal ? {
650
+ x: cursor,
651
+ y: startMeas.y
652
+ } : {
653
+ x: startMeas.x,
654
+ y: cursor
655
+ };
656
+ return {
657
+ x: containerMeasurements.x + phantomPos.x - scrollPosition.value.x,
658
+ y: containerMeasurements.y + phantomPos.y - scrollPosition.value.y
659
+ };
660
+ }
661
+ const meas = getMeasForOrigIdx(pending[i]);
662
+ if (!meas) return DraxSnapbackTargetPreset.Default;
663
+ cursor += (horizontal ? meas.width : meas.height) + gap;
664
+ displaySlot++;
665
+ }
666
+
667
+ // Phantom at end
668
+ const phantomPos = horizontal ? {
669
+ x: cursor,
670
+ y: startMeas.y
671
+ } : {
672
+ x: startMeas.x,
673
+ y: cursor
674
+ };
675
+ return {
676
+ x: containerMeasurements.x + phantomPos.x - scrollPosition.value.x,
677
+ y: containerMeasurements.y + phantomPos.y - scrollPosition.value.y
678
+ };
679
+ };
680
+
681
+ /**
682
+ * Compute the display slot (index) from a container-local content position.
683
+ * Used when dragging over empty space (no receiver hit) to determine
684
+ * which slot the dragged item should occupy.
685
+ */
686
+ const getSlotFromPosition = contentPos => {
687
+ const pending = pendingOrderRef.current;
688
+ if (pending.length === 0) return 0;
689
+
690
+ // Use ORIGINAL layout positions for stable slot boundaries.
691
+ // Key insight: slot boundaries must NOT shift when items are reordered
692
+ // during drag. Using pending-order measurements causes oscillation:
693
+ // move changes boundaries → same position maps to new slot → another
694
+ // move → boundaries shift again → gap keeps running from the finger.
695
+ // Original layout positions are fixed throughout the drag.
696
+ const measurements = originalIndexes.map(origIdx => getMeasForOrigIdx(origIdx));
697
+ const missingIdx = measurements.findIndex(m => !m);
698
+ if (missingIdx >= 0) {
699
+ return draggedDisplayIndexRef.current ?? 0;
700
+ }
701
+ const gap = computeItemGap();
702
+
703
+ // Use the shorter of measurements and pending to avoid out-of-bounds access.
704
+ const itemCount = Math.min(measurements.length, pending.length);
705
+ if (itemCount === 0) return 0;
706
+ if (numColumns <= 1) {
707
+ // Single-column list — find which slot the position falls in.
708
+ // Boundary is at the gap midpoint between adjacent items, making
709
+ // forward and backward equally responsive (distance = size/2 + gap/2).
710
+ // Using item centers (50%) would be asymmetric: the hover center
711
+ // starts AT the forward boundary but a full item-height from the
712
+ // backward boundary, making forward too sensitive and backward too sluggish.
713
+ const firstMeas = measurements[0];
714
+ if (!firstMeas) return 0;
715
+ let cursor = horizontal ? firstMeas.x : firstMeas.y;
716
+ for (let i = 0; i < itemCount; i++) {
717
+ const meas = measurements[i];
718
+ if (!meas) continue;
719
+ const size = horizontal ? meas.width : meas.height;
720
+ const boundary = cursor + size + gap / 2; // midpoint of gap after item
721
+ const pos = horizontal ? contentPos.x : contentPos.y;
722
+ if (pos < boundary) return i;
723
+ cursor += size + gap;
724
+ }
725
+ return itemCount - 1;
726
+ } else if (getItemSpan) {
727
+ // ── Mixed-size grid: map finger to cell, then to display index ──
728
+ const geo = deriveGridGeometry();
729
+ if (!geo) return draggedDisplayIndexRef.current ?? 0;
730
+
731
+ // Pack original order (stable positions during drag)
732
+ const origPacking = packGrid(itemCount, numColumns, displayIdx => getSpanForOrigIdx(originalIndexes[displayIdx]));
733
+
734
+ // Find which grid cell the finger is in
735
+ const cellCol = Math.max(0, Math.min(Math.floor((contentPos.x - geo.startX + geo.colGap / 2) / (geo.cellWidth + geo.colGap)), numColumns - 1));
736
+ const cellRow = Math.max(0, Math.floor((contentPos.y - geo.startY + geo.rowGap / 2) / (geo.cellHeight + geo.rowGap)));
737
+
738
+ // Build cell → display index map (all cells each item occupies)
739
+ const cellOwner = new Map();
740
+ for (let i = 0; i < origPacking.positions.length && i < itemCount; i++) {
741
+ const pos = origPacking.positions[i];
742
+ const span = getSpanForOrigIdx(originalIndexes[i]);
743
+ for (let r = 0; r < span.rowSpan; r++) {
744
+ for (let c = 0; c < span.colSpan; c++) {
745
+ cellOwner.set(`${pos.row + r},${pos.col + c}`, i);
746
+ }
747
+ }
748
+ }
749
+
750
+ // Direct cell hit
751
+ const owner = cellOwner.get(`${cellRow},${cellCol}`);
752
+ if (owner !== undefined) return Math.min(owner, pending.length - 1);
753
+
754
+ // Empty cell — find nearest item by center distance
755
+ let minDist = Infinity;
756
+ let nearest = 0;
757
+ for (let i = 0; i < origPacking.positions.length && i < itemCount; i++) {
758
+ const meas = measurements[i];
759
+ if (!meas) continue;
760
+ const cx = meas.x + meas.width / 2;
761
+ const cy = meas.y + meas.height / 2;
762
+ const dist = Math.abs(contentPos.x - cx) + Math.abs(contentPos.y - cy);
763
+ if (dist < minDist) {
764
+ minDist = dist;
765
+ nearest = i;
766
+ }
767
+ }
768
+ return Math.min(nearest, pending.length - 1);
769
+ } else {
770
+ // ── Uniform grid — find row then column ──
771
+ const firstMeas = measurements[0];
772
+ if (!firstMeas) return 0;
773
+ let cursorY = firstMeas.y;
774
+
775
+ // Find row — use full row boundary (not center) so the bottom
776
+ // half of a row doesn't spill into the next row.
777
+ let targetRow = 0;
778
+ const totalRows = Math.ceil(itemCount / numColumns);
779
+ for (let row = 0; row < totalRows; row++) {
780
+ const rowStart = row * numColumns;
781
+ const rowEnd = Math.min(rowStart + numColumns, itemCount);
782
+ let rowHeight = 0;
783
+ for (let col = rowStart; col < rowEnd; col++) {
784
+ const colMeas = measurements[col];
785
+ if (colMeas) rowHeight = Math.max(rowHeight, colMeas.height);
786
+ }
787
+ if (contentPos.y < cursorY + rowHeight + gap / 2) {
788
+ targetRow = row;
789
+ break;
790
+ }
791
+ cursorY += rowHeight + gap;
792
+ targetRow = row;
793
+ }
794
+
795
+ // Find column within row — use gap midpoint for symmetric sensitivity
796
+ const colXPositions = [];
797
+ for (let c = 0; c < numColumns && c < originalIndexes.length; c++) {
798
+ const origIdx = originalIndexes[c];
799
+ const colMeas = origIdx !== undefined ? getMeasForOrigIdx(origIdx) : undefined;
800
+ colXPositions.push(colMeas ? colMeas.x : 0);
801
+ }
802
+ const firstMeasWidth = firstMeas.width;
803
+ const colGap = numColumns >= 2 && colXPositions.length >= 2 ? (colXPositions[1] ?? 0) - ((colXPositions[0] ?? 0) + firstMeasWidth) : 0;
804
+ let targetCol = 0;
805
+ for (let c = 0; c < numColumns; c++) {
806
+ const colX = colXPositions[c] ?? 0;
807
+ const colMeas = measurements[Math.min(c, measurements.length - 1)];
808
+ if (!colMeas) break;
809
+ const colBoundary = colX + colMeas.width + colGap / 2;
810
+ if (contentPos.x < colBoundary) {
811
+ targetCol = c;
812
+ break;
813
+ }
814
+ targetCol = c;
815
+ }
816
+ return Math.min(targetRow * numColumns + targetCol, pending.length - 1);
817
+ }
818
+ };
819
+
820
+ // ── Snapback target ─────────────────────────────────────────────────
821
+
822
+ const getSnapbackTarget = () => {
823
+ const containerMeasurements = containerMeasurementsRef.current;
824
+ if (!containerMeasurements) return DraxSnapbackTargetPreset.Default;
825
+ const displayIdx = draggedDisplayIndexRef.current;
826
+ if (displayIdx === undefined) return DraxSnapbackTargetPreset.Default;
827
+ const pending = pendingOrderRef.current;
828
+ if (pending.length === 0) return DraxSnapbackTargetPreset.Default;
829
+
830
+ // Compute the target position for the dragged item by laying out
831
+ // items in the pending order and accumulating dimensions.
832
+ // This is the same logic as computeShifts but we only need the
833
+ // position at displayIdx.
834
+ const measurements = pending.map(origIdx => getMeasForOrigIdx(origIdx));
835
+ if (measurements.some(m => !m)) return DraxSnapbackTargetPreset.Default;
836
+ let targetPos;
837
+ const gap = computeItemGap();
838
+
839
+ // Use FlatList's actual starting position, not pending[0] which
840
+ // may be the dragged item at the wrong FlatList slot.
841
+ const snapFirstOrigIdx = originalIndexes[0];
842
+ const snapStartMeas = snapFirstOrigIdx !== undefined ? getMeasForOrigIdx(snapFirstOrigIdx) : undefined;
843
+ if (!snapStartMeas) return DraxSnapbackTargetPreset.Default;
844
+ if (numColumns <= 1) {
845
+ // Single-column list
846
+ let cursor = horizontal ? snapStartMeas.x : snapStartMeas.y;
847
+ for (let i = 0; i < displayIdx; i++) {
848
+ const meas = measurements[i];
849
+ cursor += (horizontal ? meas.width : meas.height) + gap;
850
+ }
851
+ targetPos = horizontal ? {
852
+ x: cursor,
853
+ y: snapStartMeas.y
854
+ } : {
855
+ x: snapStartMeas.x,
856
+ y: cursor
857
+ };
858
+ } else if (getItemSpan) {
859
+ // Mixed-size grid — pack items and find target position
860
+ const geo = deriveGridGeometry();
861
+ if (!geo) return DraxSnapbackTargetPreset.Default;
862
+ const packing = packGrid(pending.length, numColumns, di => getSpanForOrigIdx(pending[di]));
863
+ const gp = packing.positions[displayIdx];
864
+ if (!gp) return DraxSnapbackTargetPreset.Default;
865
+ targetPos = {
866
+ x: geo.startX + gp.col * (geo.cellWidth + geo.colGap),
867
+ y: geo.startY + gp.row * (geo.cellHeight + geo.rowGap)
868
+ };
869
+ } else {
870
+ // Uniform grid
871
+ let cursorY = snapStartMeas.y;
872
+ const targetRow = Math.floor(displayIdx / numColumns);
873
+ const targetCol = displayIdx % numColumns;
874
+ for (let row = 0; row < targetRow; row++) {
875
+ const rowStart = row * numColumns;
876
+ const rowEnd = Math.min(rowStart + numColumns, pending.length);
877
+ let rowHeight = 0;
878
+ for (let col = rowStart; col < rowEnd; col++) {
879
+ rowHeight = Math.max(rowHeight, measurements[col].height);
880
+ }
881
+ cursorY += rowHeight + gap;
882
+ }
883
+ const colMeas = getMeasForOrigIdx(originalIndexes[targetCol]);
884
+ targetPos = {
885
+ x: colMeas ? colMeas.x : 0,
886
+ y: cursorY
887
+ };
888
+ }
889
+ return {
890
+ x: containerMeasurements.x + targetPos.x - scrollPosition.value.x,
891
+ y: containerMeasurements.y + targetPos.y - scrollPosition.value.y
892
+ };
893
+ };
894
+
895
+ // ── Scroll event handlers ─────────────────────────────────────────
896
+
897
+ const onScroll = event => {
898
+ runOnUI(_event => {
899
+ 'worklet';
900
+
901
+ scrollPosition.value = {
902
+ x: _event.contentOffset.x,
903
+ y: _event.contentOffset.y
904
+ };
905
+ })(event.nativeEvent);
906
+ };
907
+ const onContentSizeChange = (width, height) => {
908
+ contentSizeRef.current = {
909
+ x: width,
910
+ y: height
911
+ };
912
+ };
913
+
914
+ // ── Build the internal object ─────────────────────────────────────
915
+ const internal = {
916
+ id,
917
+ horizontal,
918
+ numColumns,
919
+ reorderStrategy,
920
+ longPressDelay,
921
+ lockToMainAxis,
922
+ animationConfig,
923
+ getItemSpan,
924
+ inactiveItemStyle,
925
+ itemEntering,
926
+ itemExiting,
927
+ fixedKeys,
928
+ draggedItem,
929
+ itemMeasurements,
930
+ originalIndexes,
931
+ keyExtractor,
932
+ data: stableData,
933
+ rawData: stableData,
934
+ moveDraggedItem,
935
+ getSnapbackTarget,
936
+ setDraggedItem,
937
+ resetDraggedItem,
938
+ scrollPosition,
939
+ containerMeasurementsRef,
940
+ contentSizeRef,
941
+ autoScrollJumpRatio,
942
+ autoScrollBackThreshold,
943
+ autoScrollForwardThreshold,
944
+ onDragStart,
945
+ onDragPositionChange,
946
+ onDragEnd,
947
+ onReorder,
948
+ getMeasurementByOriginalIndex,
949
+ dropTargetPositionSV,
950
+ dropTargetVisibleSV,
951
+ onItemSnapEnd: undefined,
952
+ draggedDisplayIndexRef,
953
+ dragStartIndexRef,
954
+ shiftsRef,
955
+ instantClearSV,
956
+ shiftsValidSV,
957
+ initPendingOrder,
958
+ commitVisualOrder,
959
+ flushVisualOrder,
960
+ computeShiftsForOrder,
961
+ committedOrderRef,
962
+ pendingOrderRef,
963
+ cancelDrag,
964
+ getSlotFromPosition,
965
+ phantomRef,
966
+ setPhantomSlot,
967
+ clearPhantomSlot,
968
+ ejectDraggedItem,
969
+ reinjectDraggedItem,
970
+ getPhantomSnapTarget,
971
+ ghostShiftsRef,
972
+ committedShiftsRef,
973
+ skipShiftsInvalidationRef
974
+ };
975
+
976
+ // Stable index-based keyExtractor prevents FlatList from unmounting cells
977
+ // when data reorders. Cells stay at their FlatList index and React updates
978
+ // content in place (no unmount/remount), eliminating the multi-frame blink.
979
+ const stableKeyExtractor = useStableKeyExtractor();
980
+ return {
981
+ data: stableData,
982
+ onScroll,
983
+ onContentSizeChange,
984
+ stableKeyExtractor,
985
+ _internal: internal
986
+ };
987
+ };
988
+ //# sourceMappingURL=useSortableList.js.map