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
@@ -11,8 +11,18 @@
11
11
  * attribute selectors for a stylesheet to key off. `Steps` resolves its states
12
12
  * the same way, so the two read as siblings.
13
13
  *
14
- * Vertical only — a horizontal timeline gives each item roughly a fifth of a
15
- * phone's width, which is not enough for a date and a title.
14
+ * ## Two orientations
15
+ *
16
+ * Vertical is the default and the ordinary case. Horizontal lays the items out
17
+ * as columns on a rail that runs off the side of the screen and is swiped
18
+ * through, which is the arrangement a long span of time wants: a decade of
19
+ * entries read top to bottom is a page nobody reaches the end of.
20
+ *
21
+ * The objection to a horizontal timeline is that each item gets a fifth of a
22
+ * phone's width, which will not hold a date and a title. The answer is that it
23
+ * does not have to fit — the rail is wider than the screen, and a column takes
24
+ * the width its contents need. An item with nothing on it collapses to a tick,
25
+ * so a quiet stretch compresses and a busy one keeps its room.
16
26
  *
17
27
  * ```tsx
18
28
  * <Timeline variant="icon" value={2}>
@@ -34,21 +44,69 @@
34
44
  * ```
35
45
  */
36
46
  import {
47
+ Children,
37
48
  createContext,
38
49
  forwardRef,
50
+ isValidElement,
39
51
  useContext,
40
52
  useMemo,
41
53
  type ReactNode,
42
54
  } from 'react';
43
- import { View, type Text as RNText, type ViewProps } from 'react-native';
55
+ import { ScrollView, View, type Text as RNText, type ViewProps } from 'react-native';
56
+ import Animated, {
57
+ interpolate,
58
+ useAnimatedScrollHandler,
59
+ useAnimatedStyle,
60
+ useReducedMotion,
61
+ useSharedValue,
62
+ type SharedValue,
63
+ } from 'react-native-reanimated';
44
64
  import { tv } from 'tailwind-variants';
45
65
  import { useCSSVariable } from 'uniwind';
46
66
  import { IconColorProvider } from '../../icons';
47
67
  import { Text, type TextProps, textChildren } from '../../primitives/text';
68
+ import { timelineColumnWidth } from './timeline-geometry';
48
69
 
49
70
  export type TimelineVariant = 'dot' | 'icon' | 'numbered' | 'card' | 'compact';
50
71
  /** Semantic colour for a single event, independent of progress. */
51
72
  export type TimelineTone = 'default' | 'info' | 'success' | 'warning' | 'danger';
73
+ /** Which way the sequence runs. */
74
+ export type TimelineOrientation = 'vertical' | 'horizontal';
75
+
76
+ const AnimatedScrollView = Animated.createAnimatedComponent(ScrollView);
77
+
78
+ /*
79
+ * Horizontal geometry.
80
+ *
81
+ * The rail has to land on exactly the same line in every column, and the
82
+ * simplest way to guarantee that is to make the band above it a fixed height
83
+ * rather than to position the rail against measured content. So the aside is
84
+ * `HORIZONTAL_RAIL_TOP` tall in every column, the rail is drawn once across the
85
+ * whole track at that offset, and each column's tick is pulled up by half its
86
+ * own height to sit centred on it.
87
+ */
88
+ const HORIZONTAL_RAIL_TOP = 64;
89
+ const HORIZONTAL_TICK_HEIGHT = 10;
90
+
91
+ /**
92
+ * How wide a column is: what it was given, else what its contents ask for.
93
+ *
94
+ * Read by the item to size itself and by the root to build the snap offsets, so
95
+ * it lives here rather than in either — the two disagreeing would put every
96
+ * snap point half a column out.
97
+ */
98
+ function itemWidth(props: TimelineItemProps): number {
99
+ let filled = false;
100
+ Children.forEach(props.children, (child) => {
101
+ if (filled || !isValidElement(child)) return;
102
+ const displayName = (child.type as { displayName?: string })?.displayName;
103
+ if (displayName !== 'Timeline.Content') return;
104
+ const inner = (child.props as { children?: ReactNode }).children;
105
+ if (inner !== undefined && inner !== null && inner !== false) filled = true;
106
+ });
107
+
108
+ return timelineColumnWidth(props.width, filled);
109
+ }
52
110
 
53
111
  /** Variants whose node is a filled disc rather than an outlined ring. */
