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,1028 @@
1
+ import {
2
+ createContext,
3
+ memo,
4
+ useCallback,
5
+ useContext,
6
+ useEffect,
7
+ useLayoutEffect,
8
+ useMemo,
9
+ useRef,
10
+ useState,
11
+ } from 'react';
12
+ import {
13
+ type FlatList,
14
+ type HostInstance,
15
+ type LayoutChangeEvent,
16
+ type ScrollView,
17
+ StyleSheet,
18
+ View,
19
+ } from 'react-native';
20
+ import Animated, {
21
+ type AnimatedRef,
22
+ type SharedValue,
23
+ makeMutable,
24
+ useAnimatedRef,
25
+ useAnimatedScrollHandler,
26
+ useAnimatedStyle,
27
+ useDerivedValue,
28
+ useSharedValue,
29
+ } from 'react-native-reanimated';
30
+ import { scheduleOnRN, scheduleOnUI } from 'react-native-worklets';
31
+
32
+ import { validateAutoScrollOptions } from '../adapters/scroll';
33
+ import { resolveListVirtualization } from '../adapters/virtualizedList';
34
+ import type { DndItem, ListLayoutZone } from '../contracts';
35
+ import {
36
+ computeListLayout,
37
+ ListSizeCache,
38
+ type ListLayoutEntry,
39
+ type ListLayoutResult,
40
+ } from '../engine/list';
41
+ import {
42
+ DndItemContext,
43
+ useDndRuntime,
44
+ useDndZoneSnapshot,
45
+ useDndItemSnapshot,
46
+ type DndRuntime,
47
+ } from '../runtime/dndRuntime';
48
+ import {
49
+ createZonePresentation,
50
+ type DndZonePresentation,
51
+ } from '../runtime/dndPresentation';
52
+ import type { RetainSlotsRule } from '../runtime/dndRenderStore';
53
+ import type { ListItemRenderArgs, SortableListProps } from './dndTypes';
54
+ import { useListCellMotion } from '../pointer/listCellMotion';
55
+
56
+ type ListCellProps<T> = {
57
+ item: DndItem<T>;
58
+ index: number;
59
+ zoneId: string;
60
+ committedRevision?: SharedValue<number>;
61
+ start: number;
62
+ size: number;
63
+ orientation: 'vertical' | 'horizontal';
64
+ crossSize: number;
65
+ fixed: boolean;
66
+ measurementIdentity: object;
67
+ renderItem(args: ListItemRenderArgs<T>): React.ReactNode;
68
+ hiddenItemId: SharedValue<string | null>;
69
+ animation: DndRuntime['animation'];
70
+ presentation: DndRuntime['presentation'];
71
+ isDragging: boolean;
72
+ };
73
+
74
+ type ListMeasurementState = {
75
+ revision: number;
76
+ onSize(itemId: string, size: number): void;
77
+ };
78
+
79
+ const ListMeasurementContext = createContext<ListMeasurementState | null>(null);
80
+
81
+ function ListCellContent<T>({
82
+ item,
83
+ index,
84
+ zoneId,
85
+ committedRevision,
86
+ start,
87
+ size,
88
+ orientation,
89
+ crossSize,
90
+ fixed,
91
+ measurementIdentity,
92
+ slotStart,
93
+ expectedStart,
94
+ renderItem,
95
+ hiddenItemId,
96
+ animation,
97
+ presentation,
98
+ isDragging,
99
+ }: ListCellProps<T> & {
100
+ /**
101
+ * Flow start of the enclosing native slot in list content coordinates. It
102
+ * is applied as a plain style in the React commit that positions the slot,
103
+ * so the slot move and its compensation reach the screen in one mount
104
+ * transaction. A measured correction would lag that commit by a frame and
105
+ * show the cell at its new slot before its content start has moved.
106
+ */
107
+ slotStart?: number;
108
+ /** UI copy of the slot's flow start for the motion fallback target. */
109
+ expectedStart?: SharedValue<number>;
110
+ }) {
111
+ const measurement = useContext(ListMeasurementContext);
112
+ if (!measurement)
113
+ throw new Error('ListCell requires its measurement context.');
114
+ const { revision, onSize } = measurement;
115
+ const ref = useAnimatedRef();
116
+ const mounted = useRef(false);
117
+ useLayoutEffect(() => {
118
+ mounted.current = true;
119
+ return () => {
120
+ mounted.current = false;
121
+ };
122
+ }, []);
123
+ const itemId = item.id;
124
+ const runtime = useDndRuntime();
125
+ const publicSlot = useDndItemSnapshot(runtime, zoneId, itemId);
126
+ const itemContext = useMemo(
127
+ () => ({ itemId, zoneId, ref }),
128
+ [itemId, zoneId, ref],
129
+ );
130
+ const publicIndex = publicSlot?.index ?? index;
131
+ const content = useMemo(
132
+ () => renderItem({ item, index: publicIndex, zoneId, isDragging }),
133
+ [item, publicIndex, zoneId, isDragging, renderItem],
134
+ );
135
+ const vertical = orientation === 'vertical';
136
+ const revisionSource = committedRevision ?? revision;
137
+ // Mount uses the Provider's JS view of the scheduled UI geometry. Reading the
138
+ // shared presentation from JS would block on the UI runtime and copy every
139
+ // rect for each new cell; the UI mapper adopts actual UI geometry first.
140
+ const { animatedStart, targetSize } = useListCellMotion({
141
+ zoneId,
142
+ itemId,
143
+ vertical,
144
+ fixed,
145
+ start: expectedStart ?? start,
146
+ size,
147
+ presentation,
148
+ revision: revisionSource,
149
+ hiddenItemId,
150
+ animation,
151
+ initial: {
152
+ presentation: runtime.presentationSnapshot?.(),
153
+ revision:
154
+ typeof revisionSource === 'number'
155
+ ? revisionSource
156
+ : (runtime.snapshot.value?.revision ?? 0),
157
+ start,
158
+ },
159
+ });
160
+ // Natural-size cells never consume a target dimension; only authoritative
161
+ // sizes read the validated size, so measurements do not restart this mapper.
162
+ // The transform is the content start itself; the slot offset below cancels
163
+ // the native flow position, so no per-frame slot measurement is involved.
164
+ const animated = useAnimatedStyle(() => ({
165
+ opacity: hiddenItemId.get() === itemId ? 0 : 1,
166
+ ...(fixed
167
+ ? vertical
168
+ ? { height: targetSize.get() }
169
+ : { width: targetSize.get() }
170
+ : {}),
171
+ transform: [
172
+ { translateX: vertical ? 0 : animatedStart.get() },
173
+ { translateY: vertical ? animatedStart.get() : 0 },
174
+ ],
175
+ }));
176
+ const measured = useCallback(
177
+ (width: number, height: number) => {
178
+ if (mounted.current && !fixed) onSize(itemId, vertical ? height : width);
179
+ },
180
+ [fixed, itemId, onSize, vertical],
181
+ );
182
+ const measurementRequest = useRef<{
183
+ identity: object;
184
+ itemId: string;
185
+ fixed: boolean;
186
+ pending: boolean;
187
+ } | null>(null);
188
+ useEffect(() => {
189
+ const previous = measurementRequest.current;
190
+ const request = {
191
+ identity: measurementIdentity,
192
+ itemId,
193
+ fixed,
194
+ pending:
195
+ !fixed &&
196
+ previous !== null &&
197
+ (previous.identity !== measurementIdentity ||
198
+ previous.itemId !== itemId ||
199
+ previous.fixed ||
200
+ previous.pending),
201
+ };
202
+ measurementRequest.current = request;
203
+ // A new native cell reports its initial size through onLayout. Asking for
204
+ // the same measurement here doubles native callbacks while windowing.
205
+ // An existing cell needs a fallback only when its cache context changes,
206
+ // since unchanged native dimensions may not produce another onLayout.
207
+ if (!request.pending) return;
208
+ let current = true;
209
+ ref.current?.measure((_x, _y, width, height) => {
210
+ if (current) {
211
+ request.pending = false;
212
+ measured(width, height);
213
+ }
214
+ });
215
+ return () => {
216
+ current = false;
217
+ };
218
+ }, [fixed, itemId, measured, measurementIdentity, ref]);
219
+ return (
220
+ <DndItemContext value={itemContext}>
221
+ <Animated.View
222
+ ref={ref}
223
+ collapsable={false}
224
+ style={[
225
+ styles.cell,
226
+ vertical
227
+ ? {
228
+ width: crossSize,
229
+ ...(slotStart !== undefined ? { top: negate(slotStart) } : {}),
230
+ ...(fixed ? { height: size } : {}),
231
+ }
232
+ : {
233
+ height: crossSize,
234
+ ...(slotStart !== undefined ? { left: negate(slotStart) } : {}),
235
+ ...(fixed ? { width: size } : {}),
236
+ },
237
+ animated,
238
+ ]}
239
+ onLayout={event =>
240
+ measured(
241
+ event.nativeEvent.layout.width,
242
+ event.nativeEvent.layout.height,
243
+ )
244
+ }
245
+ >
246
+ {content}
247
+ </Animated.View>
248
+ </DndItemContext>
249
+ );
250
+ }
251
+
252
+ // Pointer/session updates must not rerender every mounted card. Pass only the
253
+ // cell's visible state and primitive geometry; entries are rebuilt together
254
+ // when an item is measured or moved, including entries that did not change.
255
+ const ListCell = memo(ListCellContent) as typeof ListCellContent;
256
+
257
+ type ExpectedStartUpdate = { shared: SharedValue<number>; start: number };
258
+ /** Queue a slot's new flow start for the UI runtime; `null` drops it. */
259
+ type PublishExpectedStart = (
260
+ shared: SharedValue<number>,
261
+ start: number | null,
262
+ ) => void;
263
+
264
+ function applyExpectedStarts(updates: ExpectedStartUpdate[]) {
265
+ 'worklet';
266
+ for (const update of updates) update.shared.set(update.start);
267
+ }
268
+
269
+ function negate(value: number) {
270
+ return value === 0 ? 0 : -value;
271
+ }
272
+
273
+ function VirtualListCellContent<T>({
274
+ publishExpectedStart,
275
+ slotSize,
276
+ ...cellProps
277
+ }: ListCellProps<T> & {
278
+ publishExpectedStart: PublishExpectedStart;
279
+ slotSize: number;
280
+ }) {
281
+ const style = useMemo(
282
+ () =>
283
+ cellProps.orientation === 'vertical'
284
+ ? { width: cellProps.crossSize, height: slotSize }
285
+ : [
286
+ styles.horizontalSlot,
287
+ { height: cellProps.crossSize, width: slotSize },
288
+ ],
289
+ [cellProps.orientation, cellProps.crossSize, slotSize],
290
+ );
291
+ // The flow start is only ever assigned plain numbers. A plain mutable skips
292
+ // the per-value cancelAnimation UI task that useSharedValue schedules on
293
+ // unmount. It feeds the cell's motion fallback target; positioning itself
294
+ // uses the React-committed slot offset, so nothing waits for a native measure.
295
+ const [expectedStart] = useState(() => makeMutable(cellProps.start));
296
+ const published = useRef(cellProps.start);
297
+ useLayoutEffect(() => {
298
+ if (published.current === cellProps.start) return;
299
+ published.current = cellProps.start;
300
+ publishExpectedStart(expectedStart, cellProps.start);
301
+ }, [cellProps.start, expectedStart, publishExpectedStart]);
302
+ useLayoutEffect(
303
+ () => () => publishExpectedStart(expectedStart, null),
304
+ [expectedStart, publishExpectedStart],
305
+ );
306
+ return (
307
+ <View collapsable={false} style={style}>
308
+ <ListCell
309
+ {...cellProps}
310
+ slotStart={cellProps.start}
311
+ expectedStart={expectedStart}
312
+ />
313
+ </View>
314
+ );
315
+ }
316
+
317
+ const VirtualListCell = memo(
318
+ VirtualListCellContent,
319
+ ) as typeof VirtualListCellContent;
320
+
321
+ type VirtualListRenderState = {
322
+ entriesById: ReadonlyMap<string, ListLayoutEntry>;
323
+ data: ReadonlyMap<string, DndItem<unknown>>;
324
+ activeItemId: string | null;
325
+ slotSize(entry: ListLayoutEntry): number;
326
+ cellProps: Omit<
327
+ ListCellProps<unknown>,
328
+ 'item' | 'index' | 'start' | 'size' | 'isDragging'
329
+ > & {
330
+ publishExpectedStart: PublishExpectedStart;
331
+ };
332
+ };
333
+
334
+ const VirtualListContext = createContext<VirtualListRenderState | null>(null);
335
+
336
+ function VirtualItemBridge({ itemId }: { itemId: string }) {
337
+ const state = useContext(VirtualListContext);
338
+ const entry = state?.entriesById.get(itemId);
339
+ const item = state?.data.get(itemId);
340
+ if (!state || !entry || !item) return null;
341
+ return (
342
+ <VirtualListCell
343
+ {...state.cellProps}
344
+ slotSize={state.slotSize(entry)}
345
+ item={item}
346
+ index={entry.index}
347
+ start={entry.start}
348
+ size={entry.size}
349
+ isDragging={state.activeItemId === itemId}
350
+ />
351
+ );
352
+ }
353
+
354
+ // Context updates reach the bridge even when FlatList's native cell wrapper
355
+ // skips rendering. Geometry and getItemLayout use the same React snapshot.
356
+ const renderVirtualItem = ({ item }: { item: string }) => (
357
+ <VirtualItemBridge itemId={item} />
358
+ );
359
+ // Our pinned RN 0.87 FlatList supports this in its Flow implementation, but
360
+ // omits it from FlatListProps.d.ts. Its default renderer recreates a callback
361
+ // on every metrics update; strictMode memoizes that internal callback too.
362
+ const FLAT_LIST_RENDERER_OPTIONS = { strictMode: true };
363
+
364
+ const EMPTY_ENTRIES: ListLayoutEntry[] = [];
365
+ const EMPTY_IDS: string[] = [];
366
+ const itemKey = (itemId: string) => itemId;
367
+ type PreparedListDisplay = {
368
+ zone: ListLayoutZone;
369
+ revision: number;
370
+ measurementVersion: number;
371
+ layout: ListLayoutResult;
372
+ presentation: DndZonePresentation | null;
373
+ };
374
+
375
+ type RetainSlotsInputs = {
376
+ layout: ListLayoutResult | null;
377
+ gap: number;
378
+ windowSize: number;
379
+ viewportLength: number;
380
+ };
381
+
382
+ /**
383
+ * FlatList mounts cells by slot order, so it can keep committed slots during a
384
+ * same-list preview only when its render window already holds every cell the
385
+ * preview can expose. Slots between the source and the target are displaced
386
+ * by at most the dragged slot (item plus gap), and VirtualizedList renders
387
+ * (windowSize - 1) / 2 viewport lengths beyond each viewport edge. A larger
388
+ * displacement falls back to candidate-order slots for that drag. Items of
389
+ * other lists change this list's membership, which the store resolves itself.
390
+ */
391
+ function retainsCommittedSlots(
392
+ inputs: RetainSlotsInputs,
393
+ itemId: string,
394
+ ): boolean {
395
+ if (!inputs.layout?.valid) return false;
396
+ const entry = inputs.layout.entries.find(
397
+ candidate => candidate.itemId === itemId,
398
+ );
399
+ if (!entry) return true;
400
+ return (
401
+ inputs.viewportLength > 0 &&
402
+ entry.size + inputs.gap <=
403
+ ((inputs.windowSize - 1) / 2) * inputs.viewportLength
404
+ );
405
+ }
406
+
407
+ /** Shared measured layout for ScrollView and FlatList adapters. */
408
+ export function SortableList<T>({
409
+ zoneId,
410
+ renderItem,
411
+ estimatedItemSize = 48,
412
+ itemSize,
413
+ gap = 0,
414
+ paddingStart = 0,
415
+ paddingEnd = 0,
416
+ autoScroll,
417
+ scrollEnabled = true,
418
+ showsScrollIndicator = true,
419
+ onScrollOffsetChange,
420
+ virtualization = false,
421
+ onLayout,
422
+ style,
423
+ ...viewProps
424
+ }: SortableListProps<T>) {
425
+ const runtime = useDndRuntime();
426
+ const { registerZone, unregisterZone, reportIssues } = runtime;
427
+ const windowing = resolveListVirtualization(virtualization);
428
+ const virtualized = windowing.valid && windowing.options !== null;
429
+ // Native slots keep their committed order through a same-list preview and
430
+ // the shared presentation moves the cells, so a candidate change renders
431
+ // nothing here. An index-dependent size recipe must update its native slot
432
+ // lengths too. FlatList keeps committed slots only while its render window
433
+ // covers the preview's displacement; the rule answers once per drag session.
434
+ const retainInputs = useRef<RetainSlotsInputs>({
435
+ layout: null,
436
+ gap: 0,
437
+ windowSize: 0,
438
+ viewportLength: 0,
439
+ });
440
+ const retainSlots = useCallback<RetainSlotsRule>(
441
+ itemId => retainsCommittedSlots(retainInputs.current, itemId),
442
+ [],
443
+ );
444
+ const snapshot = useDndZoneSnapshot(
445
+ runtime,
446
+ zoneId,
447
+ typeof itemSize === 'function' ? false : virtualized ? retainSlots : true,
448
+ );
449
+ const identity = useRef({}).current;
450
+ const [viewportSize, setViewportSize] = useState({ width: 0, height: 0 });
451
+ const [measurementVersion, setMeasurementVersion] = useState(0);
452
+ const zone = snapshot.zone;
453
+ const displayZone = snapshot.display;
454
+ const orientation = zone?.kind === 'list' ? zone.orientation : 'vertical';
455
+ const vertical = orientation === 'vertical';
456
+ const crossSize = vertical ? viewportSize.width : viewportSize.height;
457
+ const items = snapshot.items;
458
+ const revision = snapshot.revision;
459
+ const displayRevision = snapshot.displayRevision;
460
+ const cache = useMemo(
461
+ () => new ListSizeCache(),
462
+ // A new geometry/data context must not reuse old native measurements.
463
+ // eslint-disable-next-line react-hooks/exhaustive-deps
464
+ [crossSize, orientation, items],
465
+ );
466
+ const measurements = useMemo(
467
+ () => ({
468
+ cache,
469
+ version: 0,
470
+ committedVersion: 0,
471
+ frame: null as number | null,
472
+ active: false,
473
+ }),
474
+ [cache],
475
+ );
476
+ const measurementContext = useRef<{
477
+ cache: ListSizeCache;
478
+ revision: number;
479
+ } | null>(null);
480
+ const scheduleMeasurementRender = useCallback(() => {
481
+ if (
482
+ !measurements.active ||
483
+ measurements.frame !== null ||
484
+ measurements.version <= measurements.committedVersion
485
+ )
486
+ return;
487
+ const frame = requestAnimationFrame(() => {
488
+ // A cancelled native frame can already be queued for RN delivery.
489
+ if (measurements.frame !== frame) return;
490
+ measurements.frame = null;
491
+ if (
492
+ !measurements.active ||
493
+ measurementContext.current?.cache !== measurements.cache ||
494
+ measurements.version <= measurements.committedVersion
495
+ )
496
+ return;
497
+ setMeasurementVersion(current => current + 1);
498
+ });
499
+ measurements.frame = frame;
500
+ }, [measurements]);
501
+ useLayoutEffect(() => {
502
+ measurements.active = true;
503
+ scheduleMeasurementRender();
504
+ return () => {
505
+ measurements.active = false;
506
+ if (measurements.frame !== null) cancelAnimationFrame(measurements.frame);
507
+ measurements.frame = null;
508
+ };
509
+ }, [measurements, scheduleMeasurementRender]);
510
+ const renderedMeasurementVersion = useMemo(
511
+ () => measurements.version,
512
+ // An unrelated render may reuse memoized layout; only a measurement signal
513
+ // guarantees that the layout consumed the newest cached dimensions.
514
+ // eslint-disable-next-line react-hooks/exhaustive-deps
515
+ [measurements, measurementVersion],
516
+ );
517
+ useLayoutEffect(() => {
518
+ measurements.committedVersion = renderedMeasurementVersion;
519
+ // A callback arriving between render and commit already reserved the next
520
+ // frame. Its newer version must not be acknowledged by this older render.
521
+ }, [measurements, renderedMeasurementVersion]);
522
+ const scrollRef = useAnimatedRef<ScrollView>();
523
+ const offset = useSharedValue(0);
524
+ const { motion, animation, presentation, committedRevision } = runtime;
525
+ // Virtual cells position their content with a React-committed slot offset
526
+ // plus the animated content start. The UI runtime only needs each slot's
527
+ // flow start as the motion fallback target; changed starts of one commit
528
+ // travel together in one UI task, without RN shared-value reads.
529
+ const expectedStarts = useMemo(
530
+ () => ({
531
+ pending: new Map<SharedValue<number>, number>(),
532
+ queued: false,
533
+ active: false,
534
+ }),
535
+ // Native viewports and their cells are replaced when these change.
536
+ // eslint-disable-next-line react-hooks/exhaustive-deps
537
+ [orientation, virtualized, zoneId],
538
+ );
539
+ const flushExpectedStarts = useCallback(() => {
540
+ expectedStarts.queued = false;
541
+ if (!expectedStarts.active || expectedStarts.pending.size === 0) return;
542
+ const updates = Array.from(
543
+ expectedStarts.pending,
544
+ ([shared, start]): ExpectedStartUpdate => ({ shared, start }),
545
+ );
546
+ expectedStarts.pending.clear();
547
+ scheduleOnUI(applyExpectedStarts, updates);
548
+ }, [expectedStarts]);
549
+ const publishExpectedStart = useCallback<PublishExpectedStart>(
550
+ (shared, start) => {
551
+ if (start === null) {
552
+ expectedStarts.pending.delete(shared);
553
+ return;
554
+ }
555
+ expectedStarts.pending.set(shared, start);
556
+ if (expectedStarts.queued) return;
557
+ expectedStarts.queued = true;
558
+ (
559
+ globalThis as typeof globalThis & {
560
+ queueMicrotask(callback: () => void): void;
561
+ }
562
+ ).queueMicrotask(flushExpectedStarts);
563
+ },
564
+ [expectedStarts, flushExpectedStarts],
565
+ );
566
+ useLayoutEffect(() => {
567
+ expectedStarts.active = true;
568
+ return () => {
569
+ expectedStarts.active = false;
570
+ expectedStarts.pending.clear();
571
+ };
572
+ }, [expectedStarts]);
573
+ const prepareLayout = useMemo(() => {
574
+ let previous: PreparedListDisplay | undefined;
575
+ let entries = new Map<string, ListLayoutEntry>();
576
+ return (candidate: ListLayoutZone, candidateRevision: number) => {
577
+ if (candidate.orientation !== orientation || crossSize <= 0) return null;
578
+ if (
579
+ previous?.zone === candidate &&
580
+ previous.revision === candidateRevision &&
581
+ previous.measurementVersion === measurements.version
582
+ )
583
+ return previous;
584
+ const layout = computeListLayout({
585
+ itemIds: candidate.itemIds,
586
+ orientation,
587
+ crossSize,
588
+ estimatedItemSize,
589
+ itemSize,
590
+ gap,
591
+ paddingStart,
592
+ paddingEnd,
593
+ revision: candidateRevision,
594
+ cache,
595
+ });
596
+ if (layout.valid) {
597
+ const nextEntries = new Map<string, ListLayoutEntry>();
598
+ layout.entries = layout.entries.map(entry => {
599
+ const old = entries.get(entry.itemId);
600
+ const sameRect =
601
+ old &&
602
+ old.rect.x === entry.rect.x &&
603
+ old.rect.y === entry.rect.y &&
604
+ old.rect.width === entry.rect.width &&
605
+ old.rect.height === entry.rect.height;
606
+ // Rects contain only coordinates. Preserve their immutable identity
607
+ // across revision/source changes so UI need not receive them again;
608
+ // the new entry still carries its current index and revision.
609
+ if (sameRect) entry.rect = old.rect;
610
+ const same =
611
+ old &&
612
+ old.index === entry.index &&
613
+ old.start === entry.start &&
614
+ old.size === entry.size &&
615
+ old.source === entry.source &&
616
+ old.revision === entry.revision &&
617
+ sameRect;
618
+ const retained = same ? old : entry;
619
+ nextEntries.set(entry.itemId, retained);
620
+ return retained;
621
+ });
622
+ entries = nextEntries;
623
+ }
624
+ previous = {
625
+ zone: candidate,
626
+ revision: candidateRevision,
627
+ measurementVersion: measurements.version,
628
+ layout,
629
+ // Candidates of one item set share the slot map; only coordinates cross
630
+ // to UI again.
631
+ presentation: layout.valid
632
+ ? createZonePresentation(
633
+ layout.entries.map(entry => [entry.itemId, entry.rect] as const),
634
+ previous?.presentation,
635
+ )
636
+ : null,
637
+ };
638
+ return previous;
639
+ };
640
+ }, [
641
+ cache,
642
+ crossSize,
643
+ estimatedItemSize,
644
+ gap,
645
+ itemSize,
646
+ measurements,
647
+ orientation,
648
+ paddingEnd,
649
+ paddingStart,
650
+ ]);
651
+ const prepareDisplay = useCallback(
652
+ (candidate: ListLayoutZone, candidateRevision: number) =>
653
+ prepareLayout(candidate, candidateRevision)?.presentation ?? null,
654
+ [prepareLayout],
655
+ );
656
+ const base = useMemo(
657
+ () =>
658
+ zone?.kind === 'list'
659
+ ? (prepareLayout(zone, revision)?.layout ?? null)
660
+ : null,
661
+ // Cache writes are mutable; the version explicitly invalidates this result.
662
+ // eslint-disable-next-line react-hooks/exhaustive-deps
663
+ [zone, revision, prepareLayout, measurementVersion],
664
+ );
665
+ const viewportLength = vertical ? viewportSize.height : viewportSize.width;
666
+ const windowSize = windowing.valid ? (windowing.options?.windowSize ?? 0) : 0;
667
+ useLayoutEffect(() => {
668
+ // Read by the retain rule when a drag starts. The committed layout does
669
+ // not change between idle and the first preview of a session.
670
+ retainInputs.current = { layout: base, gap, windowSize, viewportLength };
671
+ }, [base, gap, viewportLength, windowSize]);
672
+ const display = useMemo(
673
+ () =>
674
+ displayZone === zone
675
+ ? base
676
+ : displayZone?.kind === 'list'
677
+ ? (prepareLayout(displayZone, displayRevision)?.layout ?? null)
678
+ : null,
679
+ // Candidate rects must also be recomputed after an accepted measurement.
680
+ // eslint-disable-next-line react-hooks/exhaustive-deps
681
+ [
682
+ displayZone,
683
+ zone,
684
+ base,
685
+ displayRevision,
686
+ prepareLayout,
687
+ measurementVersion,
688
+ ],
689
+ );
690
+ const validScroll =
691
+ validateAutoScrollOptions(autoScroll).valid &&
692
+ typeof scrollEnabled === 'boolean';
693
+ useLayoutEffect(() => {
694
+ measurementContext.current = { cache, revision };
695
+ return () => {
696
+ measurementContext.current = null;
697
+ };
698
+ }, [cache, revision]);
699
+ useLayoutEffect(() => {
700
+ // Switching adapter/direction remounts the native viewport.
701
+ offset.set(0);
702
+ return () => unregisterZone(zoneId, identity);
703
+ }, [identity, offset, orientation, unregisterZone, virtualized, zoneId]);
704
+ useLayoutEffect(() => {
705
+ reportIssues(
706
+ `list:${zoneId}`,
707
+ zone?.kind !== 'list'
708
+ ? [
709
+ {
710
+ code: 'invalid-configuration',
711
+ zoneId,
712
+ message: 'SortableList requires an existing list zone.',
713
+ },
714
+ ]
715
+ : !windowing.valid
716
+ ? windowing.issues
717
+ : !validScroll
718
+ ? [
719
+ {
720
+ code: 'invalid-configuration',
721
+ zoneId,
722
+ message: 'List scrolling options are invalid.',
723
+ },
724
+ ]
725
+ : base && !base.valid
726
+ ? base.issues
727
+ : [],
728
+ );
729
+ });
730
+ useLayoutEffect(() => {
731
+ if (!base?.valid || !display?.valid || !validScroll || !windowing.valid) {
732
+ unregisterZone(zoneId, identity);
733
+ return;
734
+ }
735
+ registerZone(
736
+ {
737
+ kind: 'list',
738
+ zoneId,
739
+ orientation,
740
+ revision,
741
+ ref: scrollRef as unknown as AnimatedRef<HostInstance>,
742
+ offset,
743
+ contentSize: display.totalSize,
744
+ autoScroll: {
745
+ ...autoScroll,
746
+ enabled: scrollEnabled && autoScroll?.enabled !== false,
747
+ },
748
+ layout: base,
749
+ dataIdentity: items,
750
+ prepareDisplay,
751
+ renderItem: args => renderItem(args as ListItemRenderArgs<T>),
752
+ },
753
+ identity,
754
+ );
755
+ }, [
756
+ registerZone,
757
+ unregisterZone,
758
+ zoneId,
759
+ orientation,
760
+ revision,
761
+ scrollRef,
762
+ offset,
763
+ display,
764
+ base,
765
+ autoScroll,
766
+ validScroll,
767
+ scrollEnabled,
768
+ renderItem,
769
+ identity,
770
+ windowing.valid,
771
+ virtualized,
772
+ prepareDisplay,
773
+ items,
774
+ ]);
775
+ useLayoutEffect(
776
+ () => () => {
777
+ unregisterZone(zoneId, identity);
778
+ reportIssues(`list:${zoneId}`, []);
779
+ },
780
+ [zoneId, identity, unregisterZone, reportIssues],
781
+ );
782
+ const onSize = useCallback(
783
+ (itemId: string, size: number) => {
784
+ if (
785
+ measurementContext.current?.cache !== cache ||
786
+ measurementContext.current.revision !== revision
787
+ )
788
+ return;
789
+ const previous = cache.get(itemId, { orientation, crossSize });
790
+ if (previous !== undefined && Math.abs(previous - size) < 0.001) return;
791
+ if (
792
+ cache.set(itemId, size, { orientation, crossSize }, revision) &&
793
+ previous !== size
794
+ ) {
795
+ // Invalidate before React renders, since the Provider may ask for a
796
+ // validated presentation while this native measurement is still batched.
797
+ measurements.version++;
798
+ scheduleMeasurementRender();
799
+ }
800
+ },
801
+ [
802
+ cache,
803
+ orientation,
804
+ crossSize,
805
+ revision,
806
+ measurements,
807
+ scheduleMeasurementRender,
808
+ ],
809
+ );
810
+ const cellMeasurements = useMemo(
811
+ () => ({ revision, onSize }),
812
+ [revision, onSize],
813
+ );
814
+ const scrollHandler = useAnimatedScrollHandler({
815
+ onScroll: event => {
816
+ const nextOffset = vertical
817
+ ? event.contentOffset.y
818
+ : event.contentOffset.x;
819
+ offset.set(nextOffset);
820
+ if (onScrollOffsetChange)
821
+ scheduleOnRN(onScrollOffsetChange, {
822
+ x: event.contentOffset.x,
823
+ y: event.contentOffset.y,
824
+ });
825
+ },
826
+ });
827
+ const handleLayout = useCallback(
828
+ (event: LayoutChangeEvent) => {
829
+ const { width, height } = event.nativeEvent.layout;
830
+ setViewportSize(current =>
831
+ current.width === width && current.height === height
832
+ ? current
833
+ : { width, height },
834
+ );
835
+ onLayout?.(event);
836
+ },
837
+ [onLayout],
838
+ );
839
+ const data = useMemo(
840
+ () => new Map(items?.map(item => [item.id, item]) ?? []),
841
+ [items],
842
+ );
843
+ const contentSize = display?.valid ? display.totalSize : 0;
844
+ const visibleEntries = display?.valid ? display.entries : EMPTY_ENTRIES;
845
+ const visibleIds =
846
+ display?.valid && displayZone?.kind === 'list'
847
+ ? displayZone.itemIds
848
+ : EMPTY_IDS;
849
+ const entriesById = useMemo(
850
+ () => new Map(visibleEntries.map(entry => [entry.itemId, entry])),
851
+ [visibleEntries],
852
+ );
853
+ const entryCount = visibleEntries.length;
854
+ const slotSize = useCallback(
855
+ (entry: ListLayoutEntry) =>
856
+ entry.size + (entry.index < entryCount - 1 ? gap : 0),
857
+ [entryCount, gap],
858
+ );
859
+ const getItemLayout = useCallback(
860
+ (_itemIds: ArrayLike<string> | null | undefined, index: number) => {
861
+ const entry = visibleEntries[index];
862
+ return { index, offset: entry.start, length: slotSize(entry) };
863
+ },
864
+ [slotSize, visibleEntries],
865
+ );
866
+ // Cells only need the active source's visibility, not the pointer and all
867
+ // measured viewports copied into motion on every frame.
868
+ const hiddenItemId = useDerivedValue(() => {
869
+ const state = motion.get();
870
+ return state.visible ? state.itemId : null;
871
+ });
872
+ const activeItemId = snapshot.activeItemId;
873
+ const fixed = itemSize !== undefined;
874
+ const virtualRenderState = useMemo<VirtualListRenderState>(
875
+ () => ({
876
+ entriesById,
877
+ data,
878
+ activeItemId,
879
+ slotSize,
880
+ cellProps: {
881
+ publishExpectedStart,
882
+ zoneId,
883
+ committedRevision,
884
+ orientation,
885
+ crossSize,
886
+ fixed,
887
+ measurementIdentity: cache,
888
+ renderItem,
889
+ hiddenItemId,
890
+ animation,
891
+ presentation,
892
+ },
893
+ }),
894
+ [
895
+ activeItemId,
896
+ animation,
897
+ cache,
898
+ committedRevision,
899
+ crossSize,
900
+ data,
901
+ entriesById,
902
+ fixed,
903
+ hiddenItemId,
904
+ orientation,
905
+ renderItem,
906
+ slotSize,
907
+ zoneId,
908
+ presentation,
909
+ publishExpectedStart,
910
+ ],
911
+ );
912
+ const contentContainerStyle = useMemo(
913
+ () =>
914
+ vertical
915
+ ? { paddingTop: paddingStart, paddingBottom: paddingEnd }
916
+ : { paddingLeft: paddingStart, paddingRight: paddingEnd },
917
+ [paddingEnd, paddingStart, vertical],
918
+ );
919
+ if (virtualized && windowing.valid && windowing.options) {
920
+ // These are actual native flow slots, including the trailing inter-item gap.
921
+ // The natural-size child is absolute so an estimate cannot constrain its
922
+ // measurement. Cache updates change both native slots and getItemLayout.
923
+ return (
924
+ <ListMeasurementContext value={cellMeasurements}>
925
+ <VirtualListContext value={virtualRenderState}>
926
+ <Animated.FlatList<string>
927
+ {...viewProps}
928
+ {...windowing.options}
929
+ {...FLAT_LIST_RENDERER_OPTIONS}
930
+ key={`${zoneId}:${orientation}:flat`}
931
+ ref={scrollRef as unknown as AnimatedRef<FlatList<string>>}
932
+ data={visibleIds}
933
+ keyExtractor={itemKey}
934
+ horizontal={!vertical}
935
+ style={style}
936
+ contentContainerStyle={contentContainerStyle}
937
+ onLayout={handleLayout}
938
+ onScroll={scrollHandler}
939
+ scrollEventThrottle={16}
940
+ scrollEnabled={scrollEnabled && snapshot.scrollEnabled}
941
+ removeClippedSubviews={false}
942
+ showsVerticalScrollIndicator={showsScrollIndicator}
943
+ showsHorizontalScrollIndicator={showsScrollIndicator}
944
+ contentInsetAdjustmentBehavior="never"
945
+ getItemLayout={getItemLayout}
946
+ renderItem={renderVirtualItem}
947
+ />
948
+ </VirtualListContext>
949
+ </ListMeasurementContext>
950
+ );
951
+ }
952
+ const entries = new Map(
953
+ display?.valid ? display.entries.map(entry => [entry.itemId, entry]) : [],
954
+ );
955
+ // The ScrollView adapter retains source cells through cross-list previews.
956
+ // FlatList may unmount them; the provider owns the gesture and overlay.
957
+ const renderIds = new Set([
958
+ ...(zone?.kind === 'list' ? zone.itemIds : []),
959
+ ...(displayZone?.kind === 'list' ? displayZone.itemIds : []),
960
+ ]);
961
+ return (
962
+ <ListMeasurementContext value={cellMeasurements}>
963
+ <Animated.ScrollView
964
+ {...viewProps}
965
+ key={`${zoneId}:${orientation}:scroll`}
966
+ ref={scrollRef}
967
+ horizontal={!vertical}
968
+ style={style}
969
+ onLayout={handleLayout}
970
+ onScroll={scrollHandler}
971
+ scrollEventThrottle={16}
972
+ scrollEnabled={scrollEnabled && snapshot.scrollEnabled}
973
+ removeClippedSubviews={false}
974
+ showsVerticalScrollIndicator={showsScrollIndicator}
975
+ showsHorizontalScrollIndicator={showsScrollIndicator}
976
+ contentInsetAdjustmentBehavior="never"
977
+ >
978
+ <View
979
+ style={
980
+ vertical
981
+ ? { width: crossSize, height: contentSize }
982
+ : [
983
+ styles.horizontalSlot,
984
+ { height: crossSize, width: contentSize },
985
+ ]
986
+ }
987
+ >
988
+ {base?.valid &&
989
+ display?.valid &&
990
+ Array.from(renderIds).map(itemId => {
991
+ const item = data.get(itemId);
992
+ const entry =
993
+ entries.get(itemId) ??
994
+ base.entries.find(candidate => candidate.itemId === itemId);
995
+ if (!item || !entry) return null;
996
+ return (
997
+ <ListCell
998
+ key={itemId}
999
+ item={item as DndItem<T>}
1000
+ index={entry.index}
1001
+ zoneId={zoneId}
1002
+ committedRevision={committedRevision}
1003
+ start={entry.start}
1004
+ size={entry.size}
1005
+ orientation={orientation}
1006
+ crossSize={crossSize}
1007
+ fixed={fixed}
1008
+ measurementIdentity={cache}
1009
+ renderItem={renderItem}
1010
+ hiddenItemId={hiddenItemId}
1011
+ animation={animation}
1012
+ presentation={presentation}
1013
+ isDragging={activeItemId === item.id}
1014
+ />
1015
+ );
1016
+ })}
1017
+ </View>
1018
+ </Animated.ScrollView>
1019
+ </ListMeasurementContext>
1020
+ );
1021
+ }
1022
+
1023
+ const styles = StyleSheet.create({
1024
+ cell: { position: 'absolute', left: 0, top: 0 },
1025
+ // Yoga otherwise fits an auto-width absolute child to a column parent's
1026
+ // estimated width. A row leaves the measured main axis unconstrained.
1027
+ horizontalSlot: { flexDirection: 'row' },
1028
+ });