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,688 @@
1
+ "use strict";
2
+
3
+ import { useRef } from 'react';
4
+ import { StyleSheet, View } from 'react-native';
5
+ import { withDelay, withTiming } from 'react-native-reanimated';
6
+ import { runOnJS, runOnUI } from 'react-native-worklets';
7
+ import { computeAbsolutePositionWorklet, getRelativePosition } from "../math.js";
8
+ import { defaultSnapbackDelay, defaultSnapbackDuration } from "../params.js";
9
+ import { DraxSnapbackTargetPreset, DraxViewDragStatus, DraxViewReceiveStatus, isPosition } from "../types.js";
10
+ import { isDraggable } from "./useSpatialIndex.js";
11
+
12
+ /** Style override to strip margins — hover is positioned via translateX/Y */
13
+ /** Styles to strip from the hover content — margins and absolute positioning
14
+ * are not needed since hover is positioned via translateX/Y. */
15
+ import { jsx as _jsx } from "react/jsx-runtime";
16
+ const hoverResetStyle = {
17
+ margin: 0,
18
+ marginHorizontal: 0,
19
+ marginVertical: 0,
20
+ marginTop: 0,
21
+ marginBottom: 0,
22
+ marginLeft: 0,
23
+ marginRight: 0,
24
+ position: 'relative',
25
+ left: 0,
26
+ top: 0,
27
+ right: undefined,
28
+ bottom: undefined
29
+ };
30
+ /**
31
+ * Provides JS-thread callback dispatch functions that are invoked via runOnJS
32
+ * from gesture worklets. These handle ~5 calls per drag (start, receiver changes, end),
33
+ * NOT per frame.
34
+ */
35
+ export const useCallbackDispatch = deps => {
36
+ const {
37
+ getViewEntry,
38
+ spatialIndexSV,
39
+ scrollOffsetsSV,
40
+ draggedIdSV,
41
+ dragPhaseSV,
42
+ hoverPositionSV,
43
+ grabOffsetSV,
44
+ startPositionSV,
45
+ setHoverContent,
46
+ hoverReadySV,
47
+ onProviderDragStart,
48
+ onProviderDrag,
49
+ onProviderDragEnd,
50
+ droppedItemsRef
51
+ } = deps;
52
+
53
+ // Track current monitor ids for exit events
54
+ const currentMonitorIdsRef = useRef([]);
55
+
56
+ /** Build dragged view event data from current state */
57
+ const buildDraggedViewData = (draggedId, absolutePosition) => {
58
+ const entry = getViewEntry(draggedId);
59
+ if (!entry) return undefined;
60
+ const startPos = startPositionSV.value;
61
+ const grabOffset = grabOffsetSV.value;
62
+ const dragTranslation = {
63
+ x: absolutePosition.x - startPos.x,
64
+ y: absolutePosition.y - startPos.y
65
+ };
66
+ const measurements = entry.measurements;
67
+ // Use || instead of ?? intentionally: zero dimensions would cause division by zero below
68
+ const width = measurements?.width || 1;
69
+ const height = measurements?.height || 1;
70
+ return {
71
+ id: draggedId,
72
+ parentId: entry.parentId,
73
+ payload: entry.props.dragPayload ?? entry.props.payload,
74
+ measurements,
75
+ dragTranslationRatio: {
76
+ x: dragTranslation.x / width,
77
+ y: dragTranslation.y / height
78
+ },
79
+ dragOffset: {
80
+ x: absolutePosition.x - (measurements?.x ?? 0),
81
+ y: absolutePosition.y - (measurements?.y ?? 0)
82
+ },
83
+ grabOffset,
84
+ grabOffsetRatio: {
85
+ x: grabOffset.x / width,
86
+ y: grabOffset.y / height
87
+ },
88
+ hoverPosition: hoverPositionSV.value
89
+ };
90
+ };
91
+
92
+ /** Build receiver view event data */
93
+ const buildReceiverViewData = (receiverId, absolutePosition) => {
94
+ const entry = getViewEntry(receiverId);
95
+ if (!entry?.measurements) return undefined;
96
+
97
+ // Compute absolute measurements of receiver
98
+ const idx = entry.spatialIndex;
99
+ const entries = spatialIndexSV.value;
100
+ const offsets = scrollOffsetsSV.value;
101
+ const absPos = computeAbsolutePositionWorklet(idx, entries, offsets);
102
+ const absMeasurements = {
103
+ ...absPos,
104
+ width: entry.measurements.width,
105
+ height: entry.measurements.height,
106
+ _transformDetected: 0
107
+ };
108
+ const {
109
+ relativePosition,
110
+ relativePositionRatio
111
+ } = getRelativePosition(absolutePosition, absMeasurements);
112
+ return {
113
+ id: receiverId,
114
+ parentId: entry.parentId,
115
+ payload: entry.props.receiverPayload ?? entry.props.payload,
116
+ measurements: entry.measurements,
117
+ receiveOffset: relativePosition,
118
+ receiveOffsetRatio: relativePositionRatio
119
+ };
120
+ };
121
+
122
+ /** Called via runOnJS when drag starts */
123
+ const handleDragStart = (draggedId, absolutePosition, _grabOffset) => {
124
+ const draggedEntry = getViewEntry(draggedId);
125
+ if (!draggedEntry) return;
126
+ const dragged = buildDraggedViewData(draggedId, absolutePosition);
127
+ if (!dragged) return;
128
+ const startPos = startPositionSV.value;
129
+ const dragTranslation = {
130
+ x: absolutePosition.x - startPos.x,
131
+ y: absolutePosition.y - startPos.y
132
+ };
133
+
134
+ // Fire onDragStart callback
135
+ draggedEntry.props.onDragStart?.({
136
+ dragAbsolutePosition: absolutePosition,
137
+ dragTranslation,
138
+ dragged
139
+ });
140
+
141
+ // Setup hover styles — set BEFORE setHoverContent so HoverLayer
142
+ // captures them when it re-renders on hoverVersion change.
143
+ deps.hoverStylesRef.current = {
144
+ hoverStyle: flattenOrNull(draggedEntry.props.hoverStyle),
145
+ hoverDraggingStyle: flattenOrNull(draggedEntry.props.hoverDraggingStyle),
146
+ hoverDraggingWithReceiverStyle: flattenOrNull(draggedEntry.props.hoverDraggingWithReceiverStyle),
147
+ hoverDraggingWithoutReceiverStyle: flattenOrNull(draggedEntry.props.hoverDraggingWithoutReceiverStyle),
148
+ hoverDragReleasedStyle: flattenOrNull(draggedEntry.props.hoverDragReleasedStyle)
149
+ };
150
+
151
+ // Setup hover content
152
+ if (isDraggable(draggedEntry.props) && !draggedEntry.props.noHover) {
153
+ const renderFn = draggedEntry.props.renderHoverContent ?? draggedEntry.props.renderContent;
154
+ if (renderFn) {
155
+ const content = renderFn({
156
+ viewState: {
157
+ dragStatus: DraxViewDragStatus.Dragging,
158
+ receiveStatus: DraxViewReceiveStatus.Inactive,
159
+ grabOffset: dragged.grabOffset,
160
+ grabOffsetRatio: dragged.grabOffsetRatio
161
+ },
162
+ trackingStatus: {
163
+ dragging: true,
164
+ receiving: false
165
+ },
166
+ hover: true,
167
+ children: null,
168
+ dimensions: draggedEntry.measurements ? {
169
+ width: draggedEntry.measurements.width,
170
+ height: draggedEntry.measurements.height
171
+ } : undefined
172
+ });
173
+ setHoverContent(content);
174
+ } else {
175
+ // Default hover: wrap children with original view style and dimensions.
176
+ // Strip margins since hover is positioned via translateX/Y.
177
+ const dims = draggedEntry.measurements;
178
+ const viewStyle = draggedEntry.props.style;
179
+ setHoverContent(/*#__PURE__*/_jsx(View, {
180
+ style: [viewStyle, dims && {
181
+ width: dims.width,
182
+ height: dims.height
183
+ }, hoverResetStyle],
184
+ children: draggedEntry.props.children
185
+ }));
186
+ }
187
+ }
188
+
189
+ // Phase activation is handled by HoverLayer's useLayoutEffect — it fires
190
+ // AFTER React commits the hover content, ensuring both opacity:1 and
191
+ // draggingStyle apply on the same frame. See HoverLayer.tsx.
192
+
193
+ // Fire provider-level onDragStart
194
+ onProviderDragStart?.({
195
+ draggedId,
196
+ position: absolutePosition
197
+ });
198
+
199
+ // Fire monitor onMonitorDragStart callbacks
200
+ currentMonitorIdsRef.current = [];
201
+ };
202
+
203
+ /** Called via runOnJS on every gesture update for callback dispatch.
204
+ * Handles: enter/exit (on receiver change), onDragOver/onReceiveDragOver
205
+ * (continuous, same receiver), onDrag (continuous, no receiver), and monitors. */
206
+ const handleReceiverChange = (oldReceiverId, newReceiverId, absolutePosition, monitorIds) => {
207
+ const draggedId = draggedIdSV.value;
208
+
209
+ // Fast path: receiver unchanged, no monitors (now AND previously),
210
+ // and no continuous callbacks → skip event data construction entirely.
211
+ const newMonitorIds = monitorIds ?? [];
212
+ const prevMonitorIds = currentMonitorIdsRef.current;
213
+ if (oldReceiverId === newReceiverId && newMonitorIds.length === 0 && prevMonitorIds.length === 0) {
214
+ const draggedEntry = getViewEntry(draggedId);
215
+ if (!draggedEntry) return;
216
+ const hasOnDragOver = newReceiverId && draggedEntry.props.onDragOver;
217
+ const receiverEntry = newReceiverId ? getViewEntry(newReceiverId) : undefined;
218
+ const hasOnReceiveDragOver = newReceiverId && receiverEntry?.props.onReceiveDragOver;
219
+ const hasOnDrag = !newReceiverId && (draggedEntry.props.onDrag || onProviderDrag);
220
+ if (!hasOnDragOver && !hasOnReceiveDragOver && !hasOnDrag) return;
221
+ }
222
+ const dragged = buildDraggedViewData(draggedId, absolutePosition);
223
+ if (!dragged) return;
224
+ const draggedEntry = getViewEntry(draggedId);
225
+ const draggedPayload = draggedEntry?.props.dragPayload ?? draggedEntry?.props.payload;
226
+ const startPos = startPositionSV.value;
227
+ const dragTranslation = {
228
+ x: absolutePosition.x - startPos.x,
229
+ y: absolutePosition.y - startPos.y
230
+ };
231
+ const baseEventData = {
232
+ dragAbsolutePosition: absolutePosition,
233
+ dragTranslation,
234
+ dragged
235
+ };
236
+
237
+ // ── Check dynamicReceptiveCallback / acceptsDrag on new receiver ──
238
+ let acceptedReceiverId = newReceiverId;
239
+ if (newReceiverId && oldReceiverId !== newReceiverId) {
240
+ const newReceiverEntry = getViewEntry(newReceiverId);
241
+ if (newReceiverEntry) {
242
+ // Check acceptsDrag first (simpler convenience prop)
243
+ const acceptsDrag = newReceiverEntry.props.acceptsDrag;
244
+ if (acceptsDrag && !acceptsDrag(draggedPayload)) {
245
+ acceptedReceiverId = '';
246
+ }
247
+
248
+ // Check capacity
249
+ const capacity = newReceiverEntry.props.capacity;
250
+ if (acceptedReceiverId && capacity !== undefined) {
251
+ const droppedSet = droppedItemsRef.current.get(newReceiverId);
252
+ const count = droppedSet ? droppedSet.size : 0;
253
+ if (count >= capacity) {
254
+ acceptedReceiverId = '';
255
+ }
256
+ }
257
+
258
+ // Check dynamicReceptiveCallback (more detailed)
259
+ const dynamicCallback = newReceiverEntry.props.dynamicReceptiveCallback;
260
+ if (acceptedReceiverId && dynamicCallback && newReceiverEntry.measurements) {
261
+ const accepted = dynamicCallback({
262
+ targetId: newReceiverId,
263
+ targetMeasurements: newReceiverEntry.measurements,
264
+ draggedId,
265
+ draggedPayload
266
+ });
267
+ if (!accepted) {
268
+ acceptedReceiverId = '';
269
+ }
270
+ }
271
+ }
272
+
273
+ // If rejected, tell the gesture worklet to skip this receiver on future frames.
274
+ // Also clear receiverIdSV so animated styles don't flash the receiving state.
275
+ if (!acceptedReceiverId) {
276
+ runOnUI((_receiverIdSV, _rejectedReceiverIdSV, _rejectedId) => {
277
+ 'worklet';
278
+
279
+ _receiverIdSV.value = '';
280
+ _rejectedReceiverIdSV.value = _rejectedId;
281
+ })(deps.receiverIdSV, deps.rejectedReceiverIdSV, newReceiverId);
282
+ }
283
+ }
284
+
285
+ // Fire exit on old receiver (only when receiver actually changed)
286
+ if (oldReceiverId && oldReceiverId !== acceptedReceiverId) {
287
+ const oldReceiverEntry = getViewEntry(oldReceiverId);
288
+ const oldReceiverData = buildReceiverViewData(oldReceiverId, absolutePosition);
289
+ if (oldReceiverEntry && oldReceiverData) {
290
+ // Dragged view: onDragExit
291
+ draggedEntry?.props.onDragExit?.({
292
+ ...baseEventData,
293
+ receiver: oldReceiverData
294
+ });
295
+
296
+ // Receiver view: onReceiveDragExit
297
+ oldReceiverEntry.props.onReceiveDragExit?.({
298
+ ...baseEventData,
299
+ receiver: oldReceiverData,
300
+ cancelled: false
301
+ });
302
+ }
303
+ }
304
+
305
+ // Fire enter on new receiver (only when receiver actually changed)
306
+ if (acceptedReceiverId && oldReceiverId !== acceptedReceiverId) {
307
+ const newReceiverEntry = getViewEntry(acceptedReceiverId);
308
+ const newReceiverData = buildReceiverViewData(acceptedReceiverId, absolutePosition);
309
+ if (newReceiverEntry && newReceiverData) {
310
+ // Dragged view: onDragEnter
311
+ draggedEntry?.props.onDragEnter?.({
312
+ ...baseEventData,
313
+ receiver: newReceiverData
314
+ });
315
+
316
+ // Receiver view: onReceiveDragEnter
317
+ newReceiverEntry.props.onReceiveDragEnter?.({
318
+ ...baseEventData,
319
+ receiver: newReceiverData
320
+ });
321
+ }
322
+ }
323
+
324
+ // ── Continuous callbacks: onDragOver / onReceiveDragOver / onDrag ──
325
+ if (acceptedReceiverId && oldReceiverId === acceptedReceiverId) {
326
+ // Dragging over the same receiver — fire onDragOver + onReceiveDragOver
327
+ const receiverEntry = getViewEntry(acceptedReceiverId);
328
+ const receiverData = buildReceiverViewData(acceptedReceiverId, absolutePosition);
329
+ if (receiverEntry && receiverData) {
330
+ draggedEntry?.props.onDragOver?.({
331
+ ...baseEventData,
332
+ receiver: receiverData
333
+ });
334
+ receiverEntry.props.onReceiveDragOver?.({
335
+ ...baseEventData,
336
+ receiver: receiverData
337
+ });
338
+ }
339
+ } else if (!acceptedReceiverId) {
340
+ // No receiver — fire onDrag (continuous, not over any receiver)
341
+ draggedEntry?.props.onDrag?.(baseEventData);
342
+ }
343
+
344
+ // ── Dispatch monitor events ──────────────────────────────────────
345
+ const prevWasEmpty = prevMonitorIds.length === 0;
346
+
347
+ // Build receiver data for monitor event payload (use accepted receiver, not raw hit-test)
348
+ const receiverData = acceptedReceiverId ? buildReceiverViewData(acceptedReceiverId, absolutePosition) : undefined;
349
+
350
+ // Fire events on current monitors (start/enter before over)
351
+ for (const monitorId of newMonitorIds) {
352
+ const monitorEntry = getViewEntry(monitorId);
353
+ if (!monitorEntry?.measurements) continue;
354
+ const {
355
+ relativePosition: monitorOffset,
356
+ relativePositionRatio: monitorOffsetRatio
357
+ } = getRelativePosition(absolutePosition, monitorEntry.measurements);
358
+ const monitorEventData = {
359
+ ...baseEventData,
360
+ receiver: receiverData,
361
+ monitorOffset,
362
+ monitorOffsetRatio
363
+ };
364
+ const isNew = !prevMonitorIds.includes(monitorId);
365
+
366
+ // First time we see any monitor after drag start → fire onMonitorDragStart
367
+ if (isNew && prevWasEmpty) {
368
+ monitorEntry.props.onMonitorDragStart?.(monitorEventData);
369
+ }
370
+
371
+ // New monitor → fire onMonitorDragEnter
372
+ if (isNew) {
373
+ monitorEntry.props.onMonitorDragEnter?.(monitorEventData);
374
+ }
375
+
376
+ // All current monitors → fire onMonitorDragOver
377
+ monitorEntry.props.onMonitorDragOver?.(monitorEventData);
378
+ }
379
+
380
+ // Fire exit on monitors that are no longer hit
381
+ for (const prevMonitorId of prevMonitorIds) {
382
+ if (newMonitorIds.includes(prevMonitorId)) continue;
383
+ const monitorEntry = getViewEntry(prevMonitorId);
384
+ if (!monitorEntry?.measurements) continue;
385
+ const {
386
+ relativePosition: monitorOffset,
387
+ relativePositionRatio: monitorOffsetRatio
388
+ } = getRelativePosition(absolutePosition, monitorEntry.measurements);
389
+ monitorEntry.props.onMonitorDragExit?.({
390
+ ...baseEventData,
391
+ receiver: receiverData,
392
+ monitorOffset,
393
+ monitorOffsetRatio
394
+ });
395
+ }
396
+ currentMonitorIdsRef.current = newMonitorIds;
397
+
398
+ // Fire provider-level onDrag (use acceptedReceiverId, not raw newReceiverId)
399
+ onProviderDrag?.({
400
+ draggedId: draggedIdSV.value,
401
+ receiverId: acceptedReceiverId || undefined,
402
+ position: absolutePosition
403
+ });
404
+ };
405
+
406
+ /** Called via runOnJS when drag ends or is cancelled */
407
+ const handleDragEnd = (draggedId, receiverId, cancelled, finalMonitorIds) => {
408
+ // receiverIdSV is already cleared on the UI thread in onDeactivate/onFinalize,
409
+ // so the receiver's animated style resets immediately.
410
+
411
+ const draggedEntry = getViewEntry(draggedId);
412
+ if (!draggedEntry) {
413
+ // Reset drag state atomically on UI thread to avoid one-frame flash
414
+ runOnUI((_hoverReadySV, _dragPhaseSV, _draggedIdSV, _hoverPositionSV) => {
415
+ 'worklet';
416
+
417
+ _hoverReadySV.value = false;
418
+ _dragPhaseSV.value = 'idle';
419
+ _draggedIdSV.value = '';
420
+ _hoverPositionSV.value = {
421
+ x: 0,
422
+ y: 0
423
+ };
424
+ })(hoverReadySV, dragPhaseSV, draggedIdSV, hoverPositionSV);
425
+ setHoverContent(null);
426
+ return;
427
+ }
428
+ const absolutePosition = {
429
+ ...hoverPositionSV.value
430
+ };
431
+ const dragged = buildDraggedViewData(draggedId, absolutePosition);
432
+ if (!dragged) {
433
+ runOnUI((_hoverReadySV, _dragPhaseSV, _draggedIdSV, _hoverPositionSV) => {
434
+ 'worklet';
435
+
436
+ _hoverReadySV.value = false;
437
+ _dragPhaseSV.value = 'idle';
438
+ _draggedIdSV.value = '';
439
+ _hoverPositionSV.value = {
440
+ x: 0,
441
+ y: 0
442
+ };
443
+ })(hoverReadySV, dragPhaseSV, draggedIdSV, hoverPositionSV);
444
+ setHoverContent(null);
445
+ return;
446
+ }
447
+ const startPos = startPositionSV.value;
448
+ const dragTranslation = {
449
+ x: absolutePosition.x - startPos.x,
450
+ y: absolutePosition.y - startPos.y
451
+ };
452
+ const baseEventData = {
453
+ dragAbsolutePosition: absolutePosition,
454
+ dragTranslation,
455
+ dragged
456
+ };
457
+ let snapTarget = DraxSnapbackTargetPreset.Default;
458
+ if (receiverId && !cancelled) {
459
+ // Successful drop — default snap to receiver position
460
+ const receiverEntry = getViewEntry(receiverId);
461
+ const receiverData = buildReceiverViewData(receiverId, absolutePosition);
462
+ if (receiverData && receiverEntry) {
463
+ // Compute receiver's absolute position and center the dragged item within it
464
+ const receiverAbsPos = computeAbsolutePositionWorklet(receiverEntry.spatialIndex, spatialIndexSV.value, scrollOffsetsSV.value);
465
+ const draggedDims = draggedEntry.measurements;
466
+ const receiverDims = receiverEntry.measurements;
467
+ if (receiverDims && draggedDims) {
468
+ snapTarget = {
469
+ x: receiverAbsPos.x + (receiverDims.width - draggedDims.width) / 2,
470
+ y: receiverAbsPos.y + (receiverDims.height - draggedDims.height) / 2
471
+ };
472
+ } else {
473
+ snapTarget = receiverAbsPos;
474
+ }
475
+
476
+ // Fire onDragDrop on dragged (can override snap target)
477
+ const dragDropResponse = draggedEntry.props.onDragDrop?.({
478
+ ...baseEventData,
479
+ receiver: receiverData
480
+ });
481
+ if (dragDropResponse !== undefined) snapTarget = dragDropResponse;
482
+
483
+ // Fire onReceiveDragDrop on receiver (can override snap target)
484
+ const receiveDropResponse = receiverEntry.props.onReceiveDragDrop?.({
485
+ ...baseEventData,
486
+ receiver: receiverData
487
+ });
488
+ if (receiveDropResponse !== undefined) snapTarget = receiveDropResponse;
489
+
490
+ // Track the drop for capacity enforcement
491
+ if (!droppedItemsRef.current.has(receiverId)) {
492
+ droppedItemsRef.current.set(receiverId, new Set());
493
+ }
494
+ droppedItemsRef.current.get(receiverId).add(draggedId);
495
+ }
496
+ } else {
497
+ // No receiver or cancelled
498
+ const dragEndResponse = draggedEntry.props.onDragEnd?.({
499
+ ...baseEventData,
500
+ cancelled
501
+ });
502
+ if (dragEndResponse !== undefined) snapTarget = dragEndResponse;
503
+ }
504
+
505
+ // Fire monitor end events — use final hit-test monitors from onDeactivate
506
+ // if available, falling back to tracked monitors from receiver changes.
507
+ const monitorIdsToUse = finalMonitorIds ?? currentMonitorIdsRef.current;
508
+ for (const monitorId of monitorIdsToUse) {
509
+ const monitorEntry = getViewEntry(monitorId);
510
+ if (!monitorEntry?.measurements) continue;
511
+ const {
512
+ relativePosition: monitorOffset,
513
+ relativePositionRatio: monitorOffsetRatio
514
+ } = getRelativePosition(absolutePosition, monitorEntry.measurements);
515
+ if (receiverId && !cancelled) {
516
+ const receiverData = buildReceiverViewData(receiverId, absolutePosition);
517
+ if (receiverData) {
518
+ const monitorDropResponse = monitorEntry.props.onMonitorDragDrop?.({
519
+ ...baseEventData,
520
+ receiver: receiverData,
521
+ monitorOffset,
522
+ monitorOffsetRatio
523
+ });
524
+ if (monitorDropResponse !== undefined) snapTarget = monitorDropResponse;
525
+ }
526
+ } else {
527
+ const monitorEndResponse = monitorEntry.props.onMonitorDragEnd?.({
528
+ ...baseEventData,
529
+ monitorOffset,
530
+ monitorOffsetRatio,
531
+ cancelled
532
+ });
533
+ if (monitorEndResponse !== undefined) snapTarget = monitorEndResponse;
534
+ }
535
+ }
536
+
537
+ // Resolve Default snap target to root-relative visual position.
538
+ // Default triggers when monitors are empty or all callbacks return undefined.
539
+ // draggedEntry.measurements are content-relative (from measureLayout), so we
540
+ // use the spatial index to compute root-relative visual position instead.
541
+ if (snapTarget === DraxSnapbackTargetPreset.Default) {
542
+ const absPos = computeAbsolutePositionWorklet(draggedEntry.spatialIndex, spatialIndexSV.value, scrollOffsetsSV.value);
543
+ snapTarget = absPos;
544
+ }
545
+
546
+ // Handle snap-back animation
547
+ performSnapback(snapTarget, draggedEntry, receiverId ? getViewEntry(receiverId) : undefined, hoverPositionSV, dragPhaseSV, draggedIdSV, hoverReadySV, setHoverContent, deps.hoverClearDeferredRef);
548
+
549
+ // Fire provider-level onDragEnd (use last known hover position)
550
+ onProviderDragEnd?.({
551
+ draggedId,
552
+ receiverId: receiverId || undefined,
553
+ position: hoverPositionSV.value,
554
+ cancelled
555
+ });
556
+ };
557
+ return {
558
+ handleDragStart,
559
+ handleReceiverChange,
560
+ handleDragEnd
561
+ };
562
+ };
563
+
564
+ /**
565
+ * Perform the snap-back animation after drag ends.
566
+ *
567
+ * CRITICAL ORDERING: When the snap animation completes, we must:
568
+ * 1. Fire onSnapEnd callbacks → triggers finalizeDrag → commits reorder / cancels drag
569
+ * 2. THEN clear drag state (hover disappears, item becomes visible)
570
+ *
571
+ * This ordering ensures shifted items are cleaned up BEFORE the hover disappears.
572
+ * Without this, there's a visible gap where shifted items are at shifted positions
573
+ * but the hover is already gone (the "drop blink").
574
+ */
575
+ function performSnapback(target, draggedEntry, receiverEntry, hoverPositionSV, dragPhaseSV, draggedIdSV, hoverReadySV, setHoverContent, hoverClearDeferredRef) {
576
+ const animateSnap = draggedEntry.props.animateSnap ?? true;
577
+ const snapDelay = draggedEntry.props.snapDelay ?? defaultSnapbackDelay;
578
+ const snapDuration = draggedEntry.props.snapDuration ?? defaultSnapbackDuration;
579
+ const snapAnimator = draggedEntry.props.snapAnimator;
580
+
581
+ // Build snap event data for callbacks
582
+ const snapEventData = {
583
+ dragged: {
584
+ id: draggedEntry.id,
585
+ parentId: draggedEntry.parentId,
586
+ payload: draggedEntry.props.dragPayload ?? draggedEntry.props.payload
587
+ },
588
+ receiver: receiverEntry ? {
589
+ id: receiverEntry.id,
590
+ parentId: receiverEntry.parentId,
591
+ payload: receiverEntry.props.receiverPayload ?? receiverEntry.props.payload
592
+ } : undefined
593
+ };
594
+
595
+ /**
596
+ * Called when snap animation completes. Fires callbacks FIRST (so finalizeDrag
597
+ * can set permanent shifts + clear hover), THEN clears hover & drag state.
598
+ *
599
+ * For REORDER: finalizeDrag sets permanent shifts + clears hover via runOnUI
600
+ * in a single atomic block. No FlatList data change, so no blink.
601
+ *
602
+ * For CANCEL: finalizeDrag → cancelDrag → reverts to committed shifts.
603
+ * Then hover clears on next UI frame. Items at visual positions. No blink.
604
+ */
605
+ const onSnapComplete = () => {
606
+ // Reset the deferred flag before firing callbacks.
607
+ // finalizeDrag (called via onSnapEnd) may set it to true for reorder.
608
+ hoverClearDeferredRef.current = false;
609
+
610
+ // Step 1: Fire callbacks → finalizeDrag runs synchronously.
611
+ draggedEntry.props.onSnapEnd?.(snapEventData);
612
+ receiverEntry?.props.onReceiveSnapEnd?.(snapEventData);
613
+
614
+ // Step 2: Clear hover if NOT deferred by a sortable reorder.
615
+ if (!hoverClearDeferredRef.current) {
616
+ runOnUI((_hoverReadySV, _dragPhaseSV, _draggedIdSV, _hoverPositionSV) => {
617
+ 'worklet';
618
+
619
+ _hoverReadySV.value = false;
620
+ _dragPhaseSV.value = 'idle';
621
+ _draggedIdSV.value = '';
622
+ _hoverPositionSV.value = {
623
+ x: 0,
624
+ y: 0
625
+ };
626
+ })(hoverReadySV, dragPhaseSV, draggedIdSV, hoverPositionSV);
627
+ setHoverContent(null);
628
+ } else {
629
+ // Do NOT call setHoverContent(null) here — the hover must remain visible
630
+ // until the FlatList re-renders. The deferred cleanup in useLayoutEffect
631
+ // will clear SharedValues, and setHoverContent(null) is called there too.
632
+ }
633
+ };
634
+ if (target === DraxSnapbackTargetPreset.None || !animateSnap) {
635
+ // No snap animation — run cleanup immediately
636
+ onSnapComplete();
637
+ return;
638
+ }
639
+
640
+ // Determine snap-to position
641
+ let toValue;
642
+ if (isPosition(target)) {
643
+ toValue = target;
644
+ } else {
645
+ // Default: snap back to original view position
646
+ toValue = draggedEntry.measurements ? {
647
+ x: draggedEntry.measurements.x,
648
+ y: draggedEntry.measurements.y
649
+ } : {
650
+ x: 0,
651
+ y: 0
652
+ };
653
+ }
654
+ if (snapAnimator) {
655
+ // Custom snap animation
656
+ snapAnimator({
657
+ hoverPosition: hoverPositionSV,
658
+ toValue,
659
+ delay: snapDelay,
660
+ duration: snapDuration,
661
+ finishedCallback: finished => {
662
+ if (finished) {
663
+ onSnapComplete();
664
+ }
665
+ }
666
+ });
667
+ } else {
668
+ // Default withTiming snap animation.
669
+ // When animation finishes, bounce to JS for ordered cleanup.
670
+ hoverPositionSV.value = withDelay(snapDelay, withTiming(toValue, {
671
+ duration: snapDuration
672
+ }, finished => {
673
+ 'worklet';
674
+
675
+ if (finished) {
676
+ runOnJS(onSnapComplete)();
677
+ }
678
+ }));
679
+ }
680
+ }
681
+
682
+ // ─── Helpers ───────────────────────────────────────────────────────────────
683
+
684
+ function flattenOrNull(s) {
685
+ if (!s) return null;
686
+ return StyleSheet.flatten(s) ?? null;
687
+ }
688
+ //# sourceMappingURL=useCallbackDispatch.js.map