react-native-layout-dnd 0.1.0-alpha.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 (340) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +262 -0
  3. package/lib/module/adapters/coordinates.js +135 -0
  4. package/lib/module/adapters/coordinates.js.map +1 -0
  5. package/lib/module/adapters/scroll.js +81 -0
  6. package/lib/module/adapters/scroll.js.map +1 -0
  7. package/lib/module/adapters/virtualizedList.js +69 -0
  8. package/lib/module/adapters/virtualizedList.js.map +1 -0
  9. package/lib/module/components/DndProvider.js +1144 -0
  10. package/lib/module/components/DndProvider.js.map +1 -0
  11. package/lib/module/components/DragHandle.js +65 -0
  12. package/lib/module/components/DragHandle.js.map +1 -0
  13. package/lib/module/components/GridController.js +151 -0
  14. package/lib/module/components/GridController.js.map +1 -0
  15. package/lib/module/components/GridDragHandle.js +15 -0
  16. package/lib/module/components/GridDragHandle.js.map +1 -0
  17. package/lib/module/components/GridZoneView.js +59 -0
  18. package/lib/module/components/GridZoneView.js.map +1 -0
  19. package/lib/module/components/SortableGrid.js +305 -0
  20. package/lib/module/components/SortableGrid.js.map +1 -0
  21. package/lib/module/components/SortableList.js +714 -0
  22. package/lib/module/components/SortableList.js.map +1 -0
  23. package/lib/module/components/dndTypes.js +4 -0
  24. package/lib/module/components/dndTypes.js.map +1 -0
  25. package/lib/module/components/gridTypes.js +4 -0
  26. package/lib/module/components/gridTypes.js.map +1 -0
  27. package/lib/module/components/handleViewProps.js +13 -0
  28. package/lib/module/components/handleViewProps.js.map +1 -0
  29. package/lib/module/contracts.js +4 -0
  30. package/lib/module/contracts.js.map +1 -0
  31. package/lib/module/controller/approval.js +98 -0
  32. package/lib/module/controller/approval.js.map +1 -0
  33. package/lib/module/controller/coordinatorBase.js +93 -0
  34. package/lib/module/controller/coordinatorBase.js.map +1 -0
  35. package/lib/module/controller/dndSession.js +621 -0
  36. package/lib/module/controller/dndSession.js.map +1 -0
  37. package/lib/module/controller/dndState.js +103 -0
  38. package/lib/module/controller/dndState.js.map +1 -0
  39. package/lib/module/controller/gridAdapter.js +191 -0
  40. package/lib/module/controller/gridAdapter.js.map +1 -0
  41. package/lib/module/controller/gridState.js +101 -0
  42. package/lib/module/controller/gridState.js.map +1 -0
  43. package/lib/module/controller/locations.js +48 -0
  44. package/lib/module/controller/locations.js.map +1 -0
  45. package/lib/module/controller/previewPath.js +73 -0
  46. package/lib/module/controller/previewPath.js.map +1 -0
  47. package/lib/module/controller/sessionCore.js +222 -0
  48. package/lib/module/controller/sessionCore.js.map +1 -0
  49. package/lib/module/controller/useDndState.js +9 -0
  50. package/lib/module/controller/useDndState.js.map +1 -0
  51. package/lib/module/controller/useGridState.js +9 -0
  52. package/lib/module/controller/useGridState.js.map +1 -0
  53. package/lib/module/controller/useStoreState.js +15 -0
  54. package/lib/module/controller/useStoreState.js.map +1 -0
  55. package/lib/module/defaults.js +7 -0
  56. package/lib/module/defaults.js.map +1 -0
  57. package/lib/module/diagnostics.js +16 -0
  58. package/lib/module/diagnostics.js.map +1 -0
  59. package/lib/module/engine/gridPlacements.js +54 -0
  60. package/lib/module/engine/gridPlacements.js.map +1 -0
  61. package/lib/module/engine/index.js +19 -0
  62. package/lib/module/engine/index.js.map +1 -0
  63. package/lib/module/engine/layout.js +36 -0
  64. package/lib/module/engine/layout.js.map +1 -0
  65. package/lib/module/engine/layoutMove.js +326 -0
  66. package/lib/module/engine/layoutMove.js.map +1 -0
  67. package/lib/module/engine/layoutState.js +344 -0
  68. package/lib/module/engine/layoutState.js.map +1 -0
  69. package/lib/module/engine/list.js +172 -0
  70. package/lib/module/engine/list.js.map +1 -0
  71. package/lib/module/engine/listIndex.js +125 -0
  72. package/lib/module/engine/listIndex.js.map +1 -0
  73. package/lib/module/engine/move.js +216 -0
  74. package/lib/module/engine/move.js.map +1 -0
  75. package/lib/module/engine/movementPolicy.js +77 -0
  76. package/lib/module/engine/movementPolicy.js.map +1 -0
  77. package/lib/module/engine/orderedTarget.js +71 -0
  78. package/lib/module/engine/orderedTarget.js.map +1 -0
  79. package/lib/module/engine/pack.js +30 -0
  80. package/lib/module/engine/pack.js.map +1 -0
  81. package/lib/module/engine/rectangles.js +12 -0
  82. package/lib/module/engine/rectangles.js.map +1 -0
  83. package/lib/module/engine/sameLayout.js +29 -0
  84. package/lib/module/engine/sameLayout.js.map +1 -0
  85. package/lib/module/engine/search.js +159 -0
  86. package/lib/module/engine/search.js.map +1 -0
  87. package/lib/module/engine/spatial.js +27 -0
  88. package/lib/module/engine/spatial.js.map +1 -0
  89. package/lib/module/engine/spatialAppRotation.js +39 -0
  90. package/lib/module/engine/spatialAppRotation.js.map +1 -0
  91. package/lib/module/engine/spatialExchange.js +164 -0
  92. package/lib/module/engine/spatialExchange.js.map +1 -0
  93. package/lib/module/engine/spatialInsertion.js +135 -0
  94. package/lib/module/engine/spatialInsertion.js.map +1 -0
  95. package/lib/module/engine/spatialPolicy.js +41 -0
  96. package/lib/module/engine/spatialPolicy.js.map +1 -0
  97. package/lib/module/engine/spatialRotation.js +48 -0
  98. package/lib/module/engine/spatialRotation.js.map +1 -0
  99. package/lib/module/engine/validation.js +205 -0
  100. package/lib/module/engine/validation.js.map +1 -0
  101. package/lib/module/gesture/DndGestureGroup.js +268 -0
  102. package/lib/module/gesture/DndGestureGroup.js.map +1 -0
  103. package/lib/module/gesture/DndGestureHost.js +32 -0
  104. package/lib/module/gesture/DndGestureHost.js.map +1 -0
  105. package/lib/module/gesture/gestureConfig.js +81 -0
  106. package/lib/module/gesture/gestureConfig.js.map +1 -0
  107. package/lib/module/gesture/gestureGrouping.js +122 -0
  108. package/lib/module/gesture/gestureGrouping.js.map +1 -0
  109. package/lib/module/gesture/gestureHost.js +106 -0
  110. package/lib/module/gesture/gestureHost.js.map +1 -0
  111. package/lib/module/index.js +23 -0
  112. package/lib/module/index.js.map +1 -0
  113. package/lib/module/package.json +1 -0
  114. package/lib/module/pointer/dndTarget.js +101 -0
  115. package/lib/module/pointer/dndTarget.js.map +1 -0
  116. package/lib/module/pointer/geometry.js +363 -0
  117. package/lib/module/pointer/geometry.js.map +1 -0
  118. package/lib/module/pointer/layoutTarget.js +307 -0
  119. package/lib/module/pointer/layoutTarget.js.map +1 -0
  120. package/lib/module/pointer/listCellMotion.js +99 -0
  121. package/lib/module/pointer/listCellMotion.js.map +1 -0
  122. package/lib/module/pointer/listTarget.js +42 -0
  123. package/lib/module/pointer/listTarget.js.map +1 -0
  124. package/lib/module/pointer/motion.js +37 -0
  125. package/lib/module/pointer/motion.js.map +1 -0
  126. package/lib/module/runtime/dndPresentation.js +81 -0
  127. package/lib/module/runtime/dndPresentation.js.map +1 -0
  128. package/lib/module/runtime/dndRenderStore.js +129 -0
  129. package/lib/module/runtime/dndRenderStore.js.map +1 -0
  130. package/lib/module/runtime/dndRuntime.js +135 -0
  131. package/lib/module/runtime/dndRuntime.js.map +1 -0
  132. package/lib/module/runtime/dndSettlement.js +77 -0
  133. package/lib/module/runtime/dndSettlement.js.map +1 -0
  134. package/lib/module/runtime/gridRuntime.js +37 -0
  135. package/lib/module/runtime/gridRuntime.js.map +1 -0
  136. package/lib/module/types.js +4 -0
  137. package/lib/module/types.js.map +1 -0
  138. package/lib/typescript/package.json +1 -0
  139. package/lib/typescript/src/adapters/coordinates.d.ts +9 -0
  140. package/lib/typescript/src/adapters/coordinates.d.ts.map +1 -0
  141. package/lib/typescript/src/adapters/scroll.d.ts +49 -0
  142. package/lib/typescript/src/adapters/scroll.d.ts.map +1 -0
  143. package/lib/typescript/src/adapters/virtualizedList.d.ts +22 -0
  144. package/lib/typescript/src/adapters/virtualizedList.d.ts.map +1 -0
  145. package/lib/typescript/src/components/DndProvider.d.ts +4 -0
  146. package/lib/typescript/src/components/DndProvider.d.ts.map +1 -0
  147. package/lib/typescript/src/components/DragHandle.d.ts +4 -0
  148. package/lib/typescript/src/components/DragHandle.d.ts.map +1 -0
  149. package/lib/typescript/src/components/GridController.d.ts +21 -0
  150. package/lib/typescript/src/components/GridController.d.ts.map +1 -0
  151. package/lib/typescript/src/components/GridDragHandle.d.ts +8 -0
  152. package/lib/typescript/src/components/GridDragHandle.d.ts.map +1 -0
  153. package/lib/typescript/src/components/GridZoneView.d.ts +8 -0
  154. package/lib/typescript/src/components/GridZoneView.d.ts.map +1 -0
  155. package/lib/typescript/src/components/SortableGrid.d.ts +4 -0
  156. package/lib/typescript/src/components/SortableGrid.d.ts.map +1 -0
  157. package/lib/typescript/src/components/SortableList.d.ts +4 -0
  158. package/lib/typescript/src/components/SortableList.d.ts.map +1 -0
  159. package/lib/typescript/src/components/dndTypes.d.ts +94 -0
  160. package/lib/typescript/src/components/dndTypes.d.ts.map +1 -0
  161. package/lib/typescript/src/components/gridTypes.d.ts +84 -0
  162. package/lib/typescript/src/components/gridTypes.d.ts.map +1 -0
  163. package/lib/typescript/src/components/handleViewProps.d.ts +6 -0
  164. package/lib/typescript/src/components/handleViewProps.d.ts.map +1 -0
  165. package/lib/typescript/src/contracts.d.ts +145 -0
  166. package/lib/typescript/src/contracts.d.ts.map +1 -0
  167. package/lib/typescript/src/controller/approval.d.ts +66 -0
  168. package/lib/typescript/src/controller/approval.d.ts.map +1 -0
  169. package/lib/typescript/src/controller/coordinatorBase.d.ts +37 -0
  170. package/lib/typescript/src/controller/coordinatorBase.d.ts.map +1 -0
  171. package/lib/typescript/src/controller/dndSession.d.ts +152 -0
  172. package/lib/typescript/src/controller/dndSession.d.ts.map +1 -0
  173. package/lib/typescript/src/controller/dndState.d.ts +27 -0
  174. package/lib/typescript/src/controller/dndState.d.ts.map +1 -0
  175. package/lib/typescript/src/controller/gridAdapter.d.ts +44 -0
  176. package/lib/typescript/src/controller/gridAdapter.d.ts.map +1 -0
  177. package/lib/typescript/src/controller/gridState.d.ts +18 -0
  178. package/lib/typescript/src/controller/gridState.d.ts.map +1 -0
  179. package/lib/typescript/src/controller/locations.d.ts +9 -0
  180. package/lib/typescript/src/controller/locations.d.ts.map +1 -0
  181. package/lib/typescript/src/controller/previewPath.d.ts +49 -0
  182. package/lib/typescript/src/controller/previewPath.d.ts.map +1 -0
  183. package/lib/typescript/src/controller/sessionCore.d.ts +106 -0
  184. package/lib/typescript/src/controller/sessionCore.d.ts.map +1 -0
  185. package/lib/typescript/src/controller/useDndState.d.ts +8 -0
  186. package/lib/typescript/src/controller/useDndState.d.ts.map +1 -0
  187. package/lib/typescript/src/controller/useGridState.d.ts +8 -0
  188. package/lib/typescript/src/controller/useGridState.d.ts.map +1 -0
  189. package/lib/typescript/src/controller/useStoreState.d.ts +16 -0
  190. package/lib/typescript/src/controller/useStoreState.d.ts.map +1 -0
  191. package/lib/typescript/src/defaults.d.ts +5 -0
  192. package/lib/typescript/src/defaults.d.ts.map +1 -0
  193. package/lib/typescript/src/diagnostics.d.ts +9 -0
  194. package/lib/typescript/src/diagnostics.d.ts.map +1 -0
  195. package/lib/typescript/src/engine/gridPlacements.d.ts +22 -0
  196. package/lib/typescript/src/engine/gridPlacements.d.ts.map +1 -0
  197. package/lib/typescript/src/engine/index.d.ts +28 -0
  198. package/lib/typescript/src/engine/index.d.ts.map +1 -0
  199. package/lib/typescript/src/engine/layout.d.ts +4 -0
  200. package/lib/typescript/src/engine/layout.d.ts.map +1 -0
  201. package/lib/typescript/src/engine/layoutMove.d.ts +46 -0
  202. package/lib/typescript/src/engine/layoutMove.d.ts.map +1 -0
  203. package/lib/typescript/src/engine/layoutState.d.ts +22 -0
  204. package/lib/typescript/src/engine/layoutState.d.ts.map +1 -0
  205. package/lib/typescript/src/engine/list.d.ts +68 -0
  206. package/lib/typescript/src/engine/list.d.ts.map +1 -0
  207. package/lib/typescript/src/engine/listIndex.d.ts +32 -0
  208. package/lib/typescript/src/engine/listIndex.d.ts.map +1 -0
  209. package/lib/typescript/src/engine/move.d.ts +10 -0
  210. package/lib/typescript/src/engine/move.d.ts.map +1 -0
  211. package/lib/typescript/src/engine/movementPolicy.d.ts +34 -0
  212. package/lib/typescript/src/engine/movementPolicy.d.ts.map +1 -0
  213. package/lib/typescript/src/engine/orderedTarget.d.ts +32 -0
  214. package/lib/typescript/src/engine/orderedTarget.d.ts.map +1 -0
  215. package/lib/typescript/src/engine/pack.d.ts +3 -0
  216. package/lib/typescript/src/engine/pack.d.ts.map +1 -0
  217. package/lib/typescript/src/engine/rectangles.d.ts +9 -0
  218. package/lib/typescript/src/engine/rectangles.d.ts.map +1 -0
  219. package/lib/typescript/src/engine/sameLayout.d.ts +4 -0
  220. package/lib/typescript/src/engine/sameLayout.d.ts.map +1 -0
  221. package/lib/typescript/src/engine/search.d.ts +28 -0
  222. package/lib/typescript/src/engine/search.d.ts.map +1 -0
  223. package/lib/typescript/src/engine/spatial.d.ts +16 -0
  224. package/lib/typescript/src/engine/spatial.d.ts.map +1 -0
  225. package/lib/typescript/src/engine/spatialAppRotation.d.ts +4 -0
  226. package/lib/typescript/src/engine/spatialAppRotation.d.ts.map +1 -0
  227. package/lib/typescript/src/engine/spatialExchange.d.ts +5 -0
  228. package/lib/typescript/src/engine/spatialExchange.d.ts.map +1 -0
  229. package/lib/typescript/src/engine/spatialInsertion.d.ts +5 -0
  230. package/lib/typescript/src/engine/spatialInsertion.d.ts.map +1 -0
  231. package/lib/typescript/src/engine/spatialPolicy.d.ts +9 -0
  232. package/lib/typescript/src/engine/spatialPolicy.d.ts.map +1 -0
  233. package/lib/typescript/src/engine/spatialRotation.d.ts +8 -0
  234. package/lib/typescript/src/engine/spatialRotation.d.ts.map +1 -0
  235. package/lib/typescript/src/engine/validation.d.ts +9 -0
  236. package/lib/typescript/src/engine/validation.d.ts.map +1 -0
  237. package/lib/typescript/src/gesture/DndGestureGroup.d.ts +33 -0
  238. package/lib/typescript/src/gesture/DndGestureGroup.d.ts.map +1 -0
  239. package/lib/typescript/src/gesture/DndGestureHost.d.ts +12 -0
  240. package/lib/typescript/src/gesture/DndGestureHost.d.ts.map +1 -0
  241. package/lib/typescript/src/gesture/gestureConfig.d.ts +15 -0
  242. package/lib/typescript/src/gesture/gestureConfig.d.ts.map +1 -0
  243. package/lib/typescript/src/gesture/gestureGrouping.d.ts +66 -0
  244. package/lib/typescript/src/gesture/gestureGrouping.d.ts.map +1 -0
  245. package/lib/typescript/src/gesture/gestureHost.d.ts +23 -0
  246. package/lib/typescript/src/gesture/gestureHost.d.ts.map +1 -0
  247. package/lib/typescript/src/index.d.ts +38 -0
  248. package/lib/typescript/src/index.d.ts.map +1 -0
  249. package/lib/typescript/src/pointer/dndTarget.d.ts +14 -0
  250. package/lib/typescript/src/pointer/dndTarget.d.ts.map +1 -0
  251. package/lib/typescript/src/pointer/geometry.d.ts +28 -0
  252. package/lib/typescript/src/pointer/geometry.d.ts.map +1 -0
  253. package/lib/typescript/src/pointer/layoutTarget.d.ts +43 -0
  254. package/lib/typescript/src/pointer/layoutTarget.d.ts.map +1 -0
  255. package/lib/typescript/src/pointer/listCellMotion.d.ts +43 -0
  256. package/lib/typescript/src/pointer/listCellMotion.d.ts.map +1 -0
  257. package/lib/typescript/src/pointer/listTarget.d.ts +7 -0
  258. package/lib/typescript/src/pointer/listTarget.d.ts.map +1 -0
  259. package/lib/typescript/src/pointer/motion.d.ts +20 -0
  260. package/lib/typescript/src/pointer/motion.d.ts.map +1 -0
  261. package/lib/typescript/src/runtime/dndPresentation.d.ts +37 -0
  262. package/lib/typescript/src/runtime/dndPresentation.d.ts.map +1 -0
  263. package/lib/typescript/src/runtime/dndRenderStore.d.ts +46 -0
  264. package/lib/typescript/src/runtime/dndRenderStore.d.ts.map +1 -0
  265. package/lib/typescript/src/runtime/dndRuntime.d.ts +138 -0
  266. package/lib/typescript/src/runtime/dndRuntime.d.ts.map +1 -0
  267. package/lib/typescript/src/runtime/dndSettlement.d.ts +18 -0
  268. package/lib/typescript/src/runtime/dndSettlement.d.ts.map +1 -0
  269. package/lib/typescript/src/runtime/gridRuntime.d.ts +54 -0
  270. package/lib/typescript/src/runtime/gridRuntime.d.ts.map +1 -0
  271. package/lib/typescript/src/types.d.ts +180 -0
  272. package/lib/typescript/src/types.d.ts.map +1 -0
  273. package/package.json +136 -0
  274. package/src/adapters/coordinates.ts +181 -0
  275. package/src/adapters/scroll.ts +147 -0
  276. package/src/adapters/virtualizedList.ts +112 -0
  277. package/src/components/DndProvider.tsx +1836 -0
  278. package/src/components/DragHandle.tsx +89 -0
  279. package/src/components/GridController.tsx +213 -0
  280. package/src/components/GridDragHandle.tsx +11 -0
  281. package/src/components/GridZoneView.tsx +57 -0
  282. package/src/components/SortableGrid.tsx +449 -0
  283. package/src/components/SortableList.tsx +1028 -0
  284. package/src/components/dndTypes.ts +102 -0
  285. package/src/components/gridTypes.ts +90 -0
  286. package/src/components/handleViewProps.ts +35 -0
  287. package/src/contracts.ts +149 -0
  288. package/src/controller/approval.ts +179 -0
  289. package/src/controller/coordinatorBase.ts +118 -0
  290. package/src/controller/dndSession.ts +1058 -0
  291. package/src/controller/dndState.ts +198 -0
  292. package/src/controller/gridAdapter.ts +268 -0
  293. package/src/controller/gridState.ts +187 -0
  294. package/src/controller/locations.ts +67 -0
  295. package/src/controller/previewPath.ts +90 -0
  296. package/src/controller/sessionCore.ts +390 -0
  297. package/src/controller/useDndState.ts +26 -0
  298. package/src/controller/useGridState.ts +22 -0
  299. package/src/controller/useStoreState.ts +27 -0
  300. package/src/defaults.ts +4 -0
  301. package/src/diagnostics.ts +23 -0
  302. package/src/engine/gridPlacements.ts +85 -0
  303. package/src/engine/index.ts +99 -0
  304. package/src/engine/layout.ts +29 -0
  305. package/src/engine/layoutMove.ts +401 -0
  306. package/src/engine/layoutState.ts +490 -0
  307. package/src/engine/list.ts +307 -0
  308. package/src/engine/listIndex.ts +174 -0
  309. package/src/engine/move.ts +274 -0
  310. package/src/engine/movementPolicy.ts +151 -0
  311. package/src/engine/orderedTarget.ts +131 -0
  312. package/src/engine/pack.ts +24 -0
  313. package/src/engine/rectangles.ts +18 -0
  314. package/src/engine/sameLayout.ts +50 -0
  315. package/src/engine/search.ts +174 -0
  316. package/src/engine/spatial.ts +36 -0
  317. package/src/engine/spatialAppRotation.ts +53 -0
  318. package/src/engine/spatialExchange.ts +212 -0
  319. package/src/engine/spatialInsertion.ts +157 -0
  320. package/src/engine/spatialPolicy.ts +48 -0
  321. package/src/engine/spatialRotation.ts +56 -0
  322. package/src/engine/validation.ts +358 -0
  323. package/src/gesture/DndGestureGroup.tsx +357 -0
  324. package/src/gesture/DndGestureHost.tsx +50 -0
  325. package/src/gesture/gestureConfig.ts +123 -0
  326. package/src/gesture/gestureGrouping.ts +210 -0
  327. package/src/gesture/gestureHost.tsx +163 -0
  328. package/src/index.ts +162 -0
  329. package/src/pointer/dndTarget.ts +213 -0
  330. package/src/pointer/geometry.ts +584 -0
  331. package/src/pointer/layoutTarget.ts +515 -0
  332. package/src/pointer/listCellMotion.ts +174 -0
  333. package/src/pointer/listTarget.ts +56 -0
  334. package/src/pointer/motion.ts +66 -0
  335. package/src/runtime/dndPresentation.ts +113 -0
  336. package/src/runtime/dndRenderStore.ts +246 -0
  337. package/src/runtime/dndRuntime.ts +311 -0
  338. package/src/runtime/dndSettlement.ts +117 -0
  339. package/src/runtime/gridRuntime.ts +73 -0
  340. package/src/types.ts +189 -0
