panelui-native 0.71.0 → 0.75.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 (306) hide show
  1. package/README.md +5 -2
  2. package/lib/module/components/accordion/index.js +10 -4
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/area-chart/index.js +1 -0
  5. package/lib/module/components/area-chart/index.js.map +1 -1
  6. package/lib/module/components/avatar/avatar-group.js +64 -0
  7. package/lib/module/components/avatar/avatar-group.js.map +1 -0
  8. package/lib/module/components/avatar/index.js +84 -2
  9. package/lib/module/components/avatar/index.js.map +1 -1
  10. package/lib/module/components/breadcrumb/index.js +2 -1
  11. package/lib/module/components/breadcrumb/index.js.map +1 -1
  12. package/lib/module/components/button/index.js +7 -5
  13. package/lib/module/components/button/index.js.map +1 -1
  14. package/lib/module/components/carousel/index.js +1 -1
  15. package/lib/module/components/carousel/index.js.map +1 -1
  16. package/lib/module/components/chip/index.js +2 -2
  17. package/lib/module/components/chip/index.js.map +1 -1
  18. package/lib/module/components/code-block/index.js.map +1 -1
  19. package/lib/module/components/collapsible/index.js +5 -1
  20. package/lib/module/components/collapsible/index.js.map +1 -1
  21. package/lib/module/components/color-picker/color-picker-accessibility.js +12 -0
  22. package/lib/module/components/color-picker/color-picker-accessibility.js.map +1 -0
  23. package/lib/module/components/color-picker/index.js +9 -5
  24. package/lib/module/components/color-picker/index.js.map +1 -1
  25. package/lib/module/components/date-time-picker/index.js +11 -3
  26. package/lib/module/components/date-time-picker/index.js.map +1 -1
  27. package/lib/module/components/drawer/index.js +58 -13
  28. package/lib/module/components/drawer/index.js.map +1 -1
  29. package/lib/module/components/fab/index.js +7 -6
  30. package/lib/module/components/fab/index.js.map +1 -1
  31. package/lib/module/components/flow/index.js +1 -1
  32. package/lib/module/components/flow/index.js.map +1 -1
  33. package/lib/module/components/frame/index.js +2 -2
  34. package/lib/module/components/frame/index.js.map +1 -1
  35. package/lib/module/components/grid-item/index.js +2 -2
  36. package/lib/module/components/grid-item/index.js.map +1 -1
  37. package/lib/module/components/heatmap-chart/index.js +1 -0
  38. package/lib/module/components/heatmap-chart/index.js.map +1 -1
  39. package/lib/module/components/hex-chart/index.js +1 -0
  40. package/lib/module/components/hex-chart/index.js.map +1 -1
  41. package/lib/module/components/input/index.js +7 -4
  42. package/lib/module/components/input/index.js.map +1 -1
  43. package/lib/module/components/input/input-content-padding.js +6 -0
  44. package/lib/module/components/input/input-content-padding.js.map +1 -0
  45. package/lib/module/components/input-group/index.js +21 -11
  46. package/lib/module/components/input-group/index.js.map +1 -1
  47. package/lib/module/components/input-group/input-group-measurements.js +60 -0
  48. package/lib/module/components/input-group/input-group-measurements.js.map +1 -0
  49. package/lib/module/components/item/index.js +2 -2
  50. package/lib/module/components/item/index.js.map +1 -1
  51. package/lib/module/components/line-chart/index.js +1 -0
  52. package/lib/module/components/line-chart/index.js.map +1 -1
  53. package/lib/module/components/marker/index.js +2 -2
  54. package/lib/module/components/marker/index.js.map +1 -1
  55. package/lib/module/components/marquee/index.js +9 -12
  56. package/lib/module/components/marquee/index.js.map +1 -1
  57. package/lib/module/components/marquee/marquee-math.js +39 -0
  58. package/lib/module/components/marquee/marquee-math.js.map +1 -0
  59. package/lib/module/components/menu/index.js +11 -3
  60. package/lib/module/components/menu/index.js.map +1 -1
  61. package/lib/module/components/otp-input/index.js +3 -1
  62. package/lib/module/components/otp-input/index.js.map +1 -1
  63. package/lib/module/components/otp-input/otp-input-state.js +7 -0
  64. package/lib/module/components/otp-input/otp-input-state.js.map +1 -0
  65. package/lib/module/components/pagination/index.js +27 -11
  66. package/lib/module/components/pagination/index.js.map +1 -1
  67. package/lib/module/components/panelside/index.js +7 -2
  68. package/lib/module/components/panelside/index.js.map +1 -1
  69. package/lib/module/components/planner/index.js +46 -11
  70. package/lib/module/components/planner/index.js.map +1 -1
  71. package/lib/module/components/planner/planner-today.js +8 -0
  72. package/lib/module/components/planner/planner-today.js.map +1 -0
  73. package/lib/module/components/plot/index.js +113 -53
  74. package/lib/module/components/plot/index.js.map +1 -1
  75. package/lib/module/components/plot/plot-series-registry.js +32 -0
  76. package/lib/module/components/plot/plot-series-registry.js.map +1 -0
  77. package/lib/module/components/post/index.js +4 -4
  78. package/lib/module/components/post/index.js.map +1 -1
  79. package/lib/module/components/radio-group/index.js +2 -1
  80. package/lib/module/components/radio-group/index.js.map +1 -1
  81. package/lib/module/components/radio-group/radio-group-value.js +8 -0
  82. package/lib/module/components/radio-group/radio-group-value.js.map +1 -0
  83. package/lib/module/components/rating/index.js +7 -4
  84. package/lib/module/components/rating/index.js.map +1 -1
  85. package/lib/module/components/rating/rating-inputs.js +15 -0
  86. package/lib/module/components/rating/rating-inputs.js.map +1 -0
  87. package/lib/module/components/search-bar/index.js +331 -0
  88. package/lib/module/components/search-bar/index.js.map +1 -0
  89. package/lib/module/components/search-bar/search-bar-debounce.js +23 -0
  90. package/lib/module/components/search-bar/search-bar-debounce.js.map +1 -0
  91. package/lib/module/components/select/index.js +9 -1
  92. package/lib/module/components/select/index.js.map +1 -1
  93. package/lib/module/components/select/native-select-contract.js +11 -0
  94. package/lib/module/components/select/native-select-contract.js.map +1 -0
  95. package/lib/module/components/split-view/index.js +439 -0
  96. package/lib/module/components/split-view/index.js.map +1 -0
  97. package/lib/module/components/split-view/split-view-math.js +132 -0
  98. package/lib/module/components/split-view/split-view-math.js.map +1 -0
  99. package/lib/module/components/splitter/index.js +433 -0
  100. package/lib/module/components/splitter/index.js.map +1 -0
  101. package/lib/module/components/splitter/splitter-math.js +206 -0
  102. package/lib/module/components/splitter/splitter-math.js.map +1 -0
  103. package/lib/module/components/steps/index.js +16 -5
  104. package/lib/module/components/steps/index.js.map +1 -1
  105. package/lib/module/components/steps/steps-trigger.js +6 -0
  106. package/lib/module/components/steps/steps-trigger.js.map +1 -0
  107. package/lib/module/components/table/index.js +3 -4
  108. package/lib/module/components/table/index.js.map +1 -1
  109. package/lib/module/components/tag-input/index.js +15 -10
  110. package/lib/module/components/tag-input/index.js.map +1 -1
  111. package/lib/module/components/tag-input/tag-input-state.js +14 -0
  112. package/lib/module/components/tag-input/tag-input-state.js.map +1 -0
  113. package/lib/module/components/textarea/index.js +9 -4
  114. package/lib/module/components/textarea/index.js.map +1 -1
  115. package/lib/module/components/textarea/textarea-disabled.js +6 -0
  116. package/lib/module/components/textarea/textarea-disabled.js.map +1 -0
  117. package/lib/module/components/timeline/index.js +352 -47
  118. package/lib/module/components/timeline/index.js.map +1 -1
  119. package/lib/module/components/timeline/timeline-geometry.js +11 -0
  120. package/lib/module/components/timeline/timeline-geometry.js.map +1 -0
  121. package/lib/module/components/toast/index.js +18 -1
  122. package/lib/module/components/toast/index.js.map +1 -1
  123. package/lib/module/components/toast/toast-store.js +36 -4
  124. package/lib/module/components/toast/toast-store.js.map +1 -1
  125. package/lib/module/components/toggle-button/index.js +1 -1
  126. package/lib/module/components/toggle-button/index.js.map +1 -1
  127. package/lib/module/components/tour/index.js +4 -2
  128. package/lib/module/components/tour/index.js.map +1 -1
  129. package/lib/module/components/tour/tour-card-measurement.js +24 -0
  130. package/lib/module/components/tour/tour-card-measurement.js.map +1 -0
  131. package/lib/module/components/tree/index.js +13 -8
  132. package/lib/module/components/tree/index.js.map +1 -1
  133. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  134. package/lib/module/index.js +3 -0
  135. package/lib/module/index.js.map +1 -1
  136. package/lib/module/utils/chart.js +50 -7
  137. package/lib/module/utils/chart.js.map +1 -1
  138. package/lib/typescript/src/components/accordion/index.d.ts +2 -2
  139. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  140. package/lib/typescript/src/components/area-chart/index.d.ts.map +1 -1
  141. package/lib/typescript/src/components/avatar/avatar-group.d.ts +46 -0
  142. package/lib/typescript/src/components/avatar/avatar-group.d.ts.map +1 -0
  143. package/lib/typescript/src/components/avatar/index.d.ts +23 -1
  144. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  145. package/lib/typescript/src/components/breadcrumb/index.d.ts.map +1 -1
  146. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  147. package/lib/typescript/src/components/code-block/index.d.ts +3 -3
  148. package/lib/typescript/src/components/code-block/index.d.ts.map +1 -1
  149. package/lib/typescript/src/components/collapsible/index.d.ts +2 -2
  150. package/lib/typescript/src/components/collapsible/index.d.ts.map +1 -1
  151. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts +8 -0
  152. package/lib/typescript/src/components/color-picker/color-picker-accessibility.d.ts.map +1 -0
  153. package/lib/typescript/src/components/color-picker/index.d.ts.map +1 -1
  154. package/lib/typescript/src/components/date-time-picker/index.d.ts.map +1 -1
  155. package/lib/typescript/src/components/drawer/index.d.ts.map +1 -1
  156. package/lib/typescript/src/components/fab/index.d.ts +4 -2
  157. package/lib/typescript/src/components/fab/index.d.ts.map +1 -1
  158. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -1
  159. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -1
  160. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  161. package/lib/typescript/src/components/input/input-content-padding.d.ts +2 -0
  162. package/lib/typescript/src/components/input/input-content-padding.d.ts.map +1 -0
  163. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  164. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts +16 -0
  165. package/lib/typescript/src/components/input-group/input-group-measurements.d.ts.map +1 -0
  166. package/lib/typescript/src/components/line-chart/index.d.ts.map +1 -1
  167. package/lib/typescript/src/components/marquee/index.d.ts +1 -1
  168. package/lib/typescript/src/components/marquee/index.d.ts.map +1 -1
  169. package/lib/typescript/src/components/marquee/marquee-math.d.ts +21 -0
  170. package/lib/typescript/src/components/marquee/marquee-math.d.ts.map +1 -0
  171. package/lib/typescript/src/components/menu/index.d.ts +1 -1
  172. package/lib/typescript/src/components/menu/index.d.ts.map +1 -1
  173. package/lib/typescript/src/components/otp-input/index.d.ts.map +1 -1
  174. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts +3 -0
  175. package/lib/typescript/src/components/otp-input/otp-input-state.d.ts.map +1 -0
  176. package/lib/typescript/src/components/pagination/index.d.ts.map +1 -1
  177. package/lib/typescript/src/components/panelside/index.d.ts +1 -1
  178. package/lib/typescript/src/components/panelside/index.d.ts.map +1 -1
  179. package/lib/typescript/src/components/plan/index.d.ts +2 -2
  180. package/lib/typescript/src/components/planner/index.d.ts.map +1 -1
  181. package/lib/typescript/src/components/planner/planner-today.d.ts +2 -0
  182. package/lib/typescript/src/components/planner/planner-today.d.ts.map +1 -0
  183. package/lib/typescript/src/components/plot/index.d.ts +60 -6
  184. package/lib/typescript/src/components/plot/index.d.ts.map +1 -1
  185. package/lib/typescript/src/components/plot/plot-series-registry.d.ts +8 -0
  186. package/lib/typescript/src/components/plot/plot-series-registry.d.ts.map +1 -0
  187. package/lib/typescript/src/components/radio-group/index.d.ts.map +1 -1
  188. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts +2 -0
  189. package/lib/typescript/src/components/radio-group/radio-group-value.d.ts.map +1 -0
  190. package/lib/typescript/src/components/rating/index.d.ts.map +1 -1
  191. package/lib/typescript/src/components/rating/rating-inputs.d.ts +4 -0
  192. package/lib/typescript/src/components/rating/rating-inputs.d.ts.map +1 -0
  193. package/lib/typescript/src/components/search-bar/index.d.ts +216 -0
  194. package/lib/typescript/src/components/search-bar/index.d.ts.map +1 -0
  195. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts +7 -0
  196. package/lib/typescript/src/components/search-bar/search-bar-debounce.d.ts.map +1 -0
  197. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  198. package/lib/typescript/src/components/select/native-select-contract.d.ts +11 -0
  199. package/lib/typescript/src/components/select/native-select-contract.d.ts.map +1 -0
  200. package/lib/typescript/src/components/split-view/index.d.ts +159 -0
  201. package/lib/typescript/src/components/split-view/index.d.ts.map +1 -0
  202. package/lib/typescript/src/components/split-view/split-view-math.d.ts +54 -0
  203. package/lib/typescript/src/components/split-view/split-view-math.d.ts.map +1 -0
  204. package/lib/typescript/src/components/splitter/index.d.ts +78 -0
  205. package/lib/typescript/src/components/splitter/index.d.ts.map +1 -0
  206. package/lib/typescript/src/components/splitter/splitter-math.d.ts +67 -0
  207. package/lib/typescript/src/components/splitter/splitter-math.d.ts.map +1 -0
  208. package/lib/typescript/src/components/steps/index.d.ts +2 -2
  209. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  210. package/lib/typescript/src/components/steps/steps-trigger.d.ts +2 -0
  211. package/lib/typescript/src/components/steps/steps-trigger.d.ts.map +1 -0
  212. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -1
  213. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts +12 -0
  214. package/lib/typescript/src/components/tag-input/tag-input-state.d.ts.map +1 -0
  215. package/lib/typescript/src/components/textarea/index.d.ts.map +1 -1
  216. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts +2 -0
  217. package/lib/typescript/src/components/textarea/textarea-disabled.d.ts.map +1 -0
  218. package/lib/typescript/src/components/timeline/index.d.ts +36 -2
  219. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -1
  220. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts +4 -0
  221. package/lib/typescript/src/components/timeline/timeline-geometry.d.ts.map +1 -0
  222. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  223. package/lib/typescript/src/components/toast/toast-store.d.ts +7 -2
  224. package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -1
  225. package/lib/typescript/src/components/tour/index.d.ts.map +1 -1
  226. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts +14 -0
  227. package/lib/typescript/src/components/tour/tour-card-measurement.d.ts.map +1 -0
  228. package/lib/typescript/src/components/tree/index.d.ts +3 -4
  229. package/lib/typescript/src/components/tree/index.d.ts.map +1 -1
  230. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +2 -2
  231. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  232. package/lib/typescript/src/index.d.ts +5 -2
  233. package/lib/typescript/src/index.d.ts.map +1 -1
  234. package/lib/typescript/src/utils/chart.d.ts +28 -3
  235. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  236. package/package.json +1 -1
  237. package/src/components/accordion/index.tsx +17 -7
  238. package/src/components/area-chart/index.tsx +8 -0
  239. package/src/components/avatar/avatar-group.ts +78 -0
  240. package/src/components/avatar/index.tsx +121 -1
  241. package/src/components/breadcrumb/index.tsx +2 -1
  242. package/src/components/button/index.tsx +3 -2
  243. package/src/components/carousel/index.tsx +1 -1
  244. package/src/components/chip/index.tsx +2 -2
  245. package/src/components/code-block/index.tsx +3 -3
  246. package/src/components/collapsible/index.tsx +13 -4
  247. package/src/components/color-picker/color-picker-accessibility.ts +12 -0
  248. package/src/components/color-picker/index.tsx +30 -10
  249. package/src/components/date-time-picker/index.tsx +11 -3
  250. package/src/components/drawer/index.tsx +42 -14
  251. package/src/components/fab/index.tsx +21 -11
  252. package/src/components/flow/index.tsx +1 -1
  253. package/src/components/frame/index.tsx +2 -2
  254. package/src/components/grid-item/index.tsx +2 -2
  255. package/src/components/heatmap-chart/index.tsx +7 -1
  256. package/src/components/hex-chart/index.tsx +7 -1
  257. package/src/components/input/index.tsx +6 -2
  258. package/src/components/input/input-content-padding.ts +3 -0
  259. package/src/components/input-group/index.tsx +45 -12
  260. package/src/components/input-group/input-group-measurements.ts +98 -0
  261. package/src/components/item/index.tsx +2 -2
  262. package/src/components/line-chart/index.tsx +9 -0
  263. package/src/components/marker/index.tsx +2 -2
  264. package/src/components/marquee/index.tsx +14 -12
  265. package/src/components/marquee/marquee-math.ts +37 -0
  266. package/src/components/menu/index.tsx +11 -3
  267. package/src/components/otp-input/index.tsx +3 -1
  268. package/src/components/otp-input/otp-input-state.ts +7 -0
  269. package/src/components/pagination/index.tsx +26 -13
  270. package/src/components/panelside/index.tsx +7 -2
  271. package/src/components/plan/index.tsx +2 -2
  272. package/src/components/planner/index.tsx +44 -11
  273. package/src/components/planner/planner-today.ts +5 -0
  274. package/src/components/plot/index.tsx +199 -51
  275. package/src/components/plot/plot-series-registry.ts +41 -0
  276. package/src/components/post/index.tsx +4 -4
  277. package/src/components/radio-group/index.tsx +4 -1
  278. package/src/components/radio-group/radio-group-value.ts +9 -0
  279. package/src/components/rating/index.tsx +11 -4
  280. package/src/components/rating/rating-inputs.ts +19 -0
  281. package/src/components/search-bar/index.tsx +441 -0
  282. package/src/components/search-bar/search-bar-debounce.ts +35 -0
  283. package/src/components/select/index.tsx +10 -1
  284. package/src/components/select/native-select-contract.ts +12 -0
  285. package/src/components/split-view/index.tsx +565 -0
  286. package/src/components/split-view/split-view-math.ts +146 -0
  287. package/src/components/splitter/index.tsx +589 -0
  288. package/src/components/splitter/splitter-math.ts +244 -0
  289. package/src/components/steps/index.tsx +35 -9
  290. package/src/components/steps/steps-trigger.ts +6 -0
  291. package/src/components/table/index.tsx +2 -2
  292. package/src/components/tag-input/index.tsx +13 -11
  293. package/src/components/tag-input/tag-input-state.ts +18 -0
  294. package/src/components/textarea/index.tsx +8 -4
  295. package/src/components/textarea/textarea-disabled.ts +6 -0
  296. package/src/components/timeline/index.tsx +405 -46
  297. package/src/components/timeline/timeline-geometry.ts +9 -0
  298. package/src/components/toast/index.tsx +26 -1
  299. package/src/components/toast/toast-store.ts +43 -8
  300. package/src/components/toggle-button/index.tsx +1 -1
  301. package/src/components/tour/index.tsx +11 -3
  302. package/src/components/tour/tour-card-measurement.ts +29 -0
  303. package/src/components/tree/index.tsx +17 -12
  304. package/src/hooks/use-keyboard-avoidance.ts +7 -2
  305. package/src/index.ts +19 -0
  306. package/src/utils/chart.ts +66 -7
