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