@@ -0,0 +1,1836 @@
1
+ import {
2
+ useCallback,
3
+ useImperativeHandle,
4
+ useLayoutEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ useSyncExternalStore,
9
+ } from 'react';
10
+ import { PixelRatio, StyleSheet } from 'react-native';
11
+ import Animated, {
12
+ cancelAnimation,
13
+ type ReduceMotion,
14
+ type FrameInfo,
15
+ scrollTo,
16
+ useAnimatedRef,
17
+ useAnimatedStyle,
18
+ useDerivedValue,
19
+ useFrameCallback,
20
+ useSharedValue,
21
+ withTiming,
22
+ } from 'react-native-reanimated';
23
+ import { runOnUISync, scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
24
+
25
+ import { computeAutoScroll } from '../adapters/scroll';
26
+ import type {
27
+ DndItem,
28
+ LayoutLocation,
29
+ LayoutRect,
30
+ LayoutValidationIssue,
31
+ } from '../contracts';
32
+ import { createListTargetIndex } from '../engine/listIndex';
33
+ import type { DndProposal } from '../controller/dndState';
34
+ import {
35
+ type DndDragEndEvent,
36
+ DndSessionCoordinator,
37
+ } from '../controller/dndSession';
38
+ import { sameGestureRelations } from '../gesture/gestureConfig';
39
+ import {
40
+ DEFAULT_ACTIVATION_DELAY_MS,
41
+ DEFAULT_SEARCH_BUDGET,
42
+ } from '../defaults';
43
+ import {
44
+ resolveGridMovementPolicy,
45
+ validateGridMovementPolicy,
46
+ } from '../engine/movementPolicy';
47
+ import { isPositiveInteger } from '../engine/validation';
48
+ import { DEFAULT_GRID_MOTION, resolveGridMotion } from '../pointer/motion';
49
+ import {
50
+ DndItemContext,
51
+ DndRuntimeContext,
52
+ EMPTY_DND_MOTION,
53
+ measureDndRect,
54
+ pointInRect,
55
+ readListViewports,
56
+ type DndMotion,
57
+ type DndHandleRegistration,
58
+ type DndRuntime,
59
+ type ZoneRegistration,
60
+ type UiZoneRegistration,
61
+ } from '../runtime/dndRuntime';
62
+ import {
63
+ resolveLayoutTarget,
64
+ isLayoutTargetVisible,
65
+ prepareGridTarget,
66
+ } from '../pointer/layoutTarget';
67
+ import { itemRect } from '../pointer/geometry';
68
+ import {
69
+ createZonePresentation,
70
+ EMPTY_DND_PRESENTATION,
71
+ presentationRect,
72
+ type DndPresentation,
73
+ type DndZonePresentation,
74
+ } from '../runtime/dndPresentation';
75
+ import {
76
+ sameLayoutTarget,
77
+ uiLayoutTarget,
78
+ type DndTargetSession,
79
+ } from '../pointer/dndTarget';
80
+ import {
81
+ interpolateSettlementRect,
82
+ settlementDestination,
83
+ settlementTarget,
84
+ } from '../runtime/dndSettlement';
85
+ import type { DndProviderProps, ListItemRenderArgs } from './dndTypes';
86
+ import type { CancelReason } from '../types';
87
+ import type { DndGestureRuntime } from '../gesture/DndGestureGroup';
88
+ import {
89
+ createDndGestureRegistry,
90
+ DndGestureHost,
91
+ } from '../gesture/DndGestureHost';
92
+ import { DndRenderStore } from '../runtime/dndRenderStore';
93
+
94
+ type RegisteredList = ZoneRegistration & {
95
+ layoutVersion: number;
96
+ identity: object;
97
+ geometryKey: string | null;
98
+ };
99
+ type RegisteredHandle = {
100
+ id: number;
101
+ identity: object;
102
+ registration: DndHandleRegistration;
103
+ mounted: boolean;
104
+ };
105
+ type Settlement = {
106
+ token: number;
107
+ itemId: string;
108
+ revision: number;
109
+ from: LayoutRect;
110
+ destination: LayoutRect | null;
111
+ zone: UiZoneRegistration | null;
112
+ contentRect: LayoutRect | null;
113
+ itemRef: DndHandleRegistration['sourceRef'] | null;
114
+ startedAt: number | null;
115
+ animationStartedAt: number | null;
116
+ animating: boolean;
117
+ done: boolean;
118
+ committed: boolean;
119
+ };
120
+ type RetainedPreview<T> = {
121
+ token: number;
122
+ item: DndItem<T>;
123
+ sessionId: string;
124
+ sourceZoneId: string;
125
+ render(args: ListItemRenderArgs<unknown>): React.ReactNode;
126
+ index: number;
127
+ width: number;
128
+ height: number;
129
+ };
130
+
131
+ function sameHandle(
132
+ first: DndHandleRegistration,
133
+ second: DndHandleRegistration,
134
+ ): boolean {
135
+ return (
136
+ first.itemId === second.itemId &&
137
+ first.zoneId === second.zoneId &&
138
+ first.ref === second.ref &&
139
+ first.sourceRef === second.sourceRef &&
140
+ first.disabled === second.disabled &&
141
+ sameGestureRelations(first.gestureRelations, second.gestureRelations)
142
+ );
143
+ }
144
+
145
+ function geometryResized(previous: DndMotion | null, next: DndMotion): boolean {
146
+ 'worklet';
147
+ return (
148
+ !!previous &&
149
+ (previous.root.width !== next.root.width ||
150
+ previous.root.height !== next.root.height ||
151
+ next.zones.some(measured => {
152
+ const old = previous.zones.find(
153
+ entry => entry.zoneId === measured.zoneId,
154
+ );
155
+ return (
156
+ old &&
157
+ (old.viewport.rect.width !== measured.viewport.rect.width ||
158
+ old.viewport.rect.height !== measured.viewport.rect.height)
159
+ );
160
+ }))
161
+ );
162
+ }
163
+
164
+ function targetEnvironmentChanged(previous: DndMotion | null, next: DndMotion) {
165
+ 'worklet';
166
+ return (
167
+ !previous ||
168
+ previous.root.x !== next.root.x ||
169
+ previous.root.y !== next.root.y ||
170
+ previous.root.width !== next.root.width ||
171
+ previous.root.height !== next.root.height ||
172
+ previous.zones.length !== next.zones.length ||
173
+ next.zones.some((zone, index) => {
174
+ const old = previous.zones[index];
175
+ return (
176
+ !old ||
177
+ old.zoneId !== zone.zoneId ||
178
+ old.offset !== zone.offset ||
179
+ old.layoutVersion !== zone.layoutVersion ||
180
+ old.viewport.revision !== zone.viewport.revision ||
181
+ old.viewport.rect.x !== zone.viewport.rect.x ||
182
+ old.viewport.rect.y !== zone.viewport.rect.y ||
183
+ old.viewport.rect.width !== zone.viewport.rect.width ||
184
+ old.viewport.rect.height !== zone.viewport.rect.height
185
+ );
186
+ })
187
+ );
188
+ }
189
+
190
+ function sameUiZone(a: UiZoneRegistration, b: UiZoneRegistration) {
191
+ return (
192
+ a.kind === b.kind &&
193
+ a.zoneId === b.zoneId &&
194
+ a.revision === b.revision &&
195
+ a.layoutVersion === b.layoutVersion &&
196
+ a.ref === b.ref &&
197
+ ((a.kind === 'grid' &&
198
+ b.kind === 'grid' &&
199
+ a.targetGrid === b.targetGrid) ||
200
+ (b.kind === 'list' &&
201
+ a.kind === 'list' &&
202
+ a.targetIndex === b.targetIndex &&
203
+ a.orientation === b.orientation &&
204
+ a.offset === b.offset &&
205
+ a.contentSize === b.contentSize &&
206
+ a.autoScroll?.enabled === b.autoScroll?.enabled &&
207
+ a.autoScroll?.edgeThreshold === b.autoScroll?.edgeThreshold &&
208
+ a.autoScroll?.maxSpeed === b.autoScroll?.maxSpeed))
209
+ );
210
+ }
211
+
212
+ // Worklets rewrites shared objects into accessors in development. Never share
213
+ // the coordinator's JS snapshot objects themselves with the UI runtime.
214
+ function copyLayoutTarget(target: LayoutLocation): LayoutLocation {
215
+ return target.kind === 'grid'
216
+ ? { ...target, position: { ...target.position } }
217
+ : { ...target };
218
+ }
219
+
220
+ /** Shared gesture, session and overlay for grid, ScrollView and FlatList zones. */
221
+ export function DndProvider<T>({
222
+ value,
223
+ onChange,
224
+ canDrop,
225
+ onDragStart,
226
+ onDragEnd,
227
+ onDragSettled,
228
+ onValidationError,
229
+ onDiagnostic,
230
+ responseTimeoutMs,
231
+ getGridItemLayout,
232
+ movementPolicy,
233
+ searchBudget,
234
+ disabled = false,
235
+ activation = { delayMs: DEFAULT_ACTIVATION_DELAY_MS },
236
+ dragAxis = 'both',
237
+ motion: motionConfig,
238
+ renderDragPreview,
239
+ waitForDragPreviewReady = false,
240
+ dragPreviewStyle,
241
+ ref,
242
+ children,
243
+ style,
244
+ onLayout,
245
+ ...viewProps
246
+ }: DndProviderProps<T>) {
247
+ const pixelRatio = PixelRatio.get();
248
+ const motionOptions = resolveGridMotion(motionConfig);
249
+ const durationMs = motionOptions.valid
250
+ ? motionOptions.motion.durationMs
251
+ : DEFAULT_GRID_MOTION.durationMs;
252
+ const reduceMotion = motionOptions.valid
253
+ ? motionOptions.motion.reduceMotion
254
+ : DEFAULT_GRID_MOTION.reduceMotion;
255
+ const animation = useMemo(
256
+ () => ({ durationMs, reduceMotion }),
257
+ [durationMs, reduceMotion],
258
+ );
259
+ const configValid =
260
+ Number.isSafeInteger(activation?.delayMs) &&
261
+ activation.delayMs >= 0 &&
262
+ ['both', 'x', 'y'].includes(dragAxis) &&
263
+ (canDrop === undefined || typeof canDrop === 'function') &&
264
+ motionOptions.valid;
265
+ const currentCanDrop = useRef(canDrop);
266
+ const policyRevision = useRef(0);
267
+ const committedValue = useRef(value);
268
+ const currentVisibility = useRef<(proposal: DndProposal<T>) => boolean>(
269
+ () => true,
270
+ );
271
+ const checkDrop = useCallback(
272
+ (proposal: DndProposal<T>) =>
273
+ currentVisibility.current(proposal)
274
+ ? currentCanDrop.current
275
+ ? currentCanDrop.current(proposal)
276
+ : { allowed: true as const }
277
+ : { allowed: false as const, reason: 'clipped-target' },
278
+ [],
279
+ );
280
+ const [registryInvalid, setRegistryInvalid] = useState(false);
281
+ const preview = useRef<RetainedPreview<T> | null>(null);
282
+ // `onDragEnd` is the coordinator's. `onDragSettled` repeats the event once
283
+ // the retained preview of that session has finished settling (finishPreview),
284
+ // or at once when no preview is retained; a new session or unmount flushes it.
285
+ const currentDragEnd = useRef(onDragEnd);
286
+ currentDragEnd.current = onDragEnd;
287
+ const currentDragSettled = useRef(onDragSettled);
288
+ currentDragSettled.current = onDragSettled;
289
+ const pendingSettled = useRef<DndDragEndEvent<T>[]>([]);
290
+ const reportSettled = useCallback((event: DndDragEndEvent<T>) => {
291
+ try {
292
+ currentDragSettled.current?.(event);
293
+ } catch (error) {
294
+ console.error('[LayoutDnD] onDragSettled callback failed', error);
295
+ }
296
+ }, []);
297
+ const flushSettled = useCallback(() => {
298
+ const queued = pendingSettled.current;
299
+ if (queued.length === 0) return;
300
+ pendingSettled.current = [];
301
+ for (const event of queued) reportSettled(event);
302
+ }, [reportSettled]);
303
+ const endSession = useCallback(
304
+ (event: DndDragEndEvent<T>) => {
305
+ const deferred = preview.current?.sessionId === event.sessionId;
306
+ if (deferred) pendingSettled.current.push(event);
307
+ try {
308
+ currentDragEnd.current?.(event);
309
+ } finally {
310
+ if (!deferred) reportSettled(event);
311
+ }
312
+ },
313
+ [reportSettled],
314
+ );
315
+ const [coordinator] = useState(
316
+ () =>
317
+ new DndSessionCoordinator({
318
+ value,
319
+ onChange,
320
+ canDrop: checkDrop,
321
+ onDragStart,
322
+ onDragEnd: endSession,
323
+ onValidationError,
324
+ onDiagnostic,
325
+ responseTimeoutMs,
326
+ getGridItemLayout,
327
+ movementPolicy,
328
+ searchBudget,
329
+ disabled: disabled || !configValid,
330
+ }),
331
+ );
332
+ const [renderStore] = useState(() => new DndRenderStore(coordinator));
333
+ const snapshot = useSyncExternalStore(
334
+ renderStore.subscribeProvider,
335
+ renderStore.getProviderSnapshot,
336
+ );
337
+ const motion = useSharedValue(EMPTY_DND_MOTION);
338
+ const nativeRevision = useSharedValue(snapshot.value?.revision ?? 0);
339
+ const presentation = useSharedValue<DndPresentation>(EMPTY_DND_PRESENTATION);
340
+ const appliedPresentationGeneration = useSharedValue(0);
341
+ const presentationGeneration = useRef(0);
342
+ const settlement = useSharedValue<Settlement | null>(null);
343
+ const settleProgress = useSharedValue(0);
344
+ const pendingMove = useSharedValue<{ token: number; seq: number } | null>(
345
+ null,
346
+ );
347
+ const settlingToken = useRef<number | null>(null);
348
+ const settlingRevision = useRef<number | null>(null);
349
+ const refreshSettlement = useRef<() => void>(() => {});
350
+ const targetSession = useSharedValue<DndTargetSession>({
351
+ token: 0,
352
+ policyRevision: 0,
353
+ gridItems: {},
354
+ });
355
+ const sentTarget = useSharedValue<{
356
+ state: DndMotion | null;
357
+ target: LayoutLocation | null;
358
+ policyRevision: number;
359
+ }>({ state: null, target: null, policyRevision: -1 });
360
+ const zones = useSharedValue<UiZoneRegistration[]>([]);
361
+ const publishedZones = useRef<UiZoneRegistration[]>([]);
362
+ const uiDisabled = useSharedValue(disabled || !configValid);
363
+ const uiEnabled = useDerivedValue(() => !uiDisabled.get());
364
+ // JS reads of a shared value block on the UI runtime whenever the UI has
365
+ // written it since the last read. Motion changes every frame; these numbers
366
+ // change only at activation, release and cleanup, so JS reads stay cached.
367
+ const uiDragToken = useDerivedValue(() => {
368
+ const state = motion.get();
369
+ return state.phase === 'dragging' ? state.token : -1;
370
+ });
371
+ const uiActiveHandle = useDerivedValue(() => {
372
+ const state = motion.get();
373
+ return state.phase === 'idle' || state.handleId === null
374
+ ? -1
375
+ : state.handleId;
376
+ });
377
+ const token = useSharedValue(0);
378
+ const rootRef = useAnimatedRef();
379
+ const handles = useRef(new Map<object, RegisteredHandle>());
380
+ const nextHandleId = useRef(0);
381
+ const [gestureRegistry] = useState(createDndGestureRegistry);
382
+ const registry = useRef(new Map<object, RegisteredList>());
383
+ const issues = useRef(new Map<string, string>());
384
+ const observer = useRef(onValidationError);
385
+ const mounted = useRef(true);
386
+ const handlePublication = useRef({
387
+ queued: false,
388
+ generation: 0,
389
+ prune: false,
390
+ }).current;
391
+ const currentConfig = useRef({ dragAxis, delayMs: activation?.delayMs });
392
+ const policyValid = validateGridMovementPolicy(movementPolicy).valid;
393
+ const nativeSettingsKey = JSON.stringify([
394
+ searchBudget === undefined
395
+ ? DEFAULT_SEARCH_BUDGET
396
+ : isPositiveInteger(searchBudget)
397
+ ? searchBudget
398
+ : null,
399
+ responseTimeoutMs === undefined
400
+ ? 2000
401
+ : isPositiveInteger(responseTimeoutMs)
402
+ ? responseTimeoutMs
403
+ : null,
404
+ policyValid ? resolveGridMovementPolicy(movementPolicy) : null,
405
+ ]);
406
+ const committedNativeSettings = useRef({
407
+ key: nativeSettingsKey,
408
+ mapper: getGridItemLayout,
409
+ revision: 0,
410
+ });
411
+ const gestureConfigRevision =
412
+ committedNativeSettings.current.key === nativeSettingsKey &&
413
+ committedNativeSettings.current.mapper === getGridItemLayout
414
+ ? committedNativeSettings.current.revision
415
+ : committedNativeSettings.current.revision + 1;
416
+ const bridge = useRef<{
417
+ token: number;
418
+ sessionId: string | null;
419
+ state: DndMotion | null;
420
+ policyRevision: number;
421
+ }>({ token: 0, sessionId: null, state: null, policyRevision: 0 });
422
+ const [retainedPreview, setRetainedPreview] =
423
+ useState<RetainedPreview<T> | null>(null);
424
+ // Readiness belongs to the session, not to pointer requests or target
425
+ // validity. The default preview needs no application acknowledgement.
426
+ const [readyPreviewSessionId, setReadyPreviewSessionId] = useState<
427
+ string | null
428
+ >(null);
429
+ const previewReady =
430
+ !waitForDragPreviewReady ||
431
+ !renderDragPreview ||
432
+ readyPreviewSessionId === snapshot.sessionId;
433
+ const handlePreviewReady = useCallback(() => {
434
+ const current = coordinator.getSnapshot();
435
+ if (!mounted.current || current.sessionId === null) return;
436
+ setReadyPreviewSessionId(current.sessionId);
437
+ }, [coordinator]);
438
+ useImperativeHandle(
439
+ ref,
440
+ () => ({
441
+ invalidateGeometry: () => coordinator.cancel('geometry-changed'),
442
+ }),
443
+ [coordinator],
444
+ );
445
+ const rootLayout = useRef<{ width: number; height: number } | null>(null);
446
+ const presentationCache = useRef(
447
+ new Map<
448
+ string,
449
+ {
450
+ zone: object;
451
+ layout: object;
452
+ geometryKey: string | null;
453
+ prepare: import('../runtime/dndRuntime').ListRegistration['prepareDisplay'];
454
+ result: DndZonePresentation;
455
+ }
456
+ >(),
457
+ );
458
+ const lastPresentation = useRef<DndPresentation>(EMPTY_DND_PRESENTATION);
459
+ const presentationSnapshot = useCallback(() => lastPresentation.current, []);
460
+ const prepareReleaseRef =
461
+ useRef<DndGestureRuntime['prepareRelease']>(undefined);
462
+ const publishPresentation = useCallback(() => {
463
+ const current = coordinator.getSnapshot();
464
+ const display = current.displayValue;
465
+ const next: DndPresentation = {
466
+ token: bridge.current.token,
467
+ revision: current.value?.revision ?? 0,
468
+ zones: {},
469
+ ...((current.phase === 'dragging' ||
470
+ current.phase === 'awaiting-response') &&
471
+ current.validity === 'valid' &&
472
+ current.target
473
+ ? {
474
+ releaseTarget: {
475
+ target: copyLayoutTarget(current.target),
476
+ policyRevision: policyRevision.current,
477
+ ...(current.phase === 'awaiting-response'
478
+ ? { releasedSeq: current.seq }
479
+ : {}),
480
+ },
481
+ }
482
+ : {}),
483
+ };
484
+ // Idle cells already own committed geometry. Sending every rect again for
485
+ // each newly measured virtual cell copies the entire list onto UI.
486
+ if (display && (current.phase !== 'idle' || preview.current !== null))
487
+ for (const registration of registry.current.values()) {
488
+ if (
489
+ registration.revision !== current.value?.revision ||
490
+ (registration.kind === 'list' &&
491
+ registration.dataIdentity &&
492
+ registration.dataIdentity !== current.value?.items)
493
+ )
494
+ continue;
495
+ const zone = display.zones.find(
496
+ entry => entry.id === registration.zoneId,
497
+ );
498
+ if (!zone) continue;
499
+ const prepare =
500
+ registration.kind === 'list'
501
+ ? registration.prepareDisplay
502
+ : undefined;
503
+ const cached = presentationCache.current.get(zone.id);
504
+ let result: DndZonePresentation | null = null;
505
+ if (
506
+ cached?.zone === zone &&
507
+ cached.layout === registration.layout &&
508
+ cached.geometryKey === registration.geometryKey &&
509
+ cached.prepare === prepare
510
+ ) {
511
+ result = cached.result;
512
+ } else if (registration.kind === 'list' && zone.kind === 'list') {
513
+ result =
514
+ registration.prepareDisplay?.(zone, display.revision) ?? null;
515
+ } else if (registration.kind === 'grid' && zone.kind === 'grid') {
516
+ result = createZonePresentation(
517
+ zone.placements.map(
518
+ placement =>
519
+ [
520
+ placement.itemId,
521
+ itemRect(
522
+ placement.position,
523
+ placement.span,
524
+ registration.geometry,
525
+ registration.cells,
526
+ ),
527
+ ] as const,
528
+ ),
529
+ cached?.result,
530
+ );
531
+ }
532
+ if (result) {
533
+ next.zones[`#${zone.id}`] = result;
534
+ presentationCache.current.set(zone.id, {
535
+ zone,
536
+ layout: registration.layout,
537
+ geometryKey: registration.geometryKey,
538
+ prepare,
539
+ result,
540
+ });
541
+ }
542
+ }
543
+ const previous = lastPresentation.current;
544
+ for (const zoneId of presentationCache.current.keys()) {
545
+ if (!next.zones[`#${zoneId}`]) presentationCache.current.delete(zoneId);
546
+ }
547
+ if (
548
+ previous.token === next.token &&
549
+ previous.revision === next.revision &&
550
+ previous.releaseTarget?.policyRevision ===
551
+ next.releaseTarget?.policyRevision &&
552
+ previous.releaseTarget?.releasedSeq === next.releaseTarget?.releasedSeq &&
553
+ sameLayoutTarget(
554
+ previous.releaseTarget?.target ?? null,
555
+ next.releaseTarget?.target ?? null,
556
+ ) &&
557
+ Object.keys(previous.zones).length === Object.keys(next.zones).length &&
558
+ Object.keys(next.zones).every(
559
+ key => previous.zones[key] === next.zones[key],
560
+ )
561
+ )
562
+ return;
563
+ lastPresentation.current = next;
564
+ const generation = ++presentationGeneration.current;
565
+ const animateRelease =
566
+ current.phase === 'awaiting-response'
567
+ ? prepareReleaseRef.current
568
+ : undefined;
569
+ const applyPresentation = () => {
570
+ 'worklet';
571
+ const state = motion.get();
572
+ if (
573
+ state.token === next.token &&
574
+ generation >= appliedPresentationGeneration.get()
575
+ ) {
576
+ appliedPresentationGeneration.set(generation);
577
+ presentation.set(next);
578
+ if (state.phase === 'released') animateRelease?.(state);
579
+ }
580
+ };
581
+ // A final validated target must reach UI before React's synchronous commit
582
+ // work delays the normal worklet microtask batch. This only shares geometry;
583
+ // animation and native measurement still run on subsequent UI frames.
584
+ if (animateRelease) runOnUISync(applyPresentation);
585
+ else scheduleOnUI(applyPresentation);
586
+ }, [coordinator, motion, presentation, appliedPresentationGeneration]);
587
+ useLayoutEffect(
588
+ () => coordinator.subscribe(publishPresentation),
589
+ [coordinator, publishPresentation],
590
+ );
591
+
592
+ const publishHandles = useCallback(() => {
593
+ if (!mounted.current || handlePublication.queued) return;
594
+ handlePublication.queued = true;
595
+ const generation = handlePublication.generation;
596
+ (
597
+ globalThis as typeof globalThis & {
598
+ queueMicrotask(callback: () => void): void;
599
+ }
600
+ ).queueMicrotask(() => {
601
+ if (!mounted.current || handlePublication.generation !== generation)
602
+ return;
603
+ handlePublication.queued = false;
604
+ if (handlePublication.prune) {
605
+ handlePublication.prune = false;
606
+ // UI activation precedes its queued JS begin. Read the native owner
607
+ // once for the whole commit, keeping that recognizer through eviction.
608
+ let activeHandle: number | undefined;
609
+ for (const [identity, owner] of handles.current) {
610
+ if (owner.mounted) continue;
611
+ activeHandle ??= uiActiveHandle.get();
612
+ if (activeHandle !== owner.id) handles.current.delete(identity);
613
+ }
614
+ }
615
+ gestureRegistry.setOwners(Array.from(handles.current.values()));
616
+ });
617
+ }, [gestureRegistry, handlePublication, uiActiveHandle]);
618
+
619
+ const registerHandle = useCallback(
620
+ (registration: DndHandleRegistration, identity: object) => {
621
+ const previous = handles.current.get(identity);
622
+ const same =
623
+ !!previous && sameHandle(previous.registration, registration);
624
+ if (previous?.mounted && same) return;
625
+ handles.current.set(identity, {
626
+ id: previous?.id ?? ++nextHandleId.current,
627
+ identity,
628
+ // A cleanup/setup pair in one commit can reuse the same native owner.
629
+ registration: same ? previous!.registration : registration,
630
+ mounted: true,
631
+ });
632
+ publishHandles();
633
+ refreshSettlement.current();
634
+ },
635
+ [publishHandles],
636
+ );
637
+ const unregisterHandle = useCallback(
638
+ (identity: object) => {
639
+ const previous = handles.current.get(identity);
640
+ if (!previous || !previous.mounted) return;
641
+ handles.current.set(identity, { ...previous, mounted: false });
642
+ handlePublication.prune = true;
643
+ publishHandles();
644
+ },
645
+ [handlePublication, publishHandles],
646
+ );
647
+
648
+ const reportIssues = useCallback(
649
+ (owner: string, next: LayoutValidationIssue[]) => {
650
+ const key = JSON.stringify(next);
651
+ const old = issues.current.get(owner);
652
+ if (next.length) issues.current.set(owner, key);
653
+ else issues.current.delete(owner);
654
+ if (!mounted.current) return;
655
+ setRegistryInvalid(issues.current.size > 0);
656
+ if (next.length && old !== key) {
657
+ coordinator.cancel('disabled');
658
+ try {
659
+ observer.current?.(next);
660
+ } catch (error) {
661
+ console.error('[LayoutDnD] Validation callback failed', error);
662
+ }
663
+ }
664
+ },
665
+ [coordinator],
666
+ );
667
+ useLayoutEffect(() => {
668
+ mounted.current = true;
669
+ if (currentCanDrop.current !== canDrop || committedValue.current !== value)
670
+ policyRevision.current++;
671
+ committedValue.current = value;
672
+ currentCanDrop.current = canDrop;
673
+ const nextPolicyRevision = policyRevision.current;
674
+ scheduleOnUI(() => {
675
+ const current = targetSession.get();
676
+ if (current.policyRevision !== nextPolicyRevision)
677
+ targetSession.set({ ...current, policyRevision: nextPolicyRevision });
678
+ });
679
+ observer.current = onValidationError;
680
+ if (
681
+ currentConfig.current.dragAxis !== dragAxis ||
682
+ currentConfig.current.delayMs !== activation?.delayMs
683
+ )
684
+ coordinator.cancel('input-changed');
685
+ currentConfig.current = { dragAxis, delayMs: activation?.delayMs };
686
+ committedNativeSettings.current = {
687
+ key: nativeSettingsKey,
688
+ mapper: getGridItemLayout,
689
+ revision: gestureConfigRevision,
690
+ };
691
+ coordinator.commit({
692
+ value,
693
+ onChange,
694
+ canDrop: checkDrop,
695
+ onDragStart,
696
+ onDragEnd: endSession,
697
+ onValidationError,
698
+ onDiagnostic,
699
+ responseTimeoutMs,
700
+ getGridItemLayout,
701
+ movementPolicy,
702
+ searchBudget,
703
+ disabled:
704
+ disabled || !configValid || registryInvalid || issues.current.size > 0,
705
+ });
706
+ nativeRevision.set(coordinator.getSnapshot().value?.revision ?? 0);
707
+ uiDisabled.set(coordinator.getSnapshot().disabled);
708
+ reportIssues(
709
+ 'provider-config',
710
+ configValid
711
+ ? []
712
+ : [
713
+ {
714
+ code: 'invalid-configuration',
715
+ message:
716
+ 'Drag axis, activation delay or motion configuration is invalid.',
717
+ },
718
+ ],
719
+ );
720
+ });
721
+ const registeredByZone = useCallback(
722
+ () =>
723
+ new Map(
724
+ Array.from(registry.current.values()).map(entry => [
725
+ entry.zoneId,
726
+ entry,
727
+ ]),
728
+ ),
729
+ [],
730
+ );
731
+ const publishZones = useCallback(() => {
732
+ const needsTargets =
733
+ coordinator.getSnapshot().phase !== 'idle' || preview.current !== null;
734
+ const entries = Array.from(registry.current.values());
735
+ const unique = entries.filter(
736
+ (entry, index) =>
737
+ entries.findIndex(other => other.zoneId === entry.zoneId) === index,
738
+ );
739
+ const next: UiZoneRegistration[] = unique.map(entry =>
740
+ entry.kind === 'list'
741
+ ? {
742
+ kind: 'list' as const,
743
+ orientation: entry.orientation,
744
+ zoneId: entry.zoneId,
745
+ revision: entry.revision,
746
+ layoutVersion: entry.layoutVersion,
747
+ ref: entry.ref,
748
+ offset: entry.offset,
749
+ contentSize: entry.contentSize,
750
+ autoScroll: entry.autoScroll,
751
+ targetIndex: needsTargets ? entry.targetIndex : undefined,
752
+ }
753
+ : {
754
+ kind: 'grid' as const,
755
+ zoneId: entry.zoneId,
756
+ revision: entry.revision,
757
+ layoutVersion: entry.layoutVersion,
758
+ ref: entry.ref,
759
+ targetGrid: needsTargets ? entry.targetGrid : undefined,
760
+ },
761
+ );
762
+ if (
763
+ next.length !== publishedZones.current.length ||
764
+ next.some(
765
+ (entry, index) => !sameUiZone(entry, publishedZones.current[index]),
766
+ )
767
+ ) {
768
+ publishedZones.current = next;
769
+ zones.set(next);
770
+ }
771
+ reportIssues(
772
+ 'duplicate-zones',
773
+ entries.length === unique.length
774
+ ? []
775
+ : [
776
+ {
777
+ code: 'duplicate-id',
778
+ message:
779
+ 'Each zone can have only one mounted sortable component.',
780
+ },
781
+ ],
782
+ );
783
+ }, [coordinator, zones, reportIssues]);
784
+ const registerZone = useCallback(
785
+ (entry: ZoneRegistration, identity: object) => {
786
+ const previous = registry.current.get(identity);
787
+ const geometryKey =
788
+ entry.kind === 'grid'
789
+ ? JSON.stringify([entry.geometry, entry.cells, entry.pixelRatio])
790
+ : null;
791
+ if (previous && previous.geometryKey !== geometryKey) {
792
+ const active = coordinator.getSnapshot();
793
+ if (
794
+ active.sourceZoneId === entry.zoneId ||
795
+ active.targetZoneId === entry.zoneId
796
+ )
797
+ coordinator.cancel('geometry-changed');
798
+ }
799
+ const layoutVersion =
800
+ (previous?.layoutVersion ?? 0) +
801
+ Number(
802
+ previous?.layout !== entry.layout ||
803
+ previous?.geometryKey !== geometryKey,
804
+ );
805
+ registry.current.set(identity, {
806
+ ...entry,
807
+ identity,
808
+ geometryKey,
809
+ layoutVersion,
810
+ ...(entry.kind === 'list'
811
+ ? {
812
+ targetIndex:
813
+ previous?.kind === 'list' && previous.layout === entry.layout
814
+ ? previous.targetIndex
815
+ : createListTargetIndex(
816
+ entry.layout,
817
+ previous?.kind === 'list'
818
+ ? previous.targetIndex
819
+ : undefined,
820
+ ),
821
+ }
822
+ : {
823
+ targetGrid: prepareGridTarget({ ...entry, layoutVersion }),
824
+ }),
825
+ });
826
+ publishZones();
827
+ publishPresentation();
828
+ refreshSettlement.current();
829
+ },
830
+ [coordinator, publishZones, publishPresentation],
831
+ );
832
+ const unregisterZone = useCallback(
833
+ (zoneId: string, identity: object) => {
834
+ if (!registry.current.delete(identity)) return;
835
+ publishZones();
836
+ const current = coordinator.getSnapshot();
837
+ if (current.sourceZoneId === zoneId || current.targetZoneId === zoneId)
838
+ coordinator.cancel('zone-removed');
839
+ },
840
+ [coordinator, publishZones],
841
+ );
842
+ const cancel = useCallback(
843
+ (reason: CancelReason) => coordinator.cancel(reason),
844
+ [coordinator],
845
+ );
846
+ const targetFor = useCallback(
847
+ (state: DndMotion) => {
848
+ const current = coordinator.getSnapshot();
849
+ return resolveLayoutTarget(
850
+ state,
851
+ registeredByZone(),
852
+ current.gridItemLayouts,
853
+ current.movementPolicy,
854
+ current.target,
855
+ );
856
+ },
857
+ [coordinator, registeredByZone],
858
+ );
859
+ currentVisibility.current = proposal => {
860
+ const state = bridge.current.state;
861
+ return (
862
+ !!state &&
863
+ isLayoutTargetVisible(
864
+ state,
865
+ registeredByZone(),
866
+ proposal.to,
867
+ coordinator.getSnapshot().gridItemLayouts,
868
+ )
869
+ );
870
+ };
871
+ const begin = useCallback(
872
+ (state: DndMotion) => {
873
+ if (!mounted.current) return;
874
+ const latest = motion.get();
875
+ if (latest.token !== state.token || latest.handleId !== state.handleId)
876
+ return;
877
+ if (bridge.current.sessionId && bridge.current.token === state.token)
878
+ return;
879
+ const owner =
880
+ state.handleId === null
881
+ ? undefined
882
+ : Array.from(handles.current.values()).find(
883
+ entry => entry.id === state.handleId,
884
+ );
885
+ const validOwner =
886
+ state.handleId === null ||
887
+ (!!owner &&
888
+ !owner.registration.disabled &&
889
+ owner.registration.itemId === state.itemId &&
890
+ owner.registration.zoneId === state.sourceZoneId);
891
+ const source = registeredByZone().get(state.sourceZoneId ?? '');
892
+ const measurement = state.zones.find(
893
+ entry => entry.zoneId === state.sourceZoneId,
894
+ );
895
+ flushSettled();
896
+ const id =
897
+ validOwner &&
898
+ state.axis === currentConfig.current.dragAxis &&
899
+ state.activationDelayMs === currentConfig.current.delayMs &&
900
+ state.configRevision === committedNativeSettings.current.revision &&
901
+ source &&
902
+ measurement?.layoutVersion === source.layoutVersion &&
903
+ measurement.viewport.revision === state.revision &&
904
+ state.itemId
905
+ ? coordinator.start(state.itemId, state.revision)
906
+ : null;
907
+ if (!id) {
908
+ if (owner && !owner.mounted) {
909
+ handles.current.delete(owner.identity);
910
+ publishHandles();
911
+ }
912
+ const previousToken = bridge.current.token;
913
+ const idlePresentation = lastPresentation.current;
914
+ const idleGeneration = presentationGeneration.current;
915
+ scheduleOnUI(() => {
916
+ if (motion.get().token === state.token) {
917
+ motion.set({ ...EMPTY_DND_MOTION, token: previousToken });
918
+ if (idlePresentation.token === previousToken) {
919
+ appliedPresentationGeneration.set(idleGeneration);
920
+ presentation.set(idlePresentation);
921
+ }
922
+ }
923
+ });
924
+ return;
925
+ }
926
+ bridge.current = {
927
+ token: state.token,
928
+ sessionId: id,
929
+ state,
930
+ policyRevision: policyRevision.current,
931
+ };
932
+ preview.current = {
933
+ token: state.token,
934
+ item: coordinator
935
+ .getSnapshot()
936
+ .value!.items.find(item => item.id === state.itemId)!,
937
+ sessionId: id,
938
+ sourceZoneId: state.sourceZoneId!,
939
+ render: source!.renderItem,
940
+ index:
941
+ source!.kind === 'list'
942
+ ? (source!.layout.entries.find(
943
+ entry => entry.itemId === state.itemId,
944
+ )?.index ?? 0)
945
+ : source!.layout.placements.findIndex(
946
+ entry => entry.itemId === state.itemId,
947
+ ),
948
+ width: state.width,
949
+ height: state.height,
950
+ };
951
+ setRetainedPreview(preview.current);
952
+ publishZones();
953
+ const initial = targetFor(state);
954
+ // A zone measured before its registered layout reached the UI has no
955
+ // position to offer yet; the first pointer packet supplies it.
956
+ if (initial.target !== null || !initial.stale)
957
+ coordinator.requestTarget(id, state.seq, initial.target);
958
+ const current = coordinator.getSnapshot();
959
+ const gridItems: DndTargetSession['gridItems'] = {};
960
+ for (const [zoneId, recipe] of current.gridItemLayouts)
961
+ gridItems[`#${zoneId}`] = recipe;
962
+ const prepared: DndTargetSession = {
963
+ token: state.token,
964
+ policyRevision: policyRevision.current,
965
+ gridItems,
966
+ movementPolicy: current.movementPolicy,
967
+ };
968
+ const target = current.target ? copyLayoutTarget(current.target) : null;
969
+ scheduleOnUI(() => {
970
+ if (motion.get().token !== state.token) return;
971
+ targetSession.set(prepared);
972
+ sentTarget.set({
973
+ state,
974
+ target,
975
+ policyRevision: prepared.policyRevision,
976
+ });
977
+ });
978
+ publishPresentation();
979
+ },
980
+ [
981
+ coordinator,
982
+ motion,
983
+ presentation,
984
+ appliedPresentationGeneration,
985
+ publishHandles,
986
+ registeredByZone,
987
+ targetFor,
988
+ targetSession,
989
+ sentTarget,
990
+ publishPresentation,
991
+ publishZones,
992
+ flushSettled,
993
+ ],
994
+ );
995
+ const move = useCallback(
996
+ (state: DndMotion) => {
997
+ try {
998
+ const active = bridge.current;
999
+ if (
1000
+ !mounted.current ||
1001
+ active.token !== state.token ||
1002
+ !active.sessionId
1003
+ )
1004
+ return;
1005
+ // A queued move can reach JS after the finger has already lifted.
1006
+ // Release validates its own final geometry; rendering this old preview
1007
+ // first only delays that validation behind another React commit.
1008
+ if (uiDragToken.get() !== state.token) return;
1009
+ if (
1010
+ !Number.isSafeInteger(state.seq) ||
1011
+ (active.state && state.seq <= active.state.seq)
1012
+ )
1013
+ return;
1014
+ if (geometryResized(active.state, state)) {
1015
+ coordinator.cancel('geometry-changed');
1016
+ return;
1017
+ }
1018
+ const resolved = targetFor(state);
1019
+ // The UI measured this zone before the layout version registered by
1020
+ // the last candidate reached it. Publishing an outside target here
1021
+ // would revert and re-apply the preview once per target change; keep
1022
+ // the current target until a packet with the new version arrives.
1023
+ if (resolved.target === null && resolved.stale) return;
1024
+ const refresh =
1025
+ targetEnvironmentChanged(active.state, state) ||
1026
+ active.policyRevision !== policyRevision.current;
1027
+ active.state = state;
1028
+ active.policyRevision = policyRevision.current;
1029
+ coordinator.requestTarget(
1030
+ active.sessionId,
1031
+ state.seq,
1032
+ resolved.target,
1033
+ {
1034
+ refresh,
1035
+ },
1036
+ );
1037
+ } finally {
1038
+ const { token: completedToken, seq } = state;
1039
+ scheduleOnUI(() => {
1040
+ const pending = pendingMove.get();
1041
+ if (pending?.token === completedToken && pending.seq === seq)
1042
+ pendingMove.set(null);
1043
+ });
1044
+ }
1045
+ },
1046
+ [coordinator, targetFor, pendingMove, uiDragToken],
1047
+ );
1048
+ const release = useCallback(
1049
+ (state: DndMotion, cancelled: boolean | CancelReason) => {
1050
+ const active = bridge.current;
1051
+ if (!mounted.current || active.token !== state.token || !active.sessionId)
1052
+ return;
1053
+ if (cancelled) {
1054
+ active.state = state;
1055
+ coordinator.cancel(
1056
+ typeof cancelled === 'string' ? cancelled : 'gesture-interrupted',
1057
+ );
1058
+ return;
1059
+ }
1060
+ if (geometryResized(active.state, state)) {
1061
+ active.state = state;
1062
+ coordinator.cancel('geometry-changed');
1063
+ return;
1064
+ }
1065
+ let finalState = state;
1066
+ const registrations = registeredByZone();
1067
+ const measurementsChanged = state.zones.some(measured => {
1068
+ const current = registrations.get(measured.zoneId);
1069
+ return (
1070
+ current?.revision === state.revision &&
1071
+ current.layoutVersion !== measured.layoutVersion
1072
+ );
1073
+ });
1074
+ if (measurementsChanged) {
1075
+ // A native size event may commit after onDeactivate captured viewports,
1076
+ // but before its queued RN release runs. Refresh that one snapshot;
1077
+ // accepting the old layout version would bypass final target validation.
1078
+ const measureZones: UiZoneRegistration[] = Array.from(
1079
+ registrations.values(),
1080
+ entry =>
1081
+ entry.kind === 'list'
1082
+ ? {
1083
+ kind: 'list',
1084
+ zoneId: entry.zoneId,
1085
+ orientation: entry.orientation,
1086
+ revision: entry.revision,
1087
+ layoutVersion: entry.layoutVersion,
1088
+ ref: entry.ref,
1089
+ offset: entry.offset,
1090
+ contentSize: entry.contentSize,
1091
+ }
1092
+ : {
1093
+ kind: 'grid',
1094
+ zoneId: entry.zoneId,
1095
+ revision: entry.revision,
1096
+ layoutVersion: entry.layoutVersion,
1097
+ ref: entry.ref,
1098
+ },
1099
+ );
1100
+ const refreshed = runOnUISync(() => {
1101
+ 'worklet';
1102
+ const root = measureDndRect(rootRef);
1103
+ if (!root) return null;
1104
+ const next = {
1105
+ ...state,
1106
+ root,
1107
+ zones: readListViewports(measureZones),
1108
+ };
1109
+ const native = motion.get();
1110
+ if (
1111
+ native.token === state.token &&
1112
+ native.phase === 'released' &&
1113
+ native.seq === state.seq
1114
+ )
1115
+ motion.set(next);
1116
+ return next;
1117
+ });
1118
+ if (!refreshed) {
1119
+ coordinator.cancel('measure-failed');
1120
+ return;
1121
+ }
1122
+ if (
1123
+ geometryResized(state, refreshed) ||
1124
+ geometryResized(active.state, refreshed)
1125
+ ) {
1126
+ coordinator.cancel('geometry-changed');
1127
+ return;
1128
+ }
1129
+ finalState = refreshed;
1130
+ }
1131
+ active.state = finalState;
1132
+ const final = targetFor(finalState);
1133
+ // Release keeps the target the user saw when only the layout version
1134
+ // published to the UI lags behind; the engine validates the index.
1135
+ coordinator.release(
1136
+ active.sessionId,
1137
+ finalState.seq,
1138
+ final.target === null && final.stale
1139
+ ? coordinator.getSnapshot().target
1140
+ : final.target,
1141
+ );
1142
+ },
1143
+ [coordinator, targetFor, registeredByZone, motion, rootRef],
1144
+ );
1145
+
1146
+ const prepareRelease = useCallback(
1147
+ (state: DndMotion) => {
1148
+ 'worklet';
1149
+ if (
1150
+ durationMs <= 0 ||
1151
+ uiDisabled.get() ||
1152
+ settlement.get() ||
1153
+ geometryResized(motion.get(), state)
1154
+ )
1155
+ return;
1156
+ const approved = presentation.get();
1157
+ const releaseTarget = approved.releaseTarget;
1158
+ const prepared = targetSession.get();
1159
+ const registered = zones.get();
1160
+ // Continue the already validated preview immediately on UI. Final
1161
+ // policy checks and asynchronous approval still gate the handoff.
1162
+ if (
1163
+ releaseTarget &&
1164
+ state.itemId !== null &&
1165
+ approved.token === state.token &&
1166
+ approved.revision === state.revision &&
1167
+ prepared.token === state.token &&
1168
+ releaseTarget.policyRevision === prepared.policyRevision &&
1169
+ (releaseTarget.releasedSeq === state.seq ||
1170
+ sameLayoutTarget(
1171
+ releaseTarget.target,
1172
+ uiLayoutTarget(state, registered, prepared, releaseTarget.target),
1173
+ ))
1174
+ ) {
1175
+ const zone = registered.find(
1176
+ entry => entry.zoneId === releaseTarget.target.zoneId,
1177
+ );
1178
+ const contentRect = presentationRect(
1179
+ approved,
1180
+ releaseTarget.target.zoneId,
1181
+ state.itemId,
1182
+ );
1183
+ if (zone && contentRect) {
1184
+ const next: Settlement = {
1185
+ token: state.token,
1186
+ itemId: state.itemId,
1187
+ revision: state.revision,
1188
+ from: {
1189
+ x: state.pointer.x - state.grip.x - state.root.x,
1190
+ y: state.pointer.y - state.grip.y - state.root.y,
1191
+ width: state.width,
1192
+ height: state.height,
1193
+ },
1194
+ destination: null,
1195
+ zone,
1196
+ contentRect,
1197
+ itemRef: null,
1198
+ startedAt: null,
1199
+ animationStartedAt: null,
1200
+ animating: false,
1201
+ done: false,
1202
+ committed: false,
1203
+ };
1204
+ settlement.set(next);
1205
+ }
1206
+ }
1207
+ },
1208
+ [
1209
+ durationMs,
1210
+ uiDisabled,
1211
+ settlement,
1212
+ presentation,
1213
+ targetSession,
1214
+ zones,
1215
+ motion,
1216
+ ],
1217
+ );
1218
+
1219
+ useLayoutEffect(() => {
1220
+ prepareReleaseRef.current = prepareRelease;
1221
+ }, [prepareRelease]);
1222
+
1223
+ const gestureRuntime = useMemo<DndGestureRuntime>(
1224
+ () => ({
1225
+ motion,
1226
+ frameDriven: true,
1227
+ token,
1228
+ zones,
1229
+ disabled: uiDisabled,
1230
+ enabled: uiEnabled,
1231
+ rootRef,
1232
+ begin,
1233
+ move,
1234
+ release,
1235
+ prepareRelease,
1236
+ dragAxis,
1237
+ revision: nativeRevision,
1238
+ activationDelayMs: configValid ? activation.delayMs : 0,
1239
+ gestureConfigRevision,
1240
+ }),
1241
+ [
1242
+ motion,
1243
+ token,
1244
+ zones,
1245
+ uiDisabled,
1246
+ uiEnabled,
1247
+ rootRef,
1248
+ begin,
1249
+ move,
1250
+ release,
1251
+ prepareRelease,
1252
+ dragAxis,
1253
+ nativeRevision,
1254
+ configValid,
1255
+ activation?.delayMs,
1256
+ gestureConfigRevision,
1257
+ ],
1258
+ );
1259
+ const finishPreview = useCallback(
1260
+ (finishedToken: number) => {
1261
+ if (bridge.current.token !== finishedToken) return;
1262
+ const native = motion.get();
1263
+ if (native.phase !== 'idle' && native.token !== finishedToken) return;
1264
+ bridge.current.sessionId = null;
1265
+ settlingToken.current = null;
1266
+ settlingRevision.current = null;
1267
+ preview.current = null;
1268
+ setRetainedPreview(null);
1269
+ publishZones();
1270
+ publishPresentation();
1271
+ let removed = false;
1272
+ for (const [identity, owner] of handles.current) {
1273
+ if (!owner.mounted) {
1274
+ handles.current.delete(identity);
1275
+ removed = true;
1276
+ }
1277
+ }
1278
+ if (removed) publishHandles();
1279
+ flushSettled();
1280
+ },
1281
+ [motion, publishHandles, publishZones, publishPresentation, flushSettled],
1282
+ );
1283
+
1284
+ refreshSettlement.current = () => {
1285
+ const retained = preview.current;
1286
+ const current = coordinator.getSnapshot();
1287
+ if (
1288
+ !retained ||
1289
+ settlingToken.current !== retained.token ||
1290
+ current.phase !== 'idle'
1291
+ )
1292
+ return;
1293
+ const zone = current.value?.zones.find(entry =>
1294
+ entry.kind === 'list'
1295
+ ? entry.itemIds.includes(retained.item.id)
1296
+ : entry.placements.some(item => item.itemId === retained.item.id),
1297
+ );
1298
+ const registration =
1299
+ zone &&
1300
+ Array.from(registry.current.values()).find(
1301
+ entry =>
1302
+ entry.zoneId === zone.id &&
1303
+ entry.revision === current.value?.revision,
1304
+ );
1305
+ let contentRect: LayoutRect | null = null;
1306
+ if (registration?.kind === 'list' && zone?.kind === 'list') {
1307
+ contentRect =
1308
+ registration.layout.entries.find(
1309
+ entry => entry.itemId === retained.item.id,
1310
+ )?.rect ?? null;
1311
+ } else if (registration?.kind === 'grid' && zone?.kind === 'grid') {
1312
+ const placement = zone.placements.find(
1313
+ entry => entry.itemId === retained.item.id,
1314
+ );
1315
+ if (placement)
1316
+ contentRect = itemRect(
1317
+ placement.position,
1318
+ placement.span,
1319
+ registration.geometry,
1320
+ registration.cells,
1321
+ );
1322
+ }
1323
+ const uiZone =
1324
+ publishedZones.current.find(entry => entry.zoneId === zone?.id) ?? null;
1325
+ const owner = Array.from(handles.current.values()).find(
1326
+ entry =>
1327
+ entry.mounted &&
1328
+ entry.registration.itemId === retained.item.id &&
1329
+ entry.registration.zoneId === zone?.id,
1330
+ );
1331
+ const itemRef = owner?.registration.sourceRef ?? null;
1332
+ const ownerToken = retained.token;
1333
+ scheduleOnUI(() => {
1334
+ const work = settlement.get();
1335
+ if (work?.token === ownerToken) {
1336
+ settlement.set({ ...work, zone: uiZone, contentRect, itemRef });
1337
+ }
1338
+ });
1339
+ };
1340
+
1341
+ useFrameCallback(
1342
+ useCallback(
1343
+ (info: FrameInfo) => {
1344
+ 'worklet';
1345
+ const state = motion.get();
1346
+ const work = settlement.get();
1347
+ if (work && work.token === state.token) {
1348
+ const startedAt = work.startedAt ?? info.timestamp;
1349
+ if (work.startedAt === null) settlement.set({ ...work, startedAt });
1350
+ const root = measureDndRect(rootRef);
1351
+ const viewport = work.zone ? measureDndRect(work.zone.ref) : null;
1352
+ const nativeRect = work.itemRef ? measureDndRect(work.itemRef) : null;
1353
+ const offset =
1354
+ work.zone?.kind === 'list' ? work.zone.offset.get() : 0;
1355
+ const geometry =
1356
+ root &&
1357
+ viewport &&
1358
+ work.contentRect &&
1359
+ work.zone?.revision === work.revision
1360
+ ? {
1361
+ root,
1362
+ viewport,
1363
+ contentRect: work.contentRect,
1364
+ nativeRect,
1365
+ scrollOffset:
1366
+ work.zone.kind === 'list' &&
1367
+ work.zone.orientation === 'horizontal'
1368
+ ? { x: offset, y: 0 }
1369
+ : { x: 0, y: offset },
1370
+ pixelRatio,
1371
+ }
1372
+ : null;
1373
+ const destination = geometry ? settlementTarget(geometry) : null;
1374
+ const ready = geometry ? settlementDestination(geometry) : null;
1375
+ if (
1376
+ work.committed &&
1377
+ ((geometry && !destination) ||
1378
+ (root &&
1379
+ (root.width !== state.root.width ||
1380
+ root.height !== state.root.height)) ||
1381
+ info.timestamp - startedAt >
1382
+ (work.animating ? durationMs + 750 : 750))
1383
+ ) {
1384
+ cancelAnimation(settleProgress);
1385
+ settlement.set(null);
1386
+ motion.set({ ...EMPTY_DND_MOTION, token: work.token });
1387
+ scheduleOnRN(finishPreview, work.token);
1388
+ } else if (destination && !work.animating) {
1389
+ settlement.set({
1390
+ ...work,
1391
+ startedAt,
1392
+ animationStartedAt: info.timestamp,
1393
+ destination,
1394
+ animating: true,
1395
+ });
1396
+ settleProgress.set(0);
1397
+ settleProgress.set(
1398
+ withTiming(
1399
+ 1,
1400
+ {
1401
+ duration: durationMs,
1402
+ reduceMotion: reduceMotion as ReduceMotion,
1403
+ },
1404
+ finished => {
1405
+ const latest = settlement.get();
1406
+ if (finished && latest?.token === work.token)
1407
+ settlement.set({ ...latest, done: true });
1408
+ },
1409
+ ),
1410
+ );
1411
+ } else if (
1412
+ destination &&
1413
+ work.animating &&
1414
+ work.destination &&
1415
+ (Math.abs(destination.x - work.destination.x) > 1 / pixelRatio ||
1416
+ Math.abs(destination.y - work.destination.y) > 1 / pixelRatio ||
1417
+ Math.abs(destination.width - work.destination.width) >
1418
+ 1 / pixelRatio ||
1419
+ Math.abs(destination.height - work.destination.height) >
1420
+ 1 / pixelRatio)
1421
+ ) {
1422
+ const progress = settleProgress.get();
1423
+ const from =
1424
+ interpolateSettlementRect(
1425
+ work.from,
1426
+ work.destination,
1427
+ progress,
1428
+ ) ?? work.from;
1429
+ cancelAnimation(settleProgress);
1430
+ settlement.set({
1431
+ ...work,
1432
+ startedAt,
1433
+ from,
1434
+ destination,
1435
+ done: false,
1436
+ animationStartedAt: work.done
1437
+ ? info.timestamp
1438
+ : work.animationStartedAt,
1439
+ });
1440
+ settleProgress.set(0);
1441
+ settleProgress.set(
1442
+ withTiming(
1443
+ 1,
1444
+ {
1445
+ duration: work.done
1446
+ ? durationMs
1447
+ : Math.max(
1448
+ 0,
1449
+ durationMs -
1450
+ (info.timestamp -
1451
+ (work.animationStartedAt ?? startedAt)),
1452
+ ),
1453
+ reduceMotion: reduceMotion as ReduceMotion,
1454
+ },
1455
+ finished => {
1456
+ const latest = settlement.get();
1457
+ if (finished && latest?.token === work.token)
1458
+ settlement.set({ ...latest, done: true });
1459
+ },
1460
+ ),
1461
+ );
1462
+ } else if (work.committed && ready && work.done) {
1463
+ cancelAnimation(settleProgress);
1464
+ settlement.set(null);
1465
+ motion.set({ ...EMPTY_DND_MOTION, token: work.token });
1466
+ scheduleOnRN(finishPreview, work.token);
1467
+ }
1468
+ return;
1469
+ }
1470
+ if (state.phase !== 'dragging' || uiDisabled.get()) return;
1471
+ const registered = zones.get();
1472
+ // Gesture events update the pointer only. One frame owns native geometry,
1473
+ // target lookup, and auto-scroll, including a stationary pointer scrolling.
1474
+ const root = measureDndRect(rootRef);
1475
+ const measured = root ? readListViewports(registered) : [];
1476
+ let next = { ...state, root: root ?? state.root, zones: measured };
1477
+ if (targetEnvironmentChanged(state, next)) {
1478
+ next = { ...next, seq: state.seq + 1 };
1479
+ motion.set(next);
1480
+ }
1481
+ const prepared = targetSession.get();
1482
+ if (prepared.token === state.token) {
1483
+ const sent = sentTarget.get();
1484
+ const target = uiLayoutTarget(
1485
+ next,
1486
+ registered,
1487
+ prepared,
1488
+ sent.target,
1489
+ );
1490
+ if (
1491
+ pendingMove.get()?.token !== next.token &&
1492
+ (!sent.state ||
1493
+ sent.state.token !== next.token ||
1494
+ !sameLayoutTarget(sent.target, target) ||
1495
+ targetEnvironmentChanged(sent.state, next) ||
1496
+ sent.policyRevision !== prepared.policyRevision)
1497
+ ) {
1498
+ if (sent.state && next.seq <= sent.state.seq) {
1499
+ next = { ...next, seq: sent.state.seq + 1 };
1500
+ motion.set(next);
1501
+ }
1502
+ sentTarget.set({
1503
+ state: next,
1504
+ target,
1505
+ policyRevision: prepared.policyRevision,
1506
+ });
1507
+ pendingMove.set({ token: next.token, seq: next.seq });
1508
+ scheduleOnRN(move, next);
1509
+ }
1510
+ }
1511
+ if (!root) return;
1512
+ if (!pointInRect(state.pointer, root)) return;
1513
+ for (const viewport of measured) {
1514
+ if (!pointInRect(state.pointer, viewport.viewport.rect)) continue;
1515
+ const list = registered.find(
1516
+ entry => entry.zoneId === viewport.zoneId,
1517
+ );
1518
+ if (!list || list.kind !== 'list') break;
1519
+ const vertical = list.orientation === 'vertical';
1520
+ const result = computeAutoScroll({
1521
+ pointer: vertical ? state.pointer.y : state.pointer.x,
1522
+ viewportStart: vertical
1523
+ ? viewport.viewport.rect.y
1524
+ : viewport.viewport.rect.x,
1525
+ viewportSize: vertical
1526
+ ? viewport.viewport.rect.height
1527
+ : viewport.viewport.rect.width,
1528
+ offset: viewport.offset,
1529
+ contentSize: list.contentSize,
1530
+ deltaTimeMs: info.timeSincePreviousFrame ?? 0,
1531
+ options: list.autoScroll,
1532
+ });
1533
+ if (result.valid && result.velocity !== 0)
1534
+ scrollTo(
1535
+ list.ref,
1536
+ vertical ? 0 : result.offset,
1537
+ vertical ? result.offset : 0,
1538
+ false,
1539
+ );
1540
+ break;
1541
+ }
1542
+ },
1543
+ [
1544
+ motion,
1545
+ settlement,
1546
+ rootRef,
1547
+ settleProgress,
1548
+ durationMs,
1549
+ reduceMotion,
1550
+ pixelRatio,
1551
+ finishPreview,
1552
+ uiDisabled,
1553
+ zones,
1554
+ targetSession,
1555
+ sentTarget,
1556
+ pendingMove,
1557
+ move,
1558
+ ],
1559
+ ),
1560
+ );
1561
+
1562
+ useLayoutEffect(() => {
1563
+ const active = bridge.current;
1564
+ if (snapshot.phase === 'idle') {
1565
+ const previousToken = active.token;
1566
+ const native = motion.get();
1567
+ const retained = preview.current;
1568
+ active.sessionId = null;
1569
+ if (
1570
+ retained &&
1571
+ native.visible &&
1572
+ native.token === previousToken &&
1573
+ durationMs > 0 &&
1574
+ !snapshot.disabled &&
1575
+ (settlingRevision.current === null ||
1576
+ settlingRevision.current === snapshot.value?.revision)
1577
+ ) {
1578
+ if (settlingToken.current !== previousToken) {
1579
+ settlingToken.current = previousToken;
1580
+ settlingRevision.current =
1581
+ snapshot.value?.revision ?? native.revision;
1582
+ const next: Settlement = {
1583
+ token: previousToken,
1584
+ itemId: retained.item.id,
1585
+ revision: snapshot.value?.revision ?? native.revision,
1586
+ from: {
1587
+ x: native.pointer.x - native.grip.x - native.root.x,
1588
+ y: native.pointer.y - native.grip.y - native.root.y,
1589
+ width: native.width,
1590
+ height: native.height,
1591
+ },
1592
+ destination: null,
1593
+ zone: null,
1594
+ contentRect: null,
1595
+ itemRef: null,
1596
+ startedAt: null,
1597
+ animationStartedAt: null,
1598
+ animating: false,
1599
+ done: false,
1600
+ committed: true,
1601
+ };
1602
+ scheduleOnUI(() => {
1603
+ if (motion.get().token !== previousToken) return;
1604
+ motion.set({ ...motion.get(), phase: 'released' });
1605
+ const current = settlement.get();
1606
+ settlement.set(
1607
+ current?.token === previousToken && !current.committed
1608
+ ? {
1609
+ ...current,
1610
+ committed: true,
1611
+ revision: next.revision,
1612
+ startedAt: null,
1613
+ zone: null,
1614
+ contentRect: null,
1615
+ itemRef: null,
1616
+ }
1617
+ : next,
1618
+ );
1619
+ });
1620
+ }
1621
+ refreshSettlement.current();
1622
+ } else {
1623
+ finishPreview(previousToken);
1624
+ scheduleOnUI(() => {
1625
+ if (motion.get().token !== previousToken) return;
1626
+ cancelAnimation(settleProgress);
1627
+ settlement.set(null);
1628
+ motion.set({ ...EMPTY_DND_MOTION, token: previousToken });
1629
+ });
1630
+ }
1631
+ } else if (previewReady) {
1632
+ const activeToken = active.token;
1633
+ scheduleOnUI(() => {
1634
+ const state = motion.get();
1635
+ if (
1636
+ state.token === activeToken &&
1637
+ state.phase !== 'idle' &&
1638
+ !state.visible
1639
+ )
1640
+ motion.set({ ...state, visible: true });
1641
+ });
1642
+ }
1643
+ }, [
1644
+ motion,
1645
+ snapshot,
1646
+ durationMs,
1647
+ settlement,
1648
+ settleProgress,
1649
+ finishPreview,
1650
+ previewReady,
1651
+ ]);
1652
+ useLayoutEffect(() => {
1653
+ mounted.current = true;
1654
+ // Child layout effects may register before the Provider is reactivated.
1655
+ publishHandles();
1656
+ return () => {
1657
+ mounted.current = false;
1658
+ handlePublication.generation++;
1659
+ handlePublication.queued = false;
1660
+ coordinator.dispose();
1661
+ flushSettled();
1662
+ scheduleOnUI(() => {
1663
+ cancelAnimation(settleProgress);
1664
+ settlement.set(null);
1665
+ motion.set(EMPTY_DND_MOTION);
1666
+ });
1667
+ };
1668
+ }, [
1669
+ coordinator,
1670
+ flushSettled,
1671
+ handlePublication,
1672
+ motion,
1673
+ publishHandles,
1674
+ settlement,
1675
+ settleProgress,
1676
+ ]);
1677
+ const overlayStyle = useAnimatedStyle(() => {
1678
+ const state = motion.get();
1679
+ const work = settlement.get();
1680
+ const destination = work?.destination;
1681
+ const rect =
1682
+ work && destination
1683
+ ? interpolateSettlementRect(
1684
+ work.from,
1685
+ destination,
1686
+ settleProgress.get(),
1687
+ )
1688
+ : null;
1689
+ const x = rect?.x ?? state.pointer.x - state.grip.x - state.root.x;
1690
+ const y = rect?.y ?? state.pointer.y - state.grip.y - state.root.y;
1691
+ const width = rect?.width ?? state.width;
1692
+ const height = rect?.height ?? state.height;
1693
+ return {
1694
+ opacity: state.visible ? 1 : 0,
1695
+ width: state.width,
1696
+ height: state.height,
1697
+ transform: [
1698
+ { translateX: x + (width - state.width) / 2 },
1699
+ { translateY: y + (height - state.height) / 2 },
1700
+ { scaleX: state.width > 0 ? width / state.width : 1 },
1701
+ { scaleY: state.height > 0 ? height / state.height : 1 },
1702
+ ],
1703
+ };
1704
+ });
1705
+ const activeItem = snapshot.value?.items.find(
1706
+ item => item.id === (snapshot.itemId ?? retainedPreview?.item.id),
1707
+ );
1708
+ const content = retainedPreview;
1709
+ const previewSessionId = snapshot.sessionId ?? content?.sessionId;
1710
+ const previewSourceZoneId = snapshot.sourceZoneId ?? content?.sourceZoneId;
1711
+ const runtime = useMemo<DndRuntime>(
1712
+ () => ({
1713
+ get snapshot() {
1714
+ return renderStore.getSnapshot();
1715
+ },
1716
+ renderStore,
1717
+ committedRevision: nativeRevision,
1718
+ motion,
1719
+ presentation,
1720
+ presentationSnapshot,
1721
+ zones,
1722
+ disabled: uiDisabled,
1723
+ token,
1724
+ rootRef,
1725
+ dragAxis,
1726
+ activationDelayMs: configValid ? activation.delayMs : 0,
1727
+ gestureConfigRevision,
1728
+ animation,
1729
+ begin,
1730
+ move,
1731
+ release,
1732
+ cancel,
1733
+ registerZone,
1734
+ unregisterZone,
1735
+ registerHandle,
1736
+ unregisterHandle,
1737
+ reportIssues,
1738
+ }),
1739
+ [
1740
+ renderStore,
1741
+ nativeRevision,
1742
+ motion,
1743
+ presentation,
1744
+ presentationSnapshot,
1745
+ zones,
1746
+ uiDisabled,
1747
+ token,
1748
+ rootRef,
1749
+ dragAxis,
1750
+ configValid,
1751
+ activation?.delayMs,
1752
+ gestureConfigRevision,
1753
+ animation,
1754
+ begin,
1755
+ move,
1756
+ release,
1757
+ cancel,
1758
+ registerZone,
1759
+ unregisterZone,
1760
+ registerHandle,
1761
+ unregisterHandle,
1762
+ reportIssues,
1763
+ ],
1764
+ );
1765
+ return (
1766
+ <DndRuntimeContext value={runtime}>
1767
+ <DndGestureHost registry={gestureRegistry} runtime={gestureRuntime}>
1768
+ <Animated.View
1769
+ {...viewProps}
1770
+ ref={rootRef}
1771
+ collapsable={false}
1772
+ style={[styles.root, style]}
1773
+ onLayout={event => {
1774
+ const { width, height } = event.nativeEvent.layout;
1775
+ if (
1776
+ rootLayout.current &&
1777
+ (rootLayout.current.width !== width ||
1778
+ rootLayout.current.height !== height)
1779
+ )
1780
+ coordinator.cancel('geometry-changed');
1781
+ rootLayout.current = { width, height };
1782
+ onLayout?.(event);
1783
+ }}
1784
+ >
1785
+ {children}
1786
+ {activeItem && content && previewSessionId && previewSourceZoneId && (
1787
+ <DndItemContext
1788
+ value={{
1789
+ itemId: activeItem.id,
1790
+ zoneId: previewSourceZoneId,
1791
+ ref: rootRef,
1792
+ preview: true,
1793
+ }}
1794
+ >
1795
+ <Animated.View
1796
+ pointerEvents="none"
1797
+ style={[styles.preview, overlayStyle, dragPreviewStyle]}
1798
+ >
1799
+ {renderDragPreview
1800
+ ? renderDragPreview({
1801
+ item: activeItem,
1802
+ index: content.index,
1803
+ zoneId: previewSourceZoneId,
1804
+ isDragging: true,
1805
+ sessionId: previewSessionId,
1806
+ targetZoneId: snapshot.targetZoneId,
1807
+ validity: snapshot.validity,
1808
+ width: content.width,
1809
+ height: content.height,
1810
+ onReady: handlePreviewReady,
1811
+ })
1812
+ : content.render({
1813
+ item: activeItem,
1814
+ index: content.index,
1815
+ zoneId: previewSourceZoneId,
1816
+ isDragging: true,
1817
+ })}
1818
+ </Animated.View>
1819
+ </DndItemContext>
1820
+ )}
1821
+ </Animated.View>
1822
+ </DndGestureHost>
1823
+ </DndRuntimeContext>
1824
+ );
1825
+ }
1826
+
1827
+ const styles = StyleSheet.create({
1828
+ root: { position: 'relative' },
1829
+ preview: {
1830
+ position: 'absolute',
1831
+ left: 0,
1832
+ top: 0,
1833
+ zIndex: 1000,
1834
+ elevation: 16,
1835
+ },
1836
+ });