54
112
  const SOLID_VARIANTS: TimelineVariant[] = ['dot', 'card'];
@@ -107,11 +165,29 @@ const timelineVariants = tv({
107
165
  true: { separator: 'bg-primary' },
108
166
  false: { separator: 'bg-muted' },
109
167
  },
168
+ /*
169
+ * Horizontal turns the item from a row into a column and the rail from a
170
+ * line down the side into a line across the top. The aside is a fixed
171
+ * height because that height *is* where the rail sits — see
172
+ * HORIZONTAL_RAIL_TOP — and it is left-aligned rather than right, since a
173
+ * column reads from its own left edge rather than towards a rail beside it.
174
+ */
175
+ orientation: {
176
+ vertical: {},
177
+ horizontal: {
178
+ item: 'w-auto shrink-0 flex-col gap-0 pr-6',
179
+ aside: 'w-auto items-start justify-end gap-1 pb-2 pt-0',
180
+ rail: 'w-full items-start',
181
+ body: 'w-auto flex-none pb-0 pt-5',
182
+ title: 'text-sm',
183
+ },
184
+ },
110
185
  },
111
186
  defaultVariants: {
112
187
  variant: 'dot',
113
188
  tone: 'default',
114
189
  completed: false,
190
+ orientation: 'vertical',
115
191
  },
116
192
  });
117
193
 