@@ -0,0 +1,565 @@
1
+ /**
2
+ * SplitView — two stacked panes whose seam settles on one of a few named
3
+ * heights rather than wherever the finger stopped.
4
+ *
5
+ * ```tsx
6
+ * <SplitView className="h-96" snapPoints={[0.25, 0.6]} minHeight={80}>
7
+ * <SplitView.Top>{map}</SplitView.Top>
8
+ * <SplitView.DragArea>
9
+ * <SplitView.Handle />
10
+ * </SplitView.DragArea>
11
+ * <SplitView.Bottom>{list}</SplitView.Bottom>
12
+ * </SplitView>
13
+ * ```
14
+ *
15
+ * The snapping is the difference between this and a free-resize split. A layout
16
+ * with a few right answers — a map over a list, a preview over an editor — is
17
+ * better served by a control that lands on one of them than by one that lets
18
+ * the reader stop three points short of it and live with the result.
19
+ *
20
+ * Snap points are ratios of the room the two panes share, so they mean the same
21
+ * thing on any screen and a rotation costs no re-measuring. That room is the
22
+ * container minus the drag area, which takes real layout height: half means
23
+ * half of what is actually divisible rather than half of a number the seam then
24
+ * eats into.
25
+ *
26
+ * The split view has no height of its own — give it one, or put it in something
27
+ * that has one, or it collapses and takes its panes with it.
28
+ *
29
+ * Dragging runs on the UI thread. `onSnap` fires once the pane has settled
30
+ * rather than on every frame, because a layout that round-trips through React
31
+ * sixty times a second is the one thing that makes this feel slow.
32
+ */
33
+ import {
34
+ createContext,
35
+ useCallback,
36
+ useContext,
37
+ useEffect,
38
+ useMemo,
39
+ useRef,
40
+ useState,
41
+ type ReactNode,
42
+ } from 'react';
43
+ import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
44
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
45
+ import Animated, {
46
+ runOnJS,
47
+ useAnimatedStyle,
48
+ useReducedMotion,
49
+ useSharedValue,
50
+ withSpring,
51
+ type SharedValue,
52
+ } from 'react-native-reanimated';
53
+ import { tv } from 'tailwind-variants';
54
+ import { selectionTick } from '../../utils/haptics';
55
+ import {
56
+ DEFAULT_MIN_HEIGHT,
57
+ clamp,
58
+ nearestSnapIndex,
59
+ normalizeSnapIndex,
60
+ resolveLength,
61
+ resolveSnapPoints,
62
+ } from './split-view-math';
63
+
64
+ /** Settles the pane onto a snap point without overshooting past its limits. */
65
+ const SPRING = {
66
+ damping: 25,
67
+ stiffness: 300,
68
+ mass: 0.8,
69
+ overshootClamping: false,
70
+ restDisplacementThreshold: 0.01,
71
+ restSpeedThreshold: 0.01,
72
+ } as const;
73
+
74
+ /** How much the grip grows while it is being dragged. */
75
+ const GRIP_SPRING = { damping: 18, stiffness: 300, mass: 0.8 } as const;
76
+
77
+ const splitViewVariants = tv({
78
+ slots: {
79
+ root: 'w-full flex-col overflow-hidden',
80
+ top: 'w-full overflow-hidden',
81
+ bottom: 'w-full flex-1 overflow-hidden',
82
+ dragArea: 'w-full items-center justify-center py-2',
83
+ handle: 'h-1 w-10 rounded-full bg-muted-foreground/30',
84
+ },
85
+ });
86
+
87
+ interface SplitViewContextValue {
88
+ /** The top pane's height in points. Written by drags, read by both panes. */
89
+ topHeight: SharedValue<number>;
90
+ /** True while a finger is on the seam. */
91
+ dragging: SharedValue<boolean>;
92
+ /** Snap heights in points, in order. Empty until the container is measured. */
93
+ points: number[];
94
+ minPx: number;
95
+ maxPx: number;
96
+ room: number;
97
+ measured: boolean;
98
+ disabled: boolean;
99
+ snapIndex: number;
100
+ animate: boolean;
101
+ snapTo: (index: number) => void;
102
+ measureDragArea: (height: number) => void;
103
+ dragAreaHeight: number;
104
+ }
105
+
106
+ const SplitViewContext = createContext<SplitViewContextValue | null>(null);
107
+
108
+ function useSplitViewContext(component: string): SplitViewContextValue {
109
+ const context = useContext(SplitViewContext);
110
+ if (!context) throw new Error(`${component} must be used within a <SplitView>`);
111
+ return context;
112
+ }
113
+
114
+ /**
115
+ * Reads the live layout from inside a split view, and moves the seam.
116
+ *
117
+ * `topHeight` is a shared value on the UI thread — read it in a worklet, not in
118
+ * render, where it is only ever the number the last commit happened to see.
119
+ */
120
+ export function useSplitView() {
121
+ return useSplitViewContext('useSplitView');
122
+ }
123
+
124
+ export interface SplitViewProps extends ViewProps {
125
+ className?: string;
126
+ /**
127
+ * Heights the seam settles on. A number at or below `1` is a fraction of the
128
+ * room the panes share; anything larger is points. Defaults to
129
+ * `[0.2, 0.5, 0.8]`.
130
+ */
131
+ snapPoints?: readonly number[];
132
+ /** Smallest the top pane may get, as a fraction or in points. Defaults to `100`. */
133
+ minHeight?: number;
134
+ /**
135
+ * Largest the top pane may get, as a fraction or in points. A negative number
136
+ * is measured back from the bottom — `-80` leaves eighty points for the other
137
+ * pane. Defaults to all the room there is.
138
+ */
139
+ maxHeight?: number;
140
+ /** Which snap point the seam starts at when uncontrolled. Defaults to `1`. */
141
+ defaultSnapIndex?: number;
142
+ /** Controlled snap index. Pair it with `onSnapIndexChange`. */
143
+ snapIndex?: number;
144
+ /** Called with the index the seam settled on. */
145
+ onSnapIndexChange?: (index: number) => void;
146
+ /** Called once the pane has settled, with the index and its height in points. */
147
+ onSnap?: (index: number, topHeight: number) => void;
148
+ /** Freezes the seam. The panes keep the heights they have. */
149
+ disabled?: boolean;
150
+ /** Springs to the starting snap point on mount instead of opening at it. */
151
+ animateOnMount?: boolean;
152
+ children?: ReactNode;
153
+ }
154
+
155
+ function SplitViewRoot({
156
+ className,
157
+ snapPoints,
158
+ minHeight,
159
+ maxHeight,
160
+ defaultSnapIndex = 1,
161
+ snapIndex: snapIndexProp,
162
+ onSnapIndexChange,
163
+ onSnap,
164
+ disabled = false,
165
+ animateOnMount = false,
166
+ children,
167
+ onLayout,
168
+ ...props
169
+ }: SplitViewProps) {
170
+ const { root } = splitViewVariants();
171
+ const reducedMotion = useReducedMotion();
172
+ const animate = !reducedMotion;
173
+
174
+ const [container, setContainer] = useState(0);
175
+ const [dragAreaHeight, setDragAreaHeight] = useState(0);
176
+
177
+ const topHeight = useSharedValue(0);
178
+ const dragging = useSharedValue(false);
179
+ const settled = useRef(false);
180
+
181
+ const room = Math.max(container - dragAreaHeight, 0);
182
+ const minPx = clamp(resolveLength(minHeight, room, DEFAULT_MIN_HEIGHT), 0, room);
183
+ const maxPx = clamp(resolveLength(maxHeight, room, room), minPx, room);
184
+
185
+ const pointsKey = JSON.stringify([snapPoints ?? null, room, minPx, maxPx]);
186
+ const points = useMemo(
187
+ () => (room > 0 ? resolveSnapPoints(snapPoints, room, minPx, maxPx) : []),
188
+ // eslint-disable-next-line react-hooks/exhaustive-deps
189
+ [pointsKey]
190
+ );
191
+
192
+ const controlled = snapIndexProp !== undefined;
193
+ const [internalIndex, setInternalIndex] = useState(() =>
194
+ normalizeSnapIndex(defaultSnapIndex, 1)
195
+ );
196
+ const snapIndex = normalizeSnapIndex(
197
+ controlled ? snapIndexProp : internalIndex,
198
+ points.length || 1
199
+ );
200
+
201
+ const onSnapRef = useRef(onSnap);
202
+ onSnapRef.current = onSnap;
203
+
204
+ /** Reports a settled pane once, from the spring's own completion. */
205
+ const report = useCallback((index: number, height: number) => {
206
+ onSnapRef.current?.(index, height);
207
+ }, []);
208
+
209
+ /*
210
+ * Moving the seam is one function whether a drag, a press or a screen reader
211
+ * asked for it — the alternative is three places that each have to remember
212
+ * to spring, to report, and to leave a controlled caller's index alone.
213
+ */
214
+ const moveTo = useCallback(
215
+ (index: number, options?: { animated?: boolean }) => {
216
+ const target = points[index];
217
+ if (target === undefined) return;
218
+
219
+ if (!controlled) setInternalIndex(index);
220
+ onSnapIndexChange?.(index);
221
+
222
+ const finish = () => report(index, target);
223
+ if (options?.animated === false || !animate) {
224
+ topHeight.value = target;
225
+ finish();
226
+ return;
227
+ }
228
+ topHeight.value = withSpring(target, SPRING, (finished) => {
229
+ 'worklet';
230
+ if (finished) runOnJS(finish)();
231
+ });
232
+ },
233
+ [animate, controlled, onSnapIndexChange, points, report, topHeight]
234
+ );
235
+
236
+ const snapTo = useCallback((index: number) => moveTo(index), [moveTo]);
237
+
238
+ /*
239
+ * The first frame with a measurement is the only one that may place the pane
240
+ * without animating: there is nothing on screen yet to animate from, and a
241
+ * spring out of zero reads as the layout arriving broken and correcting
242
+ * itself. `animateOnMount` is for when that arrival is the point.
243
+ */
244
+ useEffect(() => {
245
+ const target = points[snapIndex];
246
+ if (target === undefined) return;
247
+
248
+ if (!settled.current) {
249
+ settled.current = true;
250
+ if (!animateOnMount) {
251
+ topHeight.value = target;
252
+ report(snapIndex, target);
253
+ return;
254
+ }
255
+ }
256
+
257
+ if (dragging.value) return;
258
+ if (!animate) {
259
+ topHeight.value = target;
260
+ report(snapIndex, target);
261
+ return;
262
+ }
263
+ topHeight.value = withSpring(target, SPRING);
264
+ // Only when the resolved layout moves — a re-render that changes nothing
265
+ // must not restart a spring the reader is watching.
266
+ // eslint-disable-next-line react-hooks/exhaustive-deps
267
+ }, [points, snapIndex, animate, animateOnMount]);
268
+
269
+ const handleLayout = useCallback(
270
+ (event: LayoutChangeEvent) => {
271
+ setContainer(event.nativeEvent.layout.height);
272
+ onLayout?.(event);
273
+ },
274
+ [onLayout]
275
+ );
276
+
277
+ const measureDragArea = useCallback((height: number) => {
278
+ setDragAreaHeight((current) => (Math.abs(current - height) < 0.5 ? current : height));
279
+ }, []);
280
+
281
+ const context = useMemo<SplitViewContextValue>(
282
+ () => ({
283
+ topHeight,
284
+ dragging,
285
+ points,
286
+ minPx,
287
+ maxPx,
288
+ room,
289
+ measured: room > 0,
290
+ disabled,
291
+ snapIndex,
292
+ animate,
293
+ snapTo,
294
+ measureDragArea,
295
+ dragAreaHeight,
296
+ }),
297
+ [
298
+ topHeight,
299
+ dragging,
300
+ points,
301
+ minPx,
302
+ maxPx,
303
+ room,
304
+ disabled,
305
+ snapIndex,
306
+ animate,
307
+ snapTo,
308
+ measureDragArea,
309
+ dragAreaHeight,
310
+ ]
311
+ );
312
+
313
+ return (
314
+ <SplitViewContext.Provider value={context}>
315
+ <View className={root({ className })} onLayout={handleLayout} {...props}>
316
+ {children}
317
+ </View>
318
+ </SplitViewContext.Provider>
319
+ );
320
+ }
321
+
322
+ export interface SplitViewPaneProps extends ViewProps {
323
+ className?: string;
324
+ children?: ReactNode;
325
+ }
326
+
327
+ /**
328
+ * The upper pane.
329
+ *
330
+ * It clips what is inside it, so a pane dragged short hides its content rather
331
+ * than pushing it through the seam. Content that outgrows it is the caller's to
332
+ * scroll — put a `ScrollView` in here and it behaves like any other scroller in
333
+ * a box whose height changes.
334
+ */
335
+ function SplitViewTop({ className, children, style, ...props }: SplitViewPaneProps) {
336
+ const context = useSplitViewContext('SplitView.Top');
337
+ const { top } = splitViewVariants();
338
+
339
+ const animatedStyle = useAnimatedStyle(() => ({
340
+ height: context.topHeight.value,
341
+ }));
342
+
343
+ /*
344
+ * Before the container is measured there is no height to give, and a pane
345
+ * sized from a container of zero is a pane nobody can see. A flex basis of
346
+ * zero and a grow of one carries the first frame instead, so the pane is
347
+ * already the shape it will be when the measurement lands.
348
+ */
349
+ const unmeasured = { flexGrow: 1, flexBasis: 0, flexShrink: 1 };
350
+
351
+ return (
352
+ <Animated.View
353
+ className={top({ className })}
354
+ style={[context.measured ? null : unmeasured, style, animatedStyle]}
355
+ {...props}
356
+ >
357
+ {children}
358
+ </Animated.View>
359
+ );
360
+ }
361
+
362
+ /**
363
+ * The lower pane. It takes exactly the room the upper one gave up, with no
364
+ * second measurement — which is what keeps the two adding to the container on
365
+ * every frame of a drag rather than only at rest.
366
+ */
367
+ function SplitViewBottom({ className, children, ...props }: SplitViewPaneProps) {
368
+ useSplitViewContext('SplitView.Bottom');
369
+ const { bottom } = splitViewVariants();
370
+
371
+ return (
372
+ <View className={bottom({ className })} {...props}>
373
+ {children}
374
+ </View>
375
+ );
376
+ }
377
+
378
+ export interface SplitViewDragAreaProps extends ViewProps {
379
+ className?: string;
380
+ /** What a screen reader calls the seam. Defaults to "Resize panes". */
381
+ accessibilityLabel?: string;
382
+ children?: ReactNode;
383
+ }
384
+
385
+ /**
386
+ * The seam, and the target for the drag.
387
+ *
388
+ * It takes real layout height rather than floating over the panes, because that
389
+ * height is what the snap points are fractions of: a finger-sized target that
390
+ * did not take room would make `0.5` mean half of a number the seam then ate
391
+ * into. Give it padding to make the target larger — the room it takes is
392
+ * measured, so the arithmetic follows.
393
+ */
394
+ function SplitViewDragArea({
395
+ className,
396
+ accessibilityLabel = 'Resize panes',
397
+ children,
398
+ onLayout,
399
+ ...props
400
+ }: SplitViewDragAreaProps) {
401
+ const context = useSplitViewContext('SplitView.DragArea');
402
+ const { dragArea } = splitViewVariants();
403
+ const { topHeight, dragging, points, minPx, maxPx, room, disabled, snapTo } = context;
404
+
405
+ const start = useSharedValue(0);
406
+ const landed = useSharedValue(-1);
407
+
408
+ const settle = useCallback((index: number) => snapTo(index), [snapTo]);
409
+
410
+ const pan = useMemo(
411
+ () =>
412
+ Gesture.Pan()
413
+ .enabled(!disabled && points.length > 0)
414
+ // The seam only answers to the axis it moves on, so a split view inside
415
+ // a scroller leaves the scroll alone.
416
+ .activeOffsetY([-8, 8])
417
+ .failOffsetX([-16, 16])
418
+ .onBegin(() => {
419
+ start.value = topHeight.value;
420
+ dragging.value = true;
421
+ landed.value = -1;
422
+ })
423
+ .onUpdate((event) => {
424
+ if (room <= 0) return;
425
+ topHeight.value = clamp(start.value + event.translationY, minPx, maxPx);
426
+
427
+ // The moment worth feeling: passing the point the pane would settle
428
+ // at if the finger let go now. It is the only feedback that says a
429
+ // drag has a destination rather than a position.
430
+ const next = nearestSnapIndex(topHeight.value, points, 0, room);
431
+ if (next !== landed.value) {
432
+ landed.value = next;
433
+ runOnJS(selectionTick)();
434
+ }
435
+ })
436
+ .onFinalize((event) => {
437
+ dragging.value = false;
438
+ if (room <= 0) return;
439
+ const index = nearestSnapIndex(
440
+ topHeight.value,
441
+ points,
442
+ event.velocityY ?? 0,
443
+ room
444
+ );
445
+ runOnJS(settle)(index);
446
+ }),
447
+ [disabled, dragging, landed, maxPx, minPx, points, room, settle, start, topHeight]
448
+ );
449
+
450
+ const handleLayout = useCallback(
451
+ (event: LayoutChangeEvent) => {
452
+ context.measureDragArea(event.nativeEvent.layout.height);
453
+ onLayout?.(event);
454
+ },
455
+ [context, onLayout]
456
+ );
457
+
458
+ /*
459
+ * A step moves to the next snap point rather than by a distance, because the
460
+ * snap points are the positions this control has. Announcing a percentage
461
+ * somebody cannot stop at would be describing a different control.
462
+ */
463
+ const step = useCallback(
464
+ (direction: 1 | -1) => {
465
+ const next = clamp(context.snapIndex + direction, 0, points.length - 1);
466
+ if (next !== context.snapIndex) snapTo(next);
467
+ },
468
+ [context.snapIndex, points.length, snapTo]
469
+ );
470
+
471
+ return (
472
+ <GestureDetector gesture={pan}>
473
+ <View
474
+ accessible
475
+ accessibilityRole="adjustable"
476
+ accessibilityLabel={accessibilityLabel}
477
+ accessibilityState={{ disabled }}
478
+ accessibilityValue={{
479
+ min: 0,
480
+ max: Math.max(points.length - 1, 0),
481
+ now: context.snapIndex,
482
+ }}
483
+ accessibilityActions={
484
+ disabled ? undefined : [{ name: 'increment' }, { name: 'decrement' }]
485
+ }
486
+ onAccessibilityAction={
487
+ disabled
488
+ ? undefined
489
+ : (event) => {
490
+ if (event.nativeEvent.actionName === 'increment') step(1);
491
+ else if (event.nativeEvent.actionName === 'decrement') step(-1);
492
+ }
493
+ }
494
+ className={dragArea({ className })}
495
+ onLayout={handleLayout}
496
+ {...props}
497
+ >
498
+ {children}
499
+ </View>
500
+ </GestureDetector>
501
+ );
502
+ }
503
+
504
+ export interface SplitViewHandleProps extends ViewProps {
505
+ className?: string;
506
+ children?: ReactNode;
507
+ }
508
+
509
+ /**
510
+ * The grip inside the drag area. It grows a little while the seam is moving,
511
+ * which is the only thing on screen saying the gesture was received before the
512
+ * panes have moved far enough to say it themselves.
513
+ */
514
+ function SplitViewHandle({ className, children, style, ...props }: SplitViewHandleProps) {
515
+ const context = useSplitViewContext('SplitView.Handle');
516
+ const { handle } = splitViewVariants();
517
+ const { animate } = context;
518
+
519
+ const animatedStyle = useAnimatedStyle(() => {
520
+ if (!animate) return { transform: [{ scaleX: 1 }] };
521
+ return {
522
+ transform: [
523
+ { scaleX: withSpring(context.dragging.value ? 1.15 : 1, GRIP_SPRING) },
524
+ ],
525
+ };
526
+ });
527
+
528
+ if (children) {
529
+ return (
530
+ <Animated.View style={[style, animatedStyle]} {...props}>
531
+ {children}
532
+ </Animated.View>
533
+ );
534
+ }
535
+
536
+ return (
537
+ <Animated.View
538
+ className={handle({ className })}
539
+ style={[style, animatedStyle]}
540
+ {...props}
541
+ />
542
+ );
543
+ }
544
+
545
+ SplitViewRoot.displayName = 'SplitView';
546
+ SplitViewTop.displayName = 'SplitView.Top';
547
+ SplitViewBottom.displayName = 'SplitView.Bottom';
548
+ SplitViewDragArea.displayName = 'SplitView.DragArea';
549
+ SplitViewHandle.displayName = 'SplitView.Handle';
550
+
551
+ export const SplitView = Object.assign(SplitViewRoot, {
552
+ Top: SplitViewTop,
553
+ Bottom: SplitViewBottom,
554
+ DragArea: SplitViewDragArea,
555
+ Handle: SplitViewHandle,
556
+ });
557
+
558
+ export {
559
+ DEFAULT_MIN_HEIGHT,
560
+ DEFAULT_SNAP_POINTS,
561
+ nearestSnapIndex,
562
+ normalizeSnapIndex,
563
+ resolveLength,
564
+ resolveSnapPoints,
565
+ } from './split-view-math';
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The arithmetic a split view runs while its seam is under a finger.
3
+ *
4
+ * A split view differs from a free-resize splitter in one way that decides the
5
+ * whole of this file: it does not stop wherever the finger stopped. It settles
6
+ * on one of a few heights the caller named, so the numbers a drag produces are
7
+ * only ever on the way to one of those — and picking which one, from a position
8
+ * and the speed it arrived at, is the part with the edge cases.
9
+ *
10
+ * It lives apart from the component because it runs on the UI thread, where a
11
+ * wrong answer is a pane that sticks rather than an exception anybody sees.
12
+ */
13
+
14
+ /** Smallest top pane, in points, when the caller names none. */
15
+ export const DEFAULT_MIN_HEIGHT = 100;
16
+
17
+ /** Where the panes sit when the caller names no snap points, as ratios. */
18
+ export const DEFAULT_SNAP_POINTS = [0.2, 0.5, 0.8] as const;
19
+
20
+ /**
21
+ * How much of a flick counts toward the next snap point, in points per point of
22
+ * velocity. A release is at the height the finger left *plus* where the throw
23
+ * was going, so a fast flick carries past a midpoint the finger never crossed —
24
+ * which is the difference between a control that follows a gesture and one that
25
+ * merely records where it ended.
26
+ */
27
+ const VELOCITY_REACH = 0.12;
28
+
29
+ /** How far a flick may reach on its own, as a fraction of the room available. */
30
+ const VELOCITY_CEILING = 0.4;
31
+
32
+ /** The nearest entry to a height, by index. */
33
+ function closestIndex(height: number, points: readonly number[]): number {
34
+ 'worklet';
35
+ let best = 0;
36
+ let bestDistance = Math.abs(points[0]! - height);
37
+ for (let index = 1; index < points.length; index += 1) {
38
+ const distance = Math.abs(points[index]! - height);
39
+ if (distance < bestDistance) {
40
+ best = index;
41
+ bestDistance = distance;
42
+ }
43
+ }
44
+ return best;
45
+ }
46
+
47
+ export function clamp(value: number, low: number, high: number): number {
48
+ 'worklet';
49
+ return Math.min(Math.max(value, low), high);
50
+ }
51
+
52
+ /**
53
+ * One length, in points, from the two ways of writing it.
54
+ *
55
+ * A number at or below 1 is a fraction of the room the panes share, which is
56
+ * what survives a rotation: half is half at any screen size. Anything larger is
57
+ * already points. A negative number is measured back from the far edge, for the
58
+ * case that is always written as a subtraction — "everything but the last 80".
59
+ */
60
+ export function resolveLength(
61
+ value: number | undefined,
62
+ room: number,
63
+ fallback: number
64
+ ): number {
65
+ 'worklet';
66
+ if (typeof value !== 'number' || !Number.isFinite(value)) return fallback;
67
+ if (value < 0) return room + value;
68
+ return value <= 1 ? value * room : value;
69
+ }
70
+
71
+ /**
72
+ * The heights a seam may settle at, in points, in order and without repeats.
73
+ *
74
+ * Every point is clamped into the range the panes are allowed, which is what
75
+ * lets a caller name ratios without also checking them against a minimum they
76
+ * did not choose. Two points that clamp onto each other become one, because a
77
+ * list with the same height twice makes a flick land on a snap that looks like
78
+ * it did nothing.
79
+ */
80
+ export function resolveSnapPoints(
81
+ points: readonly number[] | undefined,
82
+ room: number,
83
+ minPx: number,
84
+ maxPx: number
85
+ ): number[] {
86
+ 'worklet';
87
+ const source = points && points.length > 0 ? points : DEFAULT_SNAP_POINTS;
88
+ const resolved: number[] = [];
89
+
90
+ for (let index = 0; index < source.length; index += 1) {
91
+ const raw = source[index]!;
92
+ if (!Number.isFinite(raw)) continue;
93
+ const height = clamp(resolveLength(raw, room, minPx), minPx, maxPx);
94
+ let seen = false;
95
+ for (let other = 0; other < resolved.length; other += 1) {
96
+ if (Math.abs(resolved[other]! - height) < 0.5) seen = true;
97
+ }
98
+ if (!seen) resolved.push(height);
99
+ }
100
+
101
+ if (resolved.length === 0) resolved.push(clamp(minPx, minPx, maxPx));
102
+ resolved.sort((a, b) => a - b);
103
+ return resolved;
104
+ }
105
+
106
+ /**
107
+ * Which snap point a release belongs to.
108
+ *
109
+ * The height it is measured against is where the pane is plus where the throw
110
+ * was going, so a fast flick carries past a midpoint the finger never crossed —
111
+ * the difference between a control that follows a gesture and one that merely
112
+ * records where it ended.
113
+ *
114
+ * The result is then held to one point either side of where the pane actually
115
+ * is. Capping the distance is not enough on its own: the same throw skips two
116
+ * points in a list packed close together and none in a list spread wide, so the
117
+ * guarantee has to be made in points rather than in pixels. A release that
118
+ * lands two snaps from where it was aimed reads as the control guessing.
119
+ */
120
+ export function nearestSnapIndex(
121
+ height: number,
122
+ points: readonly number[],
123
+ velocity: number,
124
+ room: number
125
+ ): number {
126
+ 'worklet';
127
+ if (points.length === 0) return 0;
128
+
129
+ const reach = clamp(
130
+ velocity * VELOCITY_REACH,
131
+ -room * VELOCITY_CEILING,
132
+ room * VELOCITY_CEILING
133
+ );
134
+
135
+ const resting = closestIndex(height, points);
136
+ const thrown = closestIndex(height + reach, points);
137
+ return clamp(thrown, resting - 1, resting + 1);
138
+ }
139
+
140
+ /** The index a caller asked to start at, kept inside the list that exists. */
141
+ export function normalizeSnapIndex(index: number | undefined, length: number): number {
142
+ 'worklet';
143
+ if (length === 0) return 0;
144
+ if (typeof index !== 'number' || !Number.isFinite(index)) return 0;
145
+ return clamp(Math.round(index), 0, length - 1);
146
+ }