react-native-drax 0.11.0-alpha.1 → 1.0.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 +385 -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 +561 -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 +681 -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 +824 -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 +222 -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 +213 -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 +52 -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 +743 -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 +642 -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 +823 -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 +868 -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 +110 -0
  183. package/src/math.ts +251 -0
  184. package/src/params.ts +74 -0
  185. package/src/types.ts +919 -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 -8
  229. package/build/hooks/useMeasurements.js +0 -118
  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,430 @@
1
+ import type { ComponentRef, ReactNode } from 'react';
2
+ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef } from 'react';
3
+ import { Platform } from 'react-native';
4
+ import { GestureDetector } from 'react-native-gesture-handler';
5
+ import type { SharedValue } from 'react-native-reanimated';
6
+ import Reanimated, { useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
7
+
8
+ import { DraxHandleContext } from './DraxHandleContext';
9
+ import { DraxSubprovider } from './DraxSubprovider';
10
+ import { useDraxContext, useDraxId } from './hooks';
11
+ import { useDragGesture } from './hooks/useDragGesture';
12
+ import { isDraggable as computeIsDraggable } from './hooks/useSpatialIndex';
13
+ import { useViewStyles } from './hooks/useViewStyles';
14
+ import { defaultLongPressDelay } from './params';
15
+ import type {
16
+ DraxViewMeasurementHandler,
17
+ DraxViewMeasurements,
18
+ DraxViewProps,
19
+ Position,
20
+ } from './types';
21
+ import { DraxViewDragStatus, DraxViewReceiveStatus } from './types';
22
+
23
+ /** Keys that should NOT be passed through to Reanimated.View */
24
+ const DRAX_PROP_KEYS: ReadonlySet<string> = new Set([
25
+ 'renderContent',
26
+ 'renderHoverContent',
27
+ 'noHover',
28
+ 'registration',
29
+ 'onMeasure',
30
+ 'parent',
31
+ 'isParent',
32
+ 'scrollPosition',
33
+ 'longPressDelay',
34
+ 'lockDragXPosition',
35
+ 'lockDragYPosition',
36
+ 'id',
37
+ // Callback props
38
+ 'onDragStart',
39
+ 'onDrag',
40
+ 'onDragEnter',
41
+ 'onDragOver',
42
+ 'onDragExit',
43
+ 'onDragEnd',
44
+ 'onDragDrop',
45
+ 'onSnapEnd',
46
+ 'onReceiveSnapEnd',
47
+ 'onReceiveDragEnter',
48
+ 'onReceiveDragOver',
49
+ 'onReceiveDragExit',
50
+ 'onReceiveDragDrop',
51
+ 'onMonitorDragStart',
52
+ 'onMonitorDragEnter',
53
+ 'onMonitorDragOver',
54
+ 'onMonitorDragExit',
55
+ 'onMonitorDragEnd',
56
+ 'onMonitorDragDrop',
57
+ 'animateSnap',
58
+ 'snapDelay',
59
+ 'snapDuration',
60
+ 'snapAnimator',
61
+ 'dragPayload',
62
+ 'receiverPayload',
63
+ 'payload',
64
+ 'draggable',
65
+ 'receptive',
66
+ 'monitoring',
67
+ 'rejectOwnChildren',
68
+ 'disableHoverViewMeasurementsOnLayout',
69
+ 'dynamicReceptiveCallback',
70
+ 'acceptsDrag',
71
+ 'dragBoundsRef',
72
+ // Style props (handled by useViewStyles)
73
+ 'style',
74
+ 'dragInactiveStyle',
75
+ 'draggingStyle',
76
+ 'draggingWithReceiverStyle',
77
+ 'draggingWithoutReceiverStyle',
78
+ 'dragReleasedStyle',
79
+ 'hoverStyle',
80
+ 'hoverDraggingStyle',
81
+ 'hoverDraggingWithReceiverStyle',
82
+ 'hoverDraggingWithoutReceiverStyle',
83
+ 'hoverDragReleasedStyle',
84
+ 'receiverInactiveStyle',
85
+ 'receivingStyle',
86
+ 'otherDraggingStyle',
87
+ 'otherDraggingWithReceiverStyle',
88
+ 'otherDraggingWithoutReceiverStyle',
89
+ 'dragHandle',
90
+ 'dragActivationFailOffset',
91
+ 'collisionAlgorithm',
92
+ 'scrollHorizontal',
93
+ ]);
94
+
95
+ /** Extract only ViewProps-compatible props by filtering out Drax-specific keys */
96
+ function extractViewProps(props: DraxViewProps): Record<string, unknown> {
97
+ const viewProps: Record<string, unknown> = {};
98
+ for (const [key, value] of Object.entries(props)) {
99
+ if (!DRAX_PROP_KEYS.has(key)) {
100
+ viewProps[key] = value;
101
+ }
102
+ }
103
+ return viewProps;
104
+ }
105
+
106
+ /**
107
+ * Isolated hook for scroll-position → spatial-index sync.
108
+ * Kept separate from DraxView so the worklet closure only captures
109
+ * SharedValues — never React refs from the component scope. The worklets
110
+ * serializer recursively freezes all plain objects in a worklet's closure,
111
+ * which would freeze useRef objects and trigger "Tried to modify key `current`"
112
+ * warnings when React nullifies refs on unmount.
113
+ */
114
+ function useScrollPositionSync(
115
+ scrollPosition: SharedValue<Position> | undefined,
116
+ spatialIndexSV: SharedValue<number>,
117
+ scrollOffsetsSV: SharedValue<Position[]>
118
+ ) {
119
+ useAnimatedReaction(
120
+ () => scrollPosition?.value,
121
+ (pos, prev) => {
122
+ 'worklet';
123
+ if (!pos) return;
124
+ if (prev && pos.x === prev.x && pos.y === prev.y) return;
125
+ const idx = spatialIndexSV.value;
126
+ if (idx < 0) return;
127
+ scrollOffsetsSV.modify((offsets) => {
128
+ if (idx >= 0 && idx < offsets.length) {
129
+ offsets[idx] = pos;
130
+ }
131
+ return offsets;
132
+ });
133
+ }
134
+ );
135
+ }
136
+
137
+ export const DraxView = memo((props: DraxViewProps): ReactNode => {
138
+ const {
139
+ renderContent,
140
+ registration,
141
+ onMeasure,
142
+ parent: parentProp,
143
+ isParent,
144
+ scrollPosition,
145
+ longPressDelay = defaultLongPressDelay,
146
+ lockDragXPosition,
147
+ lockDragYPosition,
148
+ scrollHorizontal,
149
+ dragHandle,
150
+ dragBoundsRef,
151
+ children,
152
+ style,
153
+ id: idProp,
154
+ } = props;
155
+
156
+ // Determine capabilities from props (shared with useSpatialIndex)
157
+ const draggable = computeIsDraggable(props);
158
+
159
+ // Unique id
160
+ const id = useDraxId(idProp);
161
+
162
+ // Connect with Drax context
163
+ const {
164
+ registerView,
165
+ unregisterView,
166
+ updateMeasurements: updateMeasurementsCtx,
167
+ updateViewProps,
168
+ getViewEntry,
169
+ rootViewRef,
170
+ scrollOffsetsSV,
171
+ parent: contextParent,
172
+ } = useDraxContext();
173
+
174
+ // Parent view (from prop or context)
175
+ const parent = parentProp ?? contextParent;
176
+ const parentId = parent?.id;
177
+ const parentViewRef = parent ? parent.viewRef : rootViewRef;
178
+
179
+ // View ref for measuring
180
+ const viewRef = useRef<ComponentRef<typeof Reanimated.View>>(null);
181
+ const measurementsRef = useRef<DraxViewMeasurements | undefined>(undefined);
182
+
183
+ // ── Measurement ────────────────────────────────────────────────────
184
+
185
+ /** Finalize measurements and notify consumers.
186
+ * `transformDetected` = 1 when auto-detection found transform-based positioning
187
+ * (visual measurement used instead of Yoga layout). Consumers can check
188
+ * `measurements._transformDetected` to know whether shift subtraction is needed. */
189
+ const finalizeMeasurement = useCallback(
190
+ (x: number, y: number, width: number, height: number, handler?: DraxViewMeasurementHandler, transformDetected = 0) => {
191
+ const measurements: DraxViewMeasurements = { height, x, y, width, _transformDetected: transformDetected };
192
+ measurementsRef.current = measurements;
193
+ updateMeasurementsCtx(id, measurements);
194
+ onMeasure?.(measurements);
195
+ handler?.(measurements);
196
+ },
197
+ [id, updateMeasurementsCtx, onMeasure],
198
+ );
199
+
200
+ const measureWithHandler = useCallback((handler?: DraxViewMeasurementHandler) => {
201
+ const view = viewRef.current;
202
+ if (!view || !parentViewRef.current) return;
203
+
204
+ view.measureLayout(
205
+ parentViewRef.current,
206
+ (x, y, width, height) => {
207
+ if (Platform.OS === 'web') {
208
+ // On web, measureLayout returns visual positions — add scroll to
209
+ // convert to content-relative.
210
+ const parentData = parentId ? getViewEntry(parentId) : undefined;
211
+ const parentScroll = parentData?.scrollPosition?.value ?? { x: 0, y: 0 };
212
+ finalizeMeasurement(x! + parentScroll.x, y! + parentScroll.y, width!, height!, handler);
213
+ return;
214
+ }
215
+
216
+ // On Fabric, measureLayout uses includeTransform=false → returns Yoga
217
+ // layout positions. This is correct for FlatList/FlashList, but wrong
218
+ // for LegendList (which positions items via translateY, so all report y=0).
219
+ //
220
+ // Auto-detect: when measureLayout returns position=0 (the hallmark of
221
+ // transform-positioned items: position:absolute, top:0), also call
222
+ // measure() to get the visual position. If it differs, the item is
223
+ // transform-positioned. Only check when layoutPosition=0 to avoid
224
+ // false positives on shifted items (whose measureLayout is non-zero).
225
+ const layoutX = x!;
226
+ const layoutY = y!;
227
+ if (layoutX !== 0 && layoutY !== 0) {
228
+ // Non-zero layout position → normal Yoga layout, trust measureLayout.
229
+ finalizeMeasurement(layoutX, layoutY, width!, height!, handler);
230
+ return;
231
+ }
232
+ const parentView = parentViewRef.current;
233
+ if (!parentView) {
234
+ finalizeMeasurement(layoutX, layoutY, width!, height!, handler);
235
+ return;
236
+ }
237
+ view.measure((_vx: number, _vy: number, _vw: number, _vh: number, pageX: number, pageY: number) => {
238
+ parentView.measure((_px: number, _py: number, _pw: number, _ph: number, parentPageX: number, parentPageY: number) => {
239
+ const parentData = parentId ? getViewEntry(parentId) : undefined;
240
+ const parentScroll = parentData?.scrollPosition?.value ?? { x: 0, y: 0 };
241
+ const visualX = pageX - parentPageX + parentScroll.x;
242
+ const visualY = pageY - parentPageY + parentScroll.y;
243
+ // If visual position differs from layout, the view is transform-positioned.
244
+ if (Math.abs(visualX - layoutX) > 1 || Math.abs(visualY - layoutY) > 1) {
245
+ finalizeMeasurement(visualX, visualY, width!, height!, handler, 1);
246
+ } else {
247
+ finalizeMeasurement(layoutX, layoutY, width!, height!, handler);
248
+ }
249
+ });
250
+ });
251
+ },
252
+ () => {}
253
+ );
254
+ }, [id, parentId, viewRef, parentViewRef, getViewEntry, finalizeMeasurement]);
255
+
256
+ // ── Register/unregister with context ────────────────────────────────
257
+ // Keep a ref to the latest props so registry always has current callbacks
258
+ const propsRef = useRef(props);
259
+ propsRef.current = props;
260
+
261
+ useEffect(() => {
262
+ registerView({
263
+ id,
264
+ parentId,
265
+ scrollPosition,
266
+ props: propsRef.current,
267
+ });
268
+ // Re-measure after registration. onLayout may have fired before
269
+ // registerView (useEffect runs after paint), causing updateMeasurements
270
+ // to silently drop data (entry didn't exist yet in registry).
271
+ measureWithHandler();
272
+ return () => unregisterView(id);
273
+ }, [id, parentId, scrollPosition, registerView, unregisterView, measureWithHandler]);
274
+
275
+ // ── Update registry when props change ────────────────────────────────
276
+ useEffect(() => {
277
+ updateViewProps(id, propsRef.current);
278
+ }, [id, updateViewProps, draggable, props.receptive, props.monitoring, props.collisionAlgorithm]);
279
+
280
+ const onLayout = () => {
281
+ measureWithHandler();
282
+ // Re-measure drag bounds on every layout change. The initial useEffect
283
+ // measurement may fire before the parent flex layout has settled (especially
284
+ // on native where Fabric commits layout asynchronously). By the time this
285
+ // DraxView receives onLayout, the bounds view's layout is also finalized.
286
+ if (dragBoundsRef?.current && rootViewRef.current) {
287
+ dragBoundsRef.current.measureLayout(
288
+ rootViewRef.current,
289
+ (x: number, y: number, width: number, height: number) => {
290
+ dragBoundsSV.value = { x, y, width, height };
291
+ },
292
+ () => {}
293
+ );
294
+ }
295
+ };
296
+
297
+ // External registration — useLayoutEffect so SortableItem's FLIP
298
+ // useLayoutEffect (which runs after children) sees measureFnRef.
299
+ useLayoutEffect(() => {
300
+ if (registration) {
301
+ registration({ id, measure: measureWithHandler });
302
+ return () => registration(undefined);
303
+ }
304
+ return undefined;
305
+ }, [id, measureWithHandler, registration]);
306
+
307
+ // ── Gesture (per-view, UI thread) ──────────────────────────────────
308
+ // Use a SharedValue for spatialIndex so it updates reactively after registration
309
+ const spatialIndexSV = useSharedValue(-1);
310
+
311
+ // Update spatialIndex after registration completes
312
+ useEffect(() => {
313
+ const entry = getViewEntry(id);
314
+ const index = entry?.spatialIndex ?? -1;
315
+ spatialIndexSV.value = index;
316
+ }, [id, getViewEntry, spatialIndexSV]);
317
+
318
+ // Sync scroll position to spatial index — delegated to a separate hook
319
+ // so the worklet closure only contains SharedValues (no refs from DraxView scope).
320
+ useScrollPositionSync(scrollPosition, spatialIndexSV, scrollOffsetsSV);
321
+
322
+ // SharedValues for gesture config — RNGH 3.0 reconfigures the native
323
+ // handler on the UI thread, bypassing JS→native bridge entirely.
324
+ const draggableSV = useSharedValue(draggable);
325
+ const longPressDelaySV = useSharedValue(longPressDelay);
326
+
327
+ // Update SharedValues when props change (in useEffect to avoid render-time writes)
328
+ useEffect(() => {
329
+ draggableSV.value = draggable;
330
+ longPressDelaySV.value = longPressDelay;
331
+ }, [draggable, longPressDelay, draggableSV, longPressDelaySV]);
332
+
333
+ // Drag bounds: measure the bounds view relative to root and store in SharedValue
334
+ const dragBoundsSV = useSharedValue<{ x: number; y: number; width: number; height: number } | null>(null);
335
+ useEffect(() => {
336
+ if (dragBoundsRef?.current && rootViewRef.current) {
337
+ dragBoundsRef.current.measureLayout(
338
+ rootViewRef.current,
339
+ (x: number, y: number, width: number, height: number) => {
340
+ dragBoundsSV.value = { x, y, width, height };
341
+ },
342
+ () => {}
343
+ );
344
+ } else {
345
+ dragBoundsSV.value = null;
346
+ }
347
+ }, [dragBoundsRef, rootViewRef, dragBoundsSV]);
348
+
349
+ const gesture = useDragGesture(
350
+ id,
351
+ spatialIndexSV,
352
+ draggableSV,
353
+ longPressDelaySV,
354
+ lockDragXPosition,
355
+ lockDragYPosition,
356
+ dragBoundsSV,
357
+ props.dragActivationFailOffset,
358
+ scrollHorizontal
359
+ );
360
+
361
+ // ── Animated styles ────────────────────────────────────────────────
362
+ const { animatedDragStyle } = useViewStyles(id, props);
363
+
364
+ // ── Memoize parent for DraxSubprovider ──────────────────────────────
365
+ const subproviderParent = useMemo(
366
+ () => ({ id, viewRef }),
367
+ [id, viewRef]
368
+ );
369
+
370
+ // ── Rendered children ──────────────────────────────────────────────
371
+ let renderedContent: ReactNode;
372
+ if (renderContent) {
373
+ renderedContent = renderContent({
374
+ viewState: {
375
+ dragStatus: DraxViewDragStatus.Inactive,
376
+ receiveStatus: DraxViewReceiveStatus.Inactive,
377
+ },
378
+ hover: false,
379
+ children,
380
+ dimensions: measurementsRef.current
381
+ ? {
382
+ width: measurementsRef.current.width,
383
+ height: measurementsRef.current.height,
384
+ }
385
+ : undefined,
386
+ });
387
+ } else {
388
+ renderedContent = children;
389
+ }
390
+
391
+ if (isParent) {
392
+ renderedContent = (
393
+ <DraxSubprovider parent={subproviderParent}>{renderedContent}</DraxSubprovider>
394
+ );
395
+ }
396
+
397
+ // When dragHandle is true, provide the gesture via context so DraxHandle can attach it
398
+ if (dragHandle) {
399
+ renderedContent = (
400
+ <DraxHandleContext.Provider value={{ gesture }}>
401
+ {renderedContent}
402
+ </DraxHandleContext.Provider>
403
+ );
404
+ }
405
+
406
+ // ── Extract view-safe props ─────────────────────────────────────
407
+ // DraxView is memo()'d so props identity is stable between renders.
408
+ const viewProps = useMemo(() => extractViewProps(props), [props]);
409
+
410
+ // ── Render ─────────────────────────────────────────────────────────
411
+ const viewElement = (
412
+ <Reanimated.View
413
+ {...viewProps}
414
+ style={[style, animatedDragStyle]}
415
+ ref={viewRef}
416
+ onLayout={onLayout}
417
+ collapsable={false}
418
+ >
419
+ {renderedContent}
420
+ </Reanimated.View>
421
+ );
422
+
423
+ // When dragHandle is true, skip the GestureDetector wrapper —
424
+ // the gesture is attached to the DraxHandle child instead.
425
+ if (dragHandle) {
426
+ return viewElement;
427
+ }
428
+
429
+ return <GestureDetector gesture={gesture}>{viewElement}</GestureDetector>;
430
+ });
@@ -0,0 +1,167 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ import { memo, useLayoutEffect } from 'react';
3
+ import type { ViewStyle } from 'react-native';
4
+ import { StyleSheet } from 'react-native';
5
+ import type { SharedValue } from 'react-native-reanimated';
6
+ import Reanimated, { useAnimatedStyle } from 'react-native-reanimated';
7
+ import { runOnUI } from 'react-native-worklets';
8
+
9
+ import type { DragPhase, Position } from './types';
10
+
11
+ /** Flattened hover styles for the currently dragged view */
12
+ export interface FlattenedHoverStyles {
13
+ hoverStyle: ViewStyle | null;
14
+ hoverDraggingStyle: ViewStyle | null;
15
+ hoverDraggingWithReceiverStyle: ViewStyle | null;
16
+ hoverDraggingWithoutReceiverStyle: ViewStyle | null;
17
+ hoverDragReleasedStyle: ViewStyle | null;
18
+ }
19
+
20
+ interface HoverLayerProps {
21
+ hoverContentRef: RefObject<ReactNode>;
22
+ /** Changing this value triggers a re-render to pick up new ref content */
23
+ hoverVersion: number;
24
+ hoverPositionSV: SharedValue<Position>;
25
+ dragPhaseSV: SharedValue<DragPhase>;
26
+ receiverIdSV: SharedValue<string>;
27
+ /** Set to true after hover content is committed — SortableItem reads this for visibility */
28
+ hoverReadySV: SharedValue<boolean>;
29
+ /** Animated hover content dimensions. x=width, y=height. {0,0}=no constraint. */
30
+ hoverDimsSV: SharedValue<Position>;
31
+ /** Ref to flattened hover styles of the currently dragged view */
32
+ hoverStylesRef: RefObject<FlattenedHoverStyles | null>;
33
+ }
34
+
35
+ /**
36
+ * Single hover layer component that renders the hover content during drag.
37
+ *
38
+ * This is the ONLY component that reads hoverPositionSV (changes every frame).
39
+ * All other DraxViews read draggedIdSV/receiverIdSV/dragPhaseSV which change ~5x per drag.
40
+ *
41
+ * Content is passed via ref to avoid re-rendering the entire DraxProvider tree.
42
+ * Only this component re-renders when hover content changes (via hoverVersion).
43
+ */
44
+ export const HoverLayer = memo(
45
+ ({ hoverContentRef, hoverVersion, hoverPositionSV, dragPhaseSV, receiverIdSV, hoverReadySV, hoverDimsSV, hoverStylesRef }: HoverLayerProps) => {
46
+ // After hover content is committed to the DOM, activate drag phase + signal readiness.
47
+ // dragPhaseSV is NOT set in the gesture handler — it's set HERE, ensuring:
48
+ // 1. HoverLayer becomes visible (opacity 1) only AFTER content is rendered
49
+ // 2. SortableItem hides only AFTER hover is visible (reads hoverReadySV)
50
+ // Both writes happen in the same runOnUI call → same UI frame → no blink.
51
+ useLayoutEffect(() => {
52
+ if (hoverContentRef.current != null) {
53
+ runOnUI((_dragPhaseSV: SharedValue<DragPhase>, _hoverReadySV: SharedValue<boolean>) => {
54
+ 'worklet';
55
+ _dragPhaseSV.value = 'dragging';
56
+ _hoverReadySV.value = true;
57
+ })(dragPhaseSV, hoverReadySV);
58
+ }
59
+ }, [hoverVersion]);
60
+
61
+ // Read hover styles from ref in the component body — they're captured by the
62
+ // worklet closure when the component re-renders (on hoverVersion change).
63
+ // This ensures the latest styles are available without SharedValues.
64
+ const hs = hoverStylesRef.current;
65
+ const flatHoverStyle = hs?.hoverStyle ?? null;
66
+ const flatHoverDraggingStyle = hs?.hoverDraggingStyle ?? null;
67
+ const flatHoverDraggingWithReceiverStyle = hs?.hoverDraggingWithReceiverStyle ?? null;
68
+ const flatHoverDraggingWithoutReceiverStyle = hs?.hoverDraggingWithoutReceiverStyle ?? null;
69
+ const flatHoverDragReleasedStyle = hs?.hoverDragReleasedStyle ?? null;
70
+
71
+ // Position style: applied to the outer full-screen container.
72
+ // Only handles positioning (translate) and visibility (opacity).
73
+ const positionStyle = useAnimatedStyle(() => {
74
+ const phase = dragPhaseSV.value;
75
+ if (phase === 'idle') {
76
+ return { opacity: 0 };
77
+ }
78
+ return {
79
+ opacity: 1,
80
+ transform: [
81
+ { translateX: hoverPositionSV.value.x },
82
+ { translateY: hoverPositionSV.value.y },
83
+ ] as const,
84
+ };
85
+ });
86
+
87
+ // Visual style: applied to the inner content wrapper.
88
+ // Handles user hover styles (border, shadow, scale, rotate, etc.)
89
+ // so they apply to the content bounds, not the full-screen container.
90
+ const visualStyle = useAnimatedStyle(() => {
91
+ const phase = dragPhaseSV.value;
92
+ if (phase === 'idle') {
93
+ return {};
94
+ }
95
+
96
+ let hoverStyles: ViewStyle;
97
+ if (phase === 'dragging') {
98
+ const hasReceiver = receiverIdSV.value !== '';
99
+ hoverStyles = {
100
+ ...(flatHoverStyle ?? {}),
101
+ ...(flatHoverDraggingStyle ?? {}),
102
+ ...(hasReceiver
103
+ ? (flatHoverDraggingWithReceiverStyle ?? {})
104
+ : (flatHoverDraggingWithoutReceiverStyle ?? {})),
105
+ };
106
+ } else {
107
+ // phase === 'releasing'
108
+ hoverStyles = {
109
+ ...(flatHoverStyle ?? {}),
110
+ ...(flatHoverDragReleasedStyle ?? flatHoverDraggingStyle ?? {}),
111
+ };
112
+ }
113
+
114
+ // User transforms (rotate, scale, etc.) stay on the content wrapper.
115
+ const { transform: userTransform, ...restStyles } = hoverStyles;
116
+
117
+ return {
118
+ ...restStyles,
119
+ ...(userTransform ? { transform: userTransform as { [key: string]: number }[] } : {}),
120
+ };
121
+ });
122
+
123
+ // Animated dimensions for the inner content wrapper.
124
+ // When hoverDimsSV is non-zero, constrains hover content to those dimensions
125
+ // so cross-container transfers animate smoothly from source to target size.
126
+ const dimensionStyle = useAnimatedStyle(() => {
127
+ const dims = hoverDimsSV.value;
128
+ if (dims.x > 0 && dims.y > 0) {
129
+ return {
130
+ width: dims.x,
131
+ height: dims.y,
132
+ };
133
+ }
134
+ return {};
135
+ });
136
+
137
+ // Always render the Reanimated.View — never conditionally unmount it.
138
+ // If we returned null when content is empty, remounting causes a one-frame
139
+ // flash (the view renders at default position before useAnimatedStyle kicks in).
140
+ return (
141
+ <Reanimated.View
142
+ style={[styles.container, positionStyle]}
143
+ pointerEvents="none"
144
+ >
145
+ <Reanimated.View style={[styles.content, dimensionStyle, visualStyle]}>
146
+ {hoverContentRef.current}
147
+ </Reanimated.View>
148
+ </Reanimated.View>
149
+ );
150
+ }
151
+ );
152
+
153
+ const styles = StyleSheet.create({
154
+ container: {
155
+ ...StyleSheet.absoluteFillObject,
156
+ // Default hidden — useAnimatedStyle overrides to opacity:1 when dragging.
157
+ // Prevents a one-frame flash on first mount before the animated style evaluates.
158
+ opacity: 0,
159
+ transformOrigin: 'top left',
160
+ },
161
+ content: {
162
+ // Shrink-wrap to content width — without this the inner view stretches
163
+ // to fill the full-screen absolute parent, causing hover styles (border,
164
+ // shadow) to render at screen width instead of content width.
165
+ alignSelf: 'flex-start',
166
+ },
167
+ });