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,273 @@
1
+ import { Platform } from 'react-native';
2
+ import type { SharedValue } from 'react-native-reanimated';
3
+ import { runOnJS } from 'react-native-worklets';
4
+
5
+ import { useDraxPanGesture } from '../compat';
6
+ import { computeAbsolutePositionWorklet, hitTestWorklet } from '../math';
7
+ import type { Position } from '../types';
8
+ import { useDraxContext } from './useDraxContext';
9
+
10
+ /**
11
+ * Creates a Pan gesture for a draggable DraxView.
12
+ * Hit-testing runs entirely on the UI thread — zero runOnJS per frame
13
+ * unless the receiver changes.
14
+ *
15
+ * On RNGH v3, `enabledSV` and `longPressDelaySV` are SharedValues that
16
+ * reconfigure the native gesture handler on the UI thread — zero JS bridge.
17
+ * On RNGH v2, they are mirrored to plain values with gesture recreation on change.
18
+ */
19
+ export const useDragGesture = (
20
+ id: string,
21
+ viewSpatialIndexSV: SharedValue<number>,
22
+ enabledSV: SharedValue<boolean>,
23
+ longPressDelaySV: SharedValue<number>,
24
+ lockDragXPosition?: boolean,
25
+ lockDragYPosition?: boolean,
26
+ dragBoundsSV?: SharedValue<{ x: number; y: number; width: number; height: number } | null>,
27
+ dragActivationFailOffset?: number,
28
+ scrollHorizontal?: boolean
29
+ ) => {
30
+ const {
31
+ draggedIdSV,
32
+ receiverIdSV,
33
+ rejectedReceiverIdSV,
34
+ dragPhaseSV,
35
+ hoverPositionSV,
36
+ dragAbsolutePositionSV,
37
+ spatialIndexSV,
38
+ scrollOffsetsSV,
39
+ grabOffsetSV,
40
+ startPositionSV,
41
+ rootOffsetSV,
42
+ handleDragStart,
43
+ handleReceiverChange,
44
+ handleDragEnd,
45
+ } = useDraxContext();
46
+
47
+ // On web, RNGH defaults touch-action to 'none' which blocks native scroll.
48
+ // Allow the scroll direction so users can scroll before long-press activates.
49
+ // SortableContainer freezes the scroll container when drag starts.
50
+ //
51
+ // Priority: lockDragYPosition (explicit axis lock → pan-x) > scrollHorizontal
52
+ // (hint from SortableItem for horizontal lists without axis lock) > default pan-y.
53
+ const touchAction = Platform.OS === 'web'
54
+ ? ((lockDragYPosition || scrollHorizontal) ? 'pan-x' : 'pan-y')
55
+ : undefined;
56
+
57
+ const failOffset = dragActivationFailOffset !== undefined
58
+ ? [-dragActivationFailOffset, dragActivationFailOffset] as [number, number]
59
+ : undefined;
60
+
61
+ const gesture = useDraxPanGesture({
62
+ enabledSV,
63
+ longPressDelaySV,
64
+ maxPointers: 1,
65
+ shouldCancelWhenOutside: false,
66
+ touchAction,
67
+ failOffsetX: failOffset,
68
+ failOffsetY: failOffset,
69
+ onActivate: (event) => {
70
+ 'worklet';
71
+
72
+ // Convert screen-absolute touch to root-view-relative
73
+ const rootOffset = rootOffsetSV.value;
74
+ const rootRelX = event.absoluteX - rootOffset.x;
75
+ const rootRelY = event.absoluteY - rootOffset.y;
76
+
77
+ // Derive the view's visual position from the gesture event.
78
+ // event.x/y = touch relative to the view's bounds (accounts for transforms).
79
+ // This is more accurate than the spatial index for sortable items where
80
+ // permanent shifts move views via CSS transform without updating layout.
81
+ const viewAbsPos: Position = {
82
+ x: rootRelX - event.x,
83
+ y: rootRelY - event.y,
84
+ };
85
+
86
+ // Grab offset = touch position within the view
87
+ const grabOffset: Position = {
88
+ x: event.x,
89
+ y: event.y,
90
+ };
91
+
92
+ // Store shared state (all positions in root-relative space).
93
+ // DO NOT set dragPhaseSV here — it's set by HoverLayer's useLayoutEffect
94
+ // AFTER hover content is committed to the DOM. This prevents the grab blink
95
+ // (item going invisible before hover is visible).
96
+ draggedIdSV.value = id;
97
+ grabOffsetSV.value = grabOffset;
98
+ startPositionSV.value = { x: rootRelX, y: rootRelY };
99
+ dragAbsolutePositionSV.value = { x: rootRelX, y: rootRelY };
100
+
101
+ // Compute initial hover position (root-relative)
102
+ let hoverX = lockDragXPosition ? viewAbsPos.x : rootRelX - grabOffset.x;
103
+ let hoverY = lockDragYPosition ? viewAbsPos.y : rootRelY - grabOffset.y;
104
+
105
+ // Clamp to drag bounds if specified
106
+ if (dragBoundsSV?.value) {
107
+ const b = dragBoundsSV.value;
108
+ const entries = spatialIndexSV.value;
109
+ const viewEntry = entries[viewSpatialIndexSV.value];
110
+ const vw = viewEntry ? viewEntry.width : 0;
111
+ const vh = viewEntry ? viewEntry.height : 0;
112
+ hoverX = Math.max(b.x, Math.min(b.x + b.width - vw, hoverX));
113
+ hoverY = Math.max(b.y, Math.min(b.y + b.height - vh, hoverY));
114
+ }
115
+
116
+ hoverPositionSV.value = { x: hoverX, y: hoverY };
117
+
118
+ // Reset receiver and rejection cache
119
+ receiverIdSV.value = '';
120
+ rejectedReceiverIdSV.value = '';
121
+
122
+ // Bounce to JS for callback dispatch + hover content setup
123
+ runOnJS(handleDragStart)(
124
+ id,
125
+ { x: rootRelX, y: rootRelY },
126
+ grabOffset
127
+ );
128
+ },
129
+ onUpdate: (event) => {
130
+ 'worklet';
131
+
132
+ // Convert screen-absolute touch to root-view-relative
133
+ const rootOffset = rootOffsetSV.value;
134
+ const rootRelX = event.absoluteX - rootOffset.x;
135
+ const rootRelY = event.absoluteY - rootOffset.y;
136
+ const rootRelPos: Position = { x: rootRelX, y: rootRelY };
137
+
138
+ dragAbsolutePositionSV.value = rootRelPos;
139
+
140
+ // Compute hover position (root-relative)
141
+ const grabOffset = grabOffsetSV.value;
142
+
143
+ // Read current spatial index
144
+ const spatialIndex = viewSpatialIndexSV.value;
145
+ const entries = spatialIndexSV.value;
146
+ const viewAbsPos = computeAbsolutePositionWorklet(
147
+ spatialIndex,
148
+ entries,
149
+ scrollOffsetsSV.value
150
+ );
151
+
152
+ let hoverX = lockDragXPosition ? viewAbsPos.x : rootRelX - grabOffset.x;
153
+ let hoverY = lockDragYPosition ? viewAbsPos.y : rootRelY - grabOffset.y;
154
+
155
+ // Clamp to drag bounds if specified
156
+ if (dragBoundsSV?.value) {
157
+ const b = dragBoundsSV.value;
158
+ const viewEntry = entries[spatialIndex];
159
+ const vw = viewEntry ? viewEntry.width : 0;
160
+ const vh = viewEntry ? viewEntry.height : 0;
161
+ hoverX = Math.max(b.x, Math.min(b.x + b.width - vw, hoverX));
162
+ hoverY = Math.max(b.y, Math.min(b.y + b.height - vh, hoverY));
163
+ }
164
+
165
+ hoverPositionSV.value = { x: hoverX, y: hoverY };
166
+
167
+ // Hit-test at the center of the hover view (not at the raw finger position)
168
+ // so that receiving activates when the dragged item visually overlaps the receiver.
169
+ const viewEntry = entries[spatialIndex];
170
+ const hitTestPos: Position = {
171
+ x: hoverX + (viewEntry ? viewEntry.width / 2 : 0),
172
+ y: hoverY + (viewEntry ? viewEntry.height / 2 : 0),
173
+ };
174
+ dragAbsolutePositionSV.value = hitTestPos;
175
+
176
+ const result = hitTestWorklet(
177
+ hitTestPos,
178
+ entries,
179
+ scrollOffsetsSV.value,
180
+ id,
181
+ viewEntry ? { width: viewEntry.width, height: viewEntry.height } : undefined
182
+ );
183
+
184
+ // Skip the rejected receiver — don't set it in receiverIdSV and don't
185
+ // send it to JS. This prevents the reject → clear → re-detect → reject loop.
186
+ let candidateReceiverId = result.receiverId;
187
+ if (candidateReceiverId === rejectedReceiverIdSV.value) {
188
+ candidateReceiverId = '';
189
+ }
190
+
191
+ // Clear rejection cache once drag leaves the rejected receiver's bounds
192
+ if (result.receiverId !== rejectedReceiverIdSV.value && rejectedReceiverIdSV.value !== '') {
193
+ rejectedReceiverIdSV.value = '';
194
+ }
195
+
196
+ // Always bounce to JS for callback dispatch.
197
+ // handleReceiverChange safely handles same-receiver calls (skips exit/enter)
198
+ // and dispatches continuous callbacks (onDrag, onDragOver, onReceiveDragOver)
199
+ // plus monitor position updates for slot detection.
200
+ const oldReceiver = receiverIdSV.value;
201
+ const receiverChanged = candidateReceiverId !== oldReceiver;
202
+ if (receiverChanged) {
203
+ receiverIdSV.value = candidateReceiverId;
204
+ }
205
+ runOnJS(handleReceiverChange)(
206
+ oldReceiver,
207
+ candidateReceiverId,
208
+ hitTestPos,
209
+ result.monitorIds
210
+ );
211
+ },
212
+ onDeactivate: (_event) => {
213
+ 'worklet';
214
+
215
+ const currentDraggedId = draggedIdSV.value;
216
+ const currentReceiverId = receiverIdSV.value;
217
+
218
+ // Run final hit-test to capture current monitors.
219
+ // Monitor IDs are only updated on receiver changes, so if no receiver
220
+ // change happened during the drag, monitors would be empty in handleDragEnd.
221
+ const deactivateEntries = spatialIndexSV.value;
222
+ const viewEntryFinal = deactivateEntries[viewSpatialIndexSV.value];
223
+ const finalDims = viewEntryFinal
224
+ ? { width: viewEntryFinal.width, height: viewEntryFinal.height }
225
+ : undefined;
226
+ const finalHitResult = hitTestWorklet(
227
+ dragAbsolutePositionSV.value,
228
+ deactivateEntries,
229
+ scrollOffsetsSV.value,
230
+ id,
231
+ finalDims
232
+ );
233
+
234
+ // Set phase and clear receiver on UI thread so useAnimatedStyle
235
+ // re-evaluates immediately (receiver style clears instantly).
236
+ dragPhaseSV.value = 'releasing';
237
+ receiverIdSV.value = '';
238
+
239
+ // Bounce to JS for end callbacks + snap animation
240
+ runOnJS(handleDragEnd)(currentDraggedId, currentReceiverId, false, finalHitResult.monitorIds);
241
+ },
242
+ onFinalize: (_event, didSucceed) => {
243
+ 'worklet';
244
+
245
+ // If gesture was cancelled (not ended normally).
246
+ // Check draggedIdSV (set in onActivate) instead of dragPhaseSV
247
+ // because phase is now set later in handleDragStart via runOnUI.
248
+ if (!didSucceed && draggedIdSV.value !== '') {
249
+ const currentDraggedId = draggedIdSV.value;
250
+ const currentReceiverId = receiverIdSV.value;
251
+
252
+ const viewEntryCancel = spatialIndexSV.value[viewSpatialIndexSV.value];
253
+ const cancelDims = viewEntryCancel
254
+ ? { width: viewEntryCancel.width, height: viewEntryCancel.height }
255
+ : undefined;
256
+ const finalHitResult = hitTestWorklet(
257
+ dragAbsolutePositionSV.value,
258
+ spatialIndexSV.value,
259
+ scrollOffsetsSV.value,
260
+ id,
261
+ cancelDims
262
+ );
263
+
264
+ dragPhaseSV.value = 'releasing';
265
+ receiverIdSV.value = '';
266
+
267
+ runOnJS(handleDragEnd)(currentDraggedId, currentReceiverId, true, finalHitResult.monitorIds);
268
+ }
269
+ },
270
+ });
271
+
272
+ return gesture;
273
+ };
@@ -0,0 +1,11 @@
1
+ import { use } from 'react';
2
+
3
+ import { DraxContext } from '../DraxContext';
4
+
5
+ export const useDraxContext = () => {
6
+ const drax = use(DraxContext);
7
+ if (!drax) {
8
+ throw Error('No DraxProvider found');
9
+ }
10
+ return drax;
11
+ };
@@ -0,0 +1,11 @@
1
+ import { useState } from 'react';
2
+
3
+ import { generateRandomId } from '../math';
4
+
5
+ // Return explicitId, or a consistent randomly generated identifier if explicitId is falsy.
6
+ export const useDraxId = (explicitId?: string) => {
7
+ // A generated unique identifier for this view, for use if id prop is not specified.
8
+ const [randomId] = useState(generateRandomId);
9
+ // We use || rather than ?? for the return value in case explicitId is an empty string.
10
+ return explicitId || randomId;
11
+ };
@@ -0,0 +1,71 @@
1
+ import { useDraxContext } from './useDraxContext';
2
+
3
+ /**
4
+ * Imperative methods for controlling and querying the Drax provider.
5
+ *
6
+ * Must be called within a `<DraxProvider>`.
7
+ */
8
+ export const useDraxMethods = () => {
9
+ const ctx = useDraxContext();
10
+
11
+ /**
12
+ * Trigger re-measurement of all registered views.
13
+ * Useful after dynamic layout changes where onLayout may not fire
14
+ * (e.g., orientation changes, animated layout transitions).
15
+ */
16
+ const requestPositionUpdate = () => {
17
+ // Re-measuring is done by reading each view's measure function from
18
+ // the spatial index. For now, this triggers a spatial index refresh
19
+ // by writing the current value back — views that are still mounted
20
+ // will re-measure on the next layout pass.
21
+ const current = ctx.spatialIndexSV.value;
22
+ ctx.spatialIndexSV.value = [...current];
23
+ };
24
+
25
+ /**
26
+ * Get the set of dragged item IDs currently dropped on a specific receiver.
27
+ * Returns an empty Set if no items have been dropped on the receiver.
28
+ */
29
+ const getDroppedItems = (receiverId?: string): Map<string, Set<string>> | Set<string> => {
30
+ if (receiverId) {
31
+ return ctx.droppedItemsRef.current.get(receiverId) ?? new Set();
32
+ }
33
+ return new Map(ctx.droppedItemsRef.current);
34
+ };
35
+
36
+ /**
37
+ * Clear all tracked dropped items for a specific receiver,
38
+ * or clear all dropped items if no receiverId is provided.
39
+ * Call this when items are programmatically removed from a drop zone.
40
+ */
41
+ const clearDroppedItems = (receiverId?: string) => {
42
+ if (receiverId) {
43
+ ctx.droppedItemsRef.current.delete(receiverId);
44
+ } else {
45
+ ctx.droppedItemsRef.current.clear();
46
+ }
47
+ };
48
+
49
+ /**
50
+ * Check if a drag is currently active.
51
+ */
52
+ const isDragging = (): boolean => {
53
+ return ctx.draggedIdSV.value !== '';
54
+ };
55
+
56
+ /**
57
+ * Get the ID of the currently dragged view, or undefined if no drag is active.
58
+ */
59
+ const getDraggedId = (): string | undefined => {
60
+ const id = ctx.draggedIdSV.value;
61
+ return id || undefined;
62
+ };
63
+
64
+ return {
65
+ requestPositionUpdate,
66
+ getDroppedItems,
67
+ clearDroppedItems,
68
+ isDragging,
69
+ getDraggedId,
70
+ };
71
+ };
@@ -0,0 +1,121 @@
1
+ import type { Ref, RefObject } from 'react';
2
+ import { useCallback, useEffect, useRef } from 'react';
3
+ import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';
4
+ import { FlatList, ScrollView } from 'react-native';
5
+ import {
6
+ runOnUI,
7
+ useSharedValue,
8
+ } from 'react-native-reanimated';
9
+
10
+ import { defaultAutoScrollIntervalLength } from '../params';
11
+ import type { DraxViewMeasurements, Position } from '../types';
12
+ import { useDraxId } from './useDraxId';
13
+
14
+ // FlatList is invariant in its type parameter — `any` is the only valid union constraint
15
+ type ScrollableComponents = FlatList<any> | ScrollView;
16
+
17
+ type DraxScrollHandlerArgs<T extends ScrollableComponents> = {
18
+ idProp?: string;
19
+ onContentSizeChangeProp?: (w: number, h: number) => void;
20
+ onScrollProp?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
21
+ externalRef?: Ref<T>;
22
+ doScroll: RefObject<() => void>;
23
+ };
24
+
25
+ export const useDraxScrollHandler = <T extends ScrollableComponents>({
26
+ idProp,
27
+ onContentSizeChangeProp,
28
+ onScrollProp,
29
+ externalRef,
30
+ doScroll,
31
+ }: DraxScrollHandlerArgs<T>) => {
32
+ const scrollRef = useRef<T>(null);
33
+ const id = useDraxId(idProp);
34
+ const containerMeasurementsRef = useRef<DraxViewMeasurements | undefined>(
35
+ undefined
36
+ );
37
+ const scrollIntervalRef = useRef<ReturnType<typeof setInterval> | undefined>(
38
+ undefined
39
+ );
40
+ const contentSizeRef = useRef<Position | undefined>(undefined);
41
+
42
+ const scrollPosition = useSharedValue<Position>({ x: 0, y: 0 });
43
+
44
+ const onMeasureContainer = (measurements: DraxViewMeasurements | undefined) => {
45
+ containerMeasurementsRef.current = measurements;
46
+ };
47
+
48
+ const onContentSizeChange = (width: number, height: number) => {
49
+ contentSizeRef.current = { x: width, y: height };
50
+ return onContentSizeChangeProp?.(width, height);
51
+ };
52
+
53
+ const onScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
54
+ onScrollProp?.(event);
55
+
56
+ runOnUI((_scrollPos: typeof scrollPosition, _event: NativeScrollEvent) => {
57
+ 'worklet';
58
+ _scrollPos.value = {
59
+ x: _event.contentOffset.x,
60
+ y: _event.contentOffset.y,
61
+ };
62
+ })(scrollPosition, event.nativeEvent);
63
+ };
64
+
65
+ const setScrollRefs = (instance: T | null) => {
66
+ if (instance) {
67
+ scrollRef.current = instance;
68
+ if (externalRef) {
69
+ if (typeof externalRef === 'function') {
70
+ externalRef(instance);
71
+ } else {
72
+ externalRef.current = instance;
73
+ }
74
+ }
75
+ }
76
+ };
77
+
78
+ const startScroll = useCallback(() => {
79
+ if (scrollIntervalRef.current) {
80
+ return;
81
+ }
82
+ doScroll.current();
83
+ scrollIntervalRef.current = setInterval(
84
+ () => doScroll.current(),
85
+ defaultAutoScrollIntervalLength
86
+ );
87
+ }, [doScroll]);
88
+
89
+ const stopScroll = useCallback(() => {
90
+ if (scrollIntervalRef.current) {
91
+ clearInterval(scrollIntervalRef.current);
92
+ scrollIntervalRef.current = undefined;
93
+ }
94
+ }, []);
95
+
96
+ useEffect(() => {
97
+ if (scrollIntervalRef.current) {
98
+ stopScroll();
99
+ startScroll();
100
+ }
101
+ }, [stopScroll, startScroll]);
102
+
103
+ // Clean up interval on unmount
104
+ useEffect(() => {
105
+ return () => stopScroll();
106
+ }, [stopScroll]);
107
+
108
+ return {
109
+ id,
110
+ containerMeasurementsRef,
111
+ contentSizeRef,
112
+ onContentSizeChange,
113
+ onMeasureContainer,
114
+ onScroll,
115
+ scrollRef,
116
+ scrollPosition,
117
+ setScrollRefs,
118
+ startScroll,
119
+ stopScroll,
120
+ };
121
+ };
@@ -0,0 +1,44 @@
1
+ import { useCallback, useRef } from 'react';
2
+ import type {
3
+ SortableBoardHandle,
4
+ SortableBoardInternal,
5
+ SortableBoardTransferState,
6
+ SortableListInternal,
7
+ UseSortableBoardOptions,
8
+ } from '../types';
9
+
10
+ /**
11
+ * Board-level coordinator for cross-container sortable drag.
12
+ *
13
+ * Maintains a registry of columns (each with their own useSortableList)
14
+ * and tracks cross-container transfer state. The actual monitor callbacks
15
+ * are handled by SortableBoardContainer.
16
+ */
17
+ export const useSortableBoard = <TItem,>(
18
+ options: UseSortableBoardOptions<TItem>
19
+ ): SortableBoardHandle<TItem> => {
20
+ const { keyExtractor, onTransfer } = options;
21
+
22
+ const columnsRef = useRef<Map<string, SortableListInternal<unknown>>>(new Map());
23
+ const transferStateRef = useRef<SortableBoardTransferState | undefined>(undefined);
24
+
25
+ const registerColumn = useCallback((id: string, internal: SortableListInternal<unknown>) => {
26
+ columnsRef.current.set(id, internal);
27
+ }, []);
28
+
29
+ const unregisterColumn = useCallback((id: string) => {
30
+ columnsRef.current.delete(id);
31
+ }, []);
32
+
33
+ const internal: SortableBoardInternal<TItem> = {
34
+ keyExtractor,
35
+ onTransfer,
36
+ columns: columnsRef.current,
37
+ registerColumn,
38
+ unregisterColumn,
39
+ transferState: transferStateRef,
40
+ // finalizeTransfer is set by SortableBoardContainer (needs DraxContext access)
41
+ };
42
+
43
+ return { _internal: internal };
44
+ };