@@ -169,6 +245,13 @@ const TONE_ICON_VAR: Record<TimelineTone, string> = {
169
245
  interface TimelineContextValue {
170
246
  activeStep: number;
171
247
  variant: TimelineVariant;
248
+ orientation: TimelineOrientation;
249
+ /** Horizontal only: where the track has been scrolled to, on the UI thread. */
250
+ scrollX: SharedValue<number>;
251
+ /** Horizontal only: each column's left edge, indexed by its `step`. */
252
+ offsets: number[];
253
+ /** Horizontal only: false when the reader has asked for less movement. */
254
+ animate: boolean;
172
255
  }
173
256
 
174
257
  interface TimelineItemContextValue {
@@ -177,6 +260,15 @@ interface TimelineItemContextValue {
177
260
  tone: TimelineTone;
178
261
  /** False on the last item, so its rail does not trail into nothing. */
179
262
  showSeparator: boolean;
263
+ /**
264
+ * Horizontal only: where this column starts, in points, and how wide it is.
265
+ *
266
+ * Both are worked out by the item, and the parts inside it need the same two
267
+ * numbers to fade against the scroll. Passing them down beats deriving them
268
+ * twice from measurements that would then be free to disagree.
269
+ */
270
+ offset: number;
271
+ columnWidth: number;
180
272
  }
181
273
 
182
274
  const TimelineContext = createContext<TimelineContextValue | null>(null);
@@ -199,18 +291,120 @@ export interface TimelineProps extends ViewProps {
199
291
  /** Steps at or below this index render as completed. */
200
292
  value?: number;
201
293
  variant?: TimelineVariant;
294
+ /**
295
+ * Which way the sequence runs. `horizontal` lays the items out as columns on
296
+ * a rail wider than the screen, swiped through rather than scrolled down.
297
+ */
298
+ orientation?: TimelineOrientation;
299
+ /**
300
+ * Horizontal only: land a flick on a column rather than between two.
301
+ *
302
+ * On by default, because the thing being moved between is a column — stopping
303
+ * halfway shows two half-columns and no whole one.
304
+ */
305
+ snap?: boolean;
202
306
  children?: ReactNode;
203
307
  }
204
308
 
205
309
  const TimelineRoot = forwardRef<View, TimelineProps>(
206
- ({ className, value = 0, variant = 'dot', children, ...props }, ref) => {
207
- const { root } = timelineVariants({ variant });
208
- const context = useMemo(() => ({ activeStep: value, variant }), [value, variant]);
310
+ (
311
+ {
312
+ className,
313
+ value = 0,
314
+ variant = 'dot',
315
+ orientation = 'vertical',
316
+ snap = true,
317
+ children,
318
+ ...props
319
+ },
320
+ ref
321
+ ) => {
322
+ const horizontal = orientation === 'horizontal';
323
+ const { root } = timelineVariants({ variant, orientation });
324
+ const reducedMotion = useReducedMotion();
325
+ const scrollX = useSharedValue(0);
326
+ const border = useCSSVariable('--color-border');
327
+
328
+ /*
329
+ * The columns' left edges, from the same width rule the items size
330
+ * themselves by. These are the snap points, and they are what an item reads
331
+ * to know how far it is from the reading edge.
332
+ */
333
+ const offsets = useMemo(() => {
334
+ if (!horizontal) return [];
335
+ const result: number[] = [];
336
+ let sum = 0;
337
+ Children.forEach(children, (child) => {
338
+ if (!isValidElement(child)) return;
339
+ result.push(sum);
340
+ sum += itemWidth(child.props as TimelineItemProps);
341
+ });
342
+ return result;
343
+ }, [children, horizontal]);
344
+
345
+ const context = useMemo(
346
+ () => ({
347
+ activeStep: value,
348
+ variant,
349
+ orientation,
350
+ scrollX,
351
+ offsets,
352
+ animate: !reducedMotion,
353
+ }),
354
+ [value, variant, orientation, scrollX, offsets, reducedMotion]
355
+ );
356
+
357
+ const onScroll = useAnimatedScrollHandler((event) => {
358
+ scrollX.value = event.contentOffset.x;
359
+ });
360
+
361
+ if (!horizontal) {
362
+ return (
363
+ <TimelineContext.Provider value={context}>
364
+ <View ref={ref} className={root({ className })} {...props}>
365
+ {textChildren(children)}
366
+ </View>
367
+ </TimelineContext.Provider>
368
+ );
369
+ }
209
370
 
210
371
  return (
211
372
  <TimelineContext.Provider value={context}>
212
373
  <View ref={ref} className={root({ className })} {...props}>
213
- {textChildren(children)}
374
+ <AnimatedScrollView
375
+ horizontal
376
+ onScroll={onScroll}
377
+ scrollEventThrottle={16}
378
+ showsHorizontalScrollIndicator={false}
379
+ decelerationRate="fast"
380
+ snapToAlignment="start"
381
+ snapToOffsets={snap ? offsets : undefined}
382
+ >
383
+ {/*
384
+ The track sizes itself to its columns, so the rail stretched
385
+ between its own left and right edges runs the whole length of the
386
+ sequence rather than the width of the screen.
387
+ */}
388
+ <View className="relative flex-row">
389
+ <View
390
+ accessibilityElementsHidden
391
+ importantForAccessibility="no-hide-descendants"
392
+ pointerEvents="none"
393
+ style={{
394
+ position: 'absolute',
395
+ left: 0,
396
+ right: 0,
397
+ top: HORIZONTAL_RAIL_TOP,
398
+ borderTopWidth: 1,
399
+ // Dashed borders only render at a radius of zero, which this
400
+ // has, and the rule is the whole element.
401
+ borderStyle: 'dashed',
402
+ borderColor: typeof border === 'string' ? border : undefined,
403
+ }}
404
+ />
405
+ {textChildren(children)}
406
+ </View>
407
+ </AnimatedScrollView>
214
408
  </View>
215
409
  </TimelineContext.Provider>
216
410
  );
@@ -228,28 +422,108 @@ export interface TimelineItemProps extends ViewProps {
228
422
  tone?: TimelineTone;
229
423
  /** Set on the final item so its rail stops at the indicator. */
230
424
  last?: boolean;
425
+ /**
426
+ * Horizontal only: how wide this column is, in points.
427
+ *
428
+ * Left out, a column that carries content takes a readable width and one that
429
+ * carries none collapses to a tick — so a quiet stretch of the sequence
430
+ * compresses instead of paying full width for nothing. Set it to override
431
+ * that for a column that needs more or less room than its contents suggest.
432
+ * It must be finite and greater than zero; invalid values use the content default.
433
+ */
434
+ width?: number;
231
435
  children?: ReactNode;
232
436
  }
233
437
 
234
438
  const TimelineItem = forwardRef<View, TimelineItemProps>(
235
439
  (
236
- { className, step, completed, tone = 'default', last = false, children, ...props },
440
+ {
441
+ className,
442
+ step,
443
+ completed,
444
+ tone = 'default',
445
+ last = false,
446
+ width,
447
+ style,
448
+ children,
449
+ ...props
450
+ },
237
451
  ref
238
452
  ) => {
239
- const { activeStep, variant } = useTimeline('Timeline.Item');
453
+ const { activeStep, variant, orientation, scrollX, offsets, animate } =
454
+ useTimeline('Timeline.Item');
240
455
  const isCompleted = completed ?? step <= activeStep;
241
- const { item } = timelineVariants({ variant, tone, completed: isCompleted });
456
+ const { item } = timelineVariants({
457
+ variant,
458
+ tone,
459
+ completed: isCompleted,
460
+ orientation,
461
+ });
462
+ const horizontal = orientation === 'horizontal';
463
+
464
+ const columnWidth = horizontal
465
+ ? itemWidth({ step, width, children } as TimelineItemProps)
466
+ : undefined;
467
+ const offset = offsets[step] ?? 0;
242
468
 
243
469
  const context = useMemo(
244
- () => ({ step, completed: isCompleted, tone, showSeparator: !last }),
245
- [step, isCompleted, tone, last]
470
+ () => ({
471
+ step,
472
+ completed: isCompleted,
473
+ tone,
474
+ showSeparator: !last,
475
+ offset,
476
+ columnWidth: columnWidth ?? 0,
477
+ }),
478
+ [step, isCompleted, tone, last, offset, columnWidth]
246
479
  );
247
480
 
481
+ /*
482
+ * A column recedes as it leaves the reading edge, so the one being read is
483
+ * the one that looks read.
484
+ *
485
+ * One column's width either side, not two: a window wide enough to hold
486
+ * three columns near full strength is a row where nothing is picked out,
487
+ * and picking one out is the whole job. The neighbours stay legible at the
488
+ * far end of it — a timeline is worth reading for what came before and
489
+ * after — they simply stop competing.
490
+ *
491
+ * The scale and the drop are what turn a fade into a focus. Four points and
492
+ * four percent is under the threshold where anybody would call it movement,
493
+ * and over the one where the column at the edge stops looking flat against
494
+ * the rest.
495
+ */
496
+ const columnStyle = useAnimatedStyle(() => {
497
+ if (!horizontal || !animate) return { opacity: 1, transform: [] };
498
+ const distance = Math.abs(scrollX.value - offset);
499
+ const window = Math.max(columnWidth ?? timelineColumnWidth(undefined, true), 1);
500
+ const away = interpolate(distance, [0, window], [0, 1], 'clamp');
501
+ return {
502
+ opacity: 1 - away * 0.4,
503
+ transform: [{ scale: 1 - away * 0.04 }, { translateY: away * 4 }],
504
+ };
505
+ });
506
+
507
+ if (!horizontal) {
508
+ return (
509
+ <TimelineItemContext.Provider value={context}>
510
+ <View ref={ref} className={item({ className })} style={style} {...props}>
511
+ {textChildren(children)}
512
+ </View>
513
+ </TimelineItemContext.Provider>
514
+ );
515
+ }
516
+
248
517
  return (
249
518
  <TimelineItemContext.Provider value={context}>
250
- <View ref={ref} className={item({ className })} {...props}>
519
+ <Animated.View
520
+ ref={ref}
521
+ className={item({ className })}
522
+ {...props}
523
+ style={[style, { width: columnWidth }, columnStyle]}
524
+ >
251
525
  {textChildren(children)}
252
- </View>
526
+ </Animated.View>
253
527
  </TimelineItemContext.Provider>
254
528
  );
255
529
  }
@@ -261,10 +535,28 @@ TimelineItem.displayName = 'Timeline.Item';
261
535
  * person. Place it before `Timeline.Indicator`.
262
536
  */
263
537
  const TimelineAside = forwardRef<View, ViewProps & { className?: string }>(
264
- ({ className, ...props }, ref) => {
265
- const { variant } = useTimeline('Timeline.Aside');
266
- const { aside } = timelineVariants({ variant });
267
- return <View ref={ref} className={aside({ className })} {...props} />;
538
+ ({ className, style, ...props }, ref) => {
539
+ const { variant, orientation } = useTimeline('Timeline.Aside');
540
+ const { aside } = timelineVariants({ variant, orientation });
541
+
542
+ return (
543
+ <View
544
+ ref={ref}
545
+ className={aside({ className })}
546
+ /*
547
+ * Horizontal: this band's height is where the rail lands, so it is
548
+ * fixed rather than sized to whatever the column happens to put in it.
549
+ * A column with a longer label would otherwise push its own tick below
550
+ * everybody else's and the rail would stop being a line.
551
+ */
552
+ style={
553
+ orientation === 'horizontal'
554
+ ? [{ height: HORIZONTAL_RAIL_TOP }, style]
555
+ : style
556
+ }
557
+ {...props}
558
+ />
559
+ );
268
560
  }
269
561
  );
270
562
  TimelineAside.displayName = 'Timeline.Aside';
@@ -284,15 +576,43 @@ export interface TimelineIndicatorProps extends ViewProps {
284
576
  */
285
577
  const TimelineIndicator = forwardRef<View, TimelineIndicatorProps>(
286
578
  ({ className, children, ...props }, ref) => {
287
- const { variant } = useTimeline('Timeline.Indicator');
579
+ const { variant, orientation } = useTimeline('Timeline.Indicator');
288
580
  const { step, completed, tone, showSeparator } =
289
581
  useTimelineItem('Timeline.Indicator');
290
582
  const { rail, indicator, indicatorLabel, separator } = timelineVariants({
291
583
  variant,
292
584
  tone,
293
585
  completed,
586
+ orientation,
294
587
  });
295
588
 
589
+ /*
590
+ * Horizontal: a tick sitting on the rail, not a node with a connector
591
+ * running out of it. The rail is already drawn across the whole track by
592
+ * the root, so what a column owes it is the mark saying where this entry
593
+ * falls — pulled up by half its height to straddle the line rather than
594
+ * hang off it.
595
+ */
596
+ if (orientation === 'horizontal') {
597
+ return (
598
+ <View className={rail()}>
599
+ <View
600
+ ref={ref}
601
+ accessibilityElementsHidden
602
+ importantForAccessibility="no-hide-descendants"
603
+ className={indicator({
604
+ className: `w-px rounded-none ${completed ? 'bg-primary' : 'bg-muted-foreground'} ${className ?? ''}`,
605
+ })}
606
+ style={{
607
+ height: HORIZONTAL_TICK_HEIGHT,
608
+ marginTop: -HORIZONTAL_TICK_HEIGHT / 2,
609
+ }}
610
+ {...props}
611
+ />
612
+ </View>
613
+ );
614
+ }
615
+
296
616
  // dot/card nodes are small discs with nothing inside, so they keep full
297
617
  // saturation — a soft tint at 16px would disappear. Nodes that hold an
298
618
  // icon or a number take the tinted fill instead.
@@ -351,15 +671,54 @@ TimelineIndicator.displayName = 'Timeline.Indicator';
351
671
  * so the connector runs unbroken.
352
672
  */
353
673
  const TimelineContent = forwardRef<View, ViewProps & { className?: string }>(
354
- ({ className, children, ...props }, ref) => {
355
- const { variant } = useTimeline('Timeline.Content');
356
- const { completed, tone } = useTimelineItem('Timeline.Content');
357
- const { body, panel } = timelineVariants({ variant, tone, completed });
674
+ ({ className, children, style, ...props }, ref) => {
675
+ const { variant, orientation, scrollX, animate } = useTimeline('Timeline.Content');
676
+ const { completed, tone, offset, columnWidth } =
677
+ useTimelineItem('Timeline.Content');
678
+ const { body, panel } = timelineVariants({ variant, tone, completed, orientation });
679
+ const horizontal = orientation === 'horizontal';
680
+
681
+ /*
682
+ * Horizontal: the body recedes further than the column around it.
683
+ *
684
+ * The rail is a continuous thing and has to stay readable across the whole
685
+ * track — the years either side of the one being read are half of what a
686
+ * timeline is for. The prose under them is not: several columns of it at
687
+ * equal weight is a wall, and no amount of fading the column as a whole
688
+ * fixes that without taking the years down with it. So the body takes a
689
+ * second, steeper curve and the dates keep the first one.
690
+ */
691
+ const bodyStyle = useAnimatedStyle(() => {
692
+ if (!horizontal || !animate) return { opacity: 1 };
693
+ const distance = Math.abs(scrollX.value - offset);
694
+ const window = Math.max(columnWidth, 1);
695
+ return { opacity: interpolate(distance, [0, window], [1, 0.3], 'clamp') };
696
+ });
697
+
698
+ const inner =
699
+ variant === 'card' ? (
700
+ <View className={panel()}>{textChildren(children)}</View>
701
+ ) : (
702
+ children
703
+ );
704
+
705
+ if (!horizontal) {
706
+ return (
707
+ <View ref={ref} className={body({ className })} style={style} {...props}>
708
+ {inner}
709
+ </View>
710
+ );
711
+ }
358
712
 
359
713
  return (
360
- <View ref={ref} className={body({ className })} {...props}>
361
- {variant === 'card' ? <View className={panel()}>{textChildren(children)}</View> : children}
362
- </View>
714
+ <Animated.View
715
+ ref={ref}
716
+ className={body({ className })}
717
+ {...props}
718
+ style={[style, bodyStyle]}
719
+ >
720
+ {inner}
721
+ </Animated.View>
363
722
  );
364
723
  }
365
724
  );
@@ -368,8 +727,8 @@ TimelineContent.displayName = 'Timeline.Content';
368
727
  /** Title row: heading on the left, `Timeline.Trailing` on the right. */
369
728
  const TimelineHeader = forwardRef<View, ViewProps & { className?: string }>(
370
729
  ({ className, ...props }, ref) => {
371
- const { variant } = useTimeline('Timeline.Header');
372
- const { header } = timelineVariants({ variant });
730
+ const { variant, orientation } = useTimeline('Timeline.Header');
731
+ const { header } = timelineVariants({ variant, orientation });
373
732
  return <View ref={ref} className={header({ className })} {...props} />;
374
733
  }
375
734
  );
@@ -378,40 +737,40 @@ TimelineHeader.displayName = 'Timeline.Header';
378
737
  /** Wraps a title and anything stacked under it inside the header row. */
379
738
  const TimelineHeading = forwardRef<View, ViewProps & { className?: string }>(
380
739
  ({ className, ...props }, ref) => {
381
- const { variant } = useTimeline('Timeline.Heading');
382
- const { heading } = timelineVariants({ variant });
740
+ const { variant, orientation } = useTimeline('Timeline.Heading');
741
+ const { heading } = timelineVariants({ variant, orientation });
383
742
  return <View ref={ref} className={heading({ className })} {...props} />;
384
743
  }
385
744
  );
386
745
  TimelineHeading.displayName = 'Timeline.Heading';
387
746
 
388
747
  const TimelineDate = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
389
- const { variant } = useTimeline('Timeline.Date');
390
- const { date } = timelineVariants({ variant });
748
+ const { variant, orientation } = useTimeline('Timeline.Date');
749
+ const { date } = timelineVariants({ variant, orientation });
391
750
  return <Text ref={ref} className={date({ className })} {...props} />;
392
751
  });
393
752
  TimelineDate.displayName = 'Timeline.Date';
394
753
 
395
754
  /** Category line in the aside, coloured by the item's tone. */
396
755
  const TimelineLabel = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
397
- const { variant } = useTimeline('Timeline.Label');
756
+ const { variant, orientation } = useTimeline('Timeline.Label');
398
757
  const { tone } = useTimelineItem('Timeline.Label');
399
- const { label } = timelineVariants({ variant, tone });
758
+ const { label } = timelineVariants({ variant, tone, orientation });
400
759
  return <Text ref={ref} className={label({ className })} {...props} />;
401
760
  });
402
761
  TimelineLabel.displayName = 'Timeline.Label';
403
762
 
404
763
  /** Muted supporting line — a person's name, a source. */
405
764
  const TimelineMeta = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
406
- const { variant } = useTimeline('Timeline.Meta');
407
- const { meta } = timelineVariants({ variant });
765
+ const { variant, orientation } = useTimeline('Timeline.Meta');
766
+ const { meta } = timelineVariants({ variant, orientation });
408
767
  return <Text ref={ref} className={meta({ className })} {...props} />;
409
768
  });
410
769
  TimelineMeta.displayName = 'Timeline.Meta';
411
770
 
412
771
  const TimelineTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
413
- const { variant } = useTimeline('Timeline.Title');
414
- const { title } = timelineVariants({ variant });
772
+ const { variant, orientation } = useTimeline('Timeline.Title');
773
+ const { title } = timelineVariants({ variant, orientation });
415
774
  return <Text ref={ref} className={title({ className })} {...props} />;
416
775
  });
417
776
  TimelineTitle.displayName = 'Timeline.Title';
@@ -419,8 +778,8 @@ TimelineTitle.displayName = 'Timeline.Title';
419
778
  /** Right-hand slot in the header row — a timestamp, usually. */
420
779
  const TimelineTrailing = forwardRef<RNText, TextProps>(
421
780
  ({ className, ...props }, ref) => {
422
- const { variant } = useTimeline('Timeline.Trailing');
423
- const { trailing } = timelineVariants({ variant });
781
+ const { variant, orientation } = useTimeline('Timeline.Trailing');
782
+ const { trailing } = timelineVariants({ variant, orientation });
424
783
  return <Text ref={ref} className={trailing({ className })} {...props} />;
425
784
  }
426
785
  );
@@ -428,8 +787,8 @@ TimelineTrailing.displayName = 'Timeline.Trailing';
428
787
 
429
788
  const TimelineDescription = forwardRef<RNText, TextProps>(
430
789
  ({ className, ...props }, ref) => {
431
- const { variant } = useTimeline('Timeline.Description');
432
- const { content } = timelineVariants({ variant });
790
+ const { variant, orientation } = useTimeline('Timeline.Description');
791
+ const { content } = timelineVariants({ variant, orientation });
433
792
  return <Text ref={ref} className={content({ className })} {...props} />;
434
793
  }
435
794
  );
@@ -438,8 +797,8 @@ TimelineDescription.displayName = 'Timeline.Description';
438
797
  /** Bordered strip of label/value pairs under a title. */
439
798
  const TimelineStats = forwardRef<View, ViewProps & { className?: string }>(
440
799
  ({ className, ...props }, ref) => {
441
- const { variant } = useTimeline('Timeline.Stats');
442
- const { stats } = timelineVariants({ variant });
800
+ const { variant, orientation } = useTimeline('Timeline.Stats');
801
+ const { stats } = timelineVariants({ variant, orientation });
443
802
  return <View ref={ref} className={stats({ className })} {...props} />;
444
803
  }
445
804
  );
@@ -453,8 +812,8 @@ export interface TimelineStatProps extends ViewProps {
453
812
 
454
813
  const TimelineStat = forwardRef<View, TimelineStatProps>(
455
814
  ({ className, label, value, ...props }, ref) => {
456
- const { variant } = useTimeline('Timeline.Stat');
457
- const { statLabel, statValue } = timelineVariants({ variant });
815
+ const { variant, orientation } = useTimeline('Timeline.Stat');
816
+ const { statLabel, statValue } = timelineVariants({ variant, orientation });
458
817
  return (
459
818
  <View ref={ref} className={className} {...props}>
460
819
  <Text className={statLabel()}>{label}</Text>
@@ -0,0 +1,9 @@
1
+ export const TIMELINE_WIDE_COLUMN = 268;
2
+ export const TIMELINE_NARROW_COLUMN = 76;
3
+
4
+ export function timelineColumnWidth(requested: number | undefined, filled: boolean): number {
5
+ if (requested !== undefined && Number.isFinite(requested) && requested > 0) {
6
+ return requested;
7
+ }
8
+ return filled ? TIMELINE_WIDE_COLUMN : TIMELINE_NARROW_COLUMN;
9
+ }
@@ -18,7 +18,12 @@ import {
18
18
  useSyncExternalStore,
19
19
  type ReactNode,
20
20
  } from 'react';
21
- import { View, type ViewProps } from 'react-native';
21
+ import {
22
+ AppState,
23
+ View,
24
+ type AppStateStatus,
25
+ type ViewProps,
26
+ } from 'react-native';
22
27
  import { Gesture, GestureDetector } from 'react-native-gesture-handler';
23
28
  import Animated, {
24
29
  Easing,
@@ -538,6 +543,26 @@ function ToastStack({
538
543
  export function ToastViewport() {
539
544
  const items = useSyncExternalStore(toastStore.subscribe, toastStore.getSnapshot);
540
545
 
546
+ useEffect(() => {
547
+ /*
548
+ * Pause on the two states that mean nobody can see the toast, rather than
549
+ * on everything that is not `active`. Android reports `unknown` before it
550
+ * has decided, and a toast raised in that window would otherwise have its
551
+ * countdown parked with nothing coming to restart it.
552
+ */
553
+ const syncTimers = (state: AppStateStatus) => {
554
+ if (state === 'background' || state === 'inactive') toastStore.pauseTimers();
555
+ else toastStore.resumeTimers();
556
+ };
557
+
558
+ syncTimers(AppState.currentState);
559
+ const subscription = AppState.addEventListener('change', syncTimers);
560
+ return () => {
561
+ subscription.remove();
562
+ toastStore.pauseTimers();
563
+ };
564
+ }, []);
565
+
541
566
  if (items.length === 0) return null;
542
567
 
543
568
  const top = items.filter((item) => item.placement === 'top');