@delacour/react-native-ui 0.1.0-alpha.4 → 0.2.0-alpha.20261010061731

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 (493) hide show
  1. package/README.md +10 -6
  2. package/package.json +49 -5
  3. package/src/components/accordion/AGENTS.md +21 -9
  4. package/src/components/accordion/accordion-content.tsx +7 -4
  5. package/src/components/accordion/accordion-item.tsx +15 -13
  6. package/src/components/accordion/accordion.context.tsx +11 -5
  7. package/src/components/accordion/accordion.variants.test.ts +64 -3
  8. package/src/components/accordion/accordion.variants.ts +33 -9
  9. package/src/components/alert/AGENTS.md +102 -0
  10. package/src/components/alert/alert-action.tsx +17 -0
  11. package/src/components/alert/alert-close-button.tsx +50 -0
  12. package/src/components/alert/alert-content.tsx +15 -0
  13. package/src/components/alert/alert-description.tsx +16 -0
  14. package/src/components/alert/alert-glyphs.ts +25 -0
  15. package/src/components/alert/alert-indicator.tsx +40 -0
  16. package/src/components/alert/alert-title.tsx +18 -0
  17. package/src/components/alert/alert.context.tsx +69 -0
  18. package/src/components/alert/alert.tsx +160 -0
  19. package/src/components/alert/alert.types.ts +10 -0
  20. package/src/components/alert/alert.variants.test.ts +218 -0
  21. package/src/components/alert/alert.variants.ts +180 -0
  22. package/src/components/alert/index.ts +23 -0
  23. package/src/components/avatar/AGENTS.md +106 -0
  24. package/src/components/avatar/avatar-badge.tsx +51 -0
  25. package/src/components/avatar/avatar-group.tsx +139 -0
  26. package/src/components/avatar/avatar.context.tsx +93 -0
  27. package/src/components/avatar/avatar.tsx +201 -0
  28. package/src/components/avatar/avatar.utils.test.ts +174 -0
  29. package/src/components/avatar/avatar.utils.ts +141 -0
  30. package/src/components/avatar/avatar.variants.test.ts +251 -0
  31. package/src/components/avatar/avatar.variants.ts +270 -0
  32. package/src/components/avatar/index.ts +39 -0
  33. package/src/components/badge/AGENTS.md +10 -0
  34. package/src/components/badge/badge-close-button.tsx +2 -2
  35. package/src/components/badge/badge.context.tsx +2 -1
  36. package/src/components/badge/badge.tsx +13 -4
  37. package/src/components/badge/badge.variants.test.ts +26 -0
  38. package/src/components/badge/badge.variants.ts +21 -0
  39. package/src/components/bottom-sheet/AGENTS.md +186 -231
  40. package/src/components/bottom-sheet/bottom-sheet-background.tsx +31 -22
  41. package/src/components/bottom-sheet/bottom-sheet-close.tsx +23 -30
  42. package/src/components/bottom-sheet/bottom-sheet-container.tsx +54 -208
  43. package/src/components/bottom-sheet/bottom-sheet-content.tsx +25 -48
  44. package/src/components/bottom-sheet/bottom-sheet-description.tsx +8 -2
  45. package/src/components/bottom-sheet/bottom-sheet-flat-list.tsx +65 -0
  46. package/src/components/bottom-sheet/bottom-sheet-footer.tsx +53 -122
  47. package/src/components/bottom-sheet/bottom-sheet-handle.tsx +31 -26
  48. package/src/components/bottom-sheet/bottom-sheet-legend-list.tsx +69 -0
  49. package/src/components/bottom-sheet/bottom-sheet-overlay.tsx +25 -147
  50. package/src/components/bottom-sheet/bottom-sheet-provider.tsx +42 -0
  51. package/src/components/bottom-sheet/bottom-sheet-scroll-view.tsx +38 -62
  52. package/src/components/bottom-sheet/bottom-sheet-section-list.tsx +58 -0
  53. package/src/components/bottom-sheet/bottom-sheet-steps.tsx +88 -0
  54. package/src/components/bottom-sheet/bottom-sheet-text-input.tsx +44 -0
  55. package/src/components/bottom-sheet/bottom-sheet-title.tsx +12 -8
  56. package/src/components/bottom-sheet/bottom-sheet-trigger.tsx +19 -37
  57. package/src/components/bottom-sheet/bottom-sheet.tsx +113 -60
  58. package/src/components/bottom-sheet/bottom-sheet.variants.test.ts +47 -87
  59. package/src/components/bottom-sheet/bottom-sheet.variants.ts +50 -126
  60. package/src/components/bottom-sheet/index.ts +39 -17
  61. package/src/components/bottom-sheet/use-bottom-sheet-input.ts +16 -115
  62. package/src/components/button/AGENTS.md +51 -11
  63. package/src/components/button/button-group.tsx +44 -8
  64. package/src/components/button/button.context.tsx +10 -0
  65. package/src/components/button/button.tsx +7 -1
  66. package/src/components/button/button.variants.test.ts +190 -0
  67. package/src/components/button/button.variants.ts +87 -6
  68. package/src/components/button/index.ts +2 -0
  69. package/src/components/calendar/AGENTS.md +142 -0
  70. package/src/components/calendar/calendar-caption.tsx +116 -0
  71. package/src/components/calendar/calendar-day.tsx +103 -0
  72. package/src/components/calendar/calendar-grid.tsx +183 -0
  73. package/src/components/calendar/calendar-header.tsx +34 -0
  74. package/src/components/calendar/calendar-nav.tsx +51 -0
  75. package/src/components/calendar/calendar-picker.tsx +128 -0
  76. package/src/components/calendar/calendar-weekdays.tsx +48 -0
  77. package/src/components/calendar/calendar.context.tsx +108 -0
  78. package/src/components/calendar/calendar.date.test.ts +456 -0
  79. package/src/components/calendar/calendar.date.ts +584 -0
  80. package/src/components/calendar/calendar.tsx +389 -0
  81. package/src/components/calendar/calendar.types.ts +87 -0
  82. package/src/components/calendar/calendar.variants.test.ts +363 -0
  83. package/src/components/calendar/calendar.variants.ts +426 -0
  84. package/src/components/calendar/index.ts +96 -0
  85. package/src/components/card/AGENTS.md +92 -0
  86. package/src/components/card/card-action.tsx +16 -0
  87. package/src/components/card/card-content.tsx +12 -0
  88. package/src/components/card/card-description.tsx +12 -0
  89. package/src/components/card/card-footer.tsx +30 -0
  90. package/src/components/card/card-header.tsx +45 -0
  91. package/src/components/card/card-title.tsx +25 -0
  92. package/src/components/card/card.context.tsx +64 -0
  93. package/src/components/card/card.tsx +100 -0
  94. package/src/components/card/card.types.ts +13 -0
  95. package/src/components/card/card.variants.test.ts +240 -0
  96. package/src/components/card/card.variants.ts +177 -0
  97. package/src/components/card/index.ts +15 -0
  98. package/src/components/chart/AGENTS.md +8 -0
  99. package/src/components/chart/chart.tsx +8 -1
  100. package/src/components/chip/AGENTS.md +113 -0
  101. package/src/components/chip/chip-close-button.tsx +48 -0
  102. package/src/components/chip/chip-end-content.tsx +15 -0
  103. package/src/components/chip/chip-label.tsx +21 -0
  104. package/src/components/chip/chip-start-content.tsx +16 -0
  105. package/src/components/chip/chip.context.tsx +66 -0
  106. package/src/components/chip/chip.tsx +268 -0
  107. package/src/components/chip/chip.types.ts +10 -0
  108. package/src/components/chip/chip.variants.test.ts +335 -0
  109. package/src/components/chip/chip.variants.ts +321 -0
  110. package/src/components/chip/index.ts +24 -0
  111. package/src/components/collapsible/AGENTS.md +97 -0
  112. package/src/components/collapsible/collapsible-content.tsx +97 -0
  113. package/src/components/collapsible/collapsible-description.tsx +17 -0
  114. package/src/components/collapsible/collapsible-indicator.tsx +92 -0
  115. package/src/components/collapsible/collapsible-title.tsx +17 -0
  116. package/src/components/collapsible/collapsible-trigger.tsx +133 -0
  117. package/src/components/collapsible/collapsible.context.tsx +91 -0
  118. package/src/components/collapsible/collapsible.tsx +167 -0
  119. package/src/components/collapsible/collapsible.types.ts +9 -0
  120. package/src/components/collapsible/collapsible.variants.test.ts +338 -0
  121. package/src/components/collapsible/collapsible.variants.ts +228 -0
  122. package/src/components/collapsible/index.ts +32 -0
  123. package/src/components/context-menu/AGENTS.md +103 -0
  124. package/src/components/context-menu/context-menu-content.tsx +64 -0
  125. package/src/components/context-menu/context-menu-item.tsx +22 -0
  126. package/src/components/context-menu/context-menu-preview.tsx +60 -0
  127. package/src/components/context-menu/context-menu-trigger.tsx +160 -0
  128. package/src/components/context-menu/context-menu.context.tsx +91 -0
  129. package/src/components/context-menu/context-menu.tsx +90 -0
  130. package/src/components/context-menu/context-menu.variants.test.ts +266 -0
  131. package/src/components/context-menu/context-menu.variants.ts +147 -0
  132. package/src/components/context-menu/index.ts +18 -0
  133. package/src/components/dialog/AGENTS.md +97 -0
  134. package/src/components/dialog/dialog-body.tsx +21 -0
  135. package/src/components/dialog/dialog-close.tsx +82 -0
  136. package/src/components/dialog/dialog-content.tsx +153 -0
  137. package/src/components/dialog/dialog-description.tsx +23 -0
  138. package/src/components/dialog/dialog-footer.tsx +32 -0
  139. package/src/components/dialog/dialog-header.tsx +20 -0
  140. package/src/components/dialog/dialog-title.tsx +42 -0
  141. package/src/components/dialog/dialog-trigger.tsx +54 -0
  142. package/src/components/dialog/dialog.context.tsx +90 -0
  143. package/src/components/dialog/dialog.tsx +119 -0
  144. package/src/components/dialog/dialog.variants.test.ts +224 -0
  145. package/src/components/dialog/dialog.variants.ts +173 -0
  146. package/src/components/dialog/index.ts +30 -0
  147. package/src/components/drawer/AGENTS.md +123 -0
  148. package/src/components/drawer/drawer-body.tsx +46 -0
  149. package/src/components/drawer/drawer-close.tsx +76 -0
  150. package/src/components/drawer/drawer-content.tsx +186 -0
  151. package/src/components/drawer/drawer-description.tsx +23 -0
  152. package/src/components/drawer/drawer-footer.tsx +21 -0
  153. package/src/components/drawer/drawer-header.tsx +42 -0
  154. package/src/components/drawer/drawer-title.tsx +35 -0
  155. package/src/components/drawer/drawer-trigger.tsx +54 -0
  156. package/src/components/drawer/drawer.context.tsx +114 -0
  157. package/src/components/drawer/drawer.tsx +119 -0
  158. package/src/components/drawer/drawer.variants.test.ts +392 -0
  159. package/src/components/drawer/drawer.variants.ts +244 -0
  160. package/src/components/drawer/index.ts +41 -0
  161. package/src/components/drawer/use-drawer-pan.ts +108 -0
  162. package/src/components/empty-state/AGENTS.md +90 -0
  163. package/src/components/empty-state/empty-state-content.tsx +17 -0
  164. package/src/components/empty-state/empty-state-description.tsx +12 -0
  165. package/src/components/empty-state/empty-state-header.tsx +17 -0
  166. package/src/components/empty-state/empty-state-media.tsx +56 -0
  167. package/src/components/empty-state/empty-state-title.tsx +18 -0
  168. package/src/components/empty-state/empty-state.context.tsx +65 -0
  169. package/src/components/empty-state/empty-state.tsx +84 -0
  170. package/src/components/empty-state/empty-state.types.ts +14 -0
  171. package/src/components/empty-state/empty-state.variants.test.ts +245 -0
  172. package/src/components/empty-state/empty-state.variants.ts +132 -0
  173. package/src/components/empty-state/index.ts +20 -0
  174. package/src/components/fab/AGENTS.md +119 -0
  175. package/src/components/fab/fab-action.tsx +118 -0
  176. package/src/components/fab/fab-group.tsx +214 -0
  177. package/src/components/fab/fab-label.tsx +18 -0
  178. package/src/components/fab/fab.context.tsx +125 -0
  179. package/src/components/fab/fab.tsx +156 -0
  180. package/src/components/fab/fab.types.ts +17 -0
  181. package/src/components/fab/fab.variants.test.ts +274 -0
  182. package/src/components/fab/fab.variants.ts +234 -0
  183. package/src/components/fab/index.ts +37 -0
  184. package/src/components/feedback/AGENTS.md +95 -0
  185. package/src/components/feedback/feedback-action.tsx +16 -0
  186. package/src/components/feedback/feedback-cancel.tsx +34 -0
  187. package/src/components/feedback/feedback-close.tsx +21 -0
  188. package/src/components/feedback/feedback-content.tsx +24 -0
  189. package/src/components/feedback/feedback-field.tsx +70 -0
  190. package/src/components/feedback/feedback-footer.tsx +21 -0
  191. package/src/components/feedback/feedback-panel.tsx +54 -0
  192. package/src/components/feedback/feedback-submit.tsx +74 -0
  193. package/src/components/feedback/feedback-title.tsx +34 -0
  194. package/src/components/feedback/feedback-trigger.tsx +20 -0
  195. package/src/components/feedback/feedback.context.tsx +73 -0
  196. package/src/components/feedback/feedback.tsx +135 -0
  197. package/src/components/feedback/feedback.variants.test.ts +185 -0
  198. package/src/components/feedback/feedback.variants.ts +128 -0
  199. package/src/components/feedback/index.ts +24 -0
  200. package/src/components/field/AGENTS.md +19 -0
  201. package/src/components/field/field-label.tsx +19 -5
  202. package/src/components/field/field.context.tsx +17 -0
  203. package/src/components/field/field.tsx +15 -2
  204. package/src/components/field/field.variants.test.ts +24 -0
  205. package/src/components/field/field.variants.ts +20 -0
  206. package/src/components/field/index.ts +1 -0
  207. package/src/components/input/AGENTS.md +5 -0
  208. package/src/components/input/input.variants.test.ts +32 -0
  209. package/src/components/input/input.variants.ts +13 -1
  210. package/src/components/item/AGENTS.md +92 -0
  211. package/src/components/item/index.ts +25 -0
  212. package/src/components/item/item-actions.tsx +28 -0
  213. package/src/components/item/item-content.tsx +15 -0
  214. package/src/components/item/item-description.tsx +12 -0
  215. package/src/components/item/item-footer.tsx +13 -0
  216. package/src/components/item/item-group.tsx +32 -0
  217. package/src/components/item/item-header.tsx +13 -0
  218. package/src/components/item/item-media.tsx +37 -0
  219. package/src/components/item/item-separator.tsx +13 -0
  220. package/src/components/item/item-title.tsx +17 -0
  221. package/src/components/item/item.context.tsx +66 -0
  222. package/src/components/item/item.tsx +218 -0
  223. package/src/components/item/item.types.ts +12 -0
  224. package/src/components/item/item.variants.test.ts +263 -0
  225. package/src/components/item/item.variants.ts +200 -0
  226. package/src/components/kpi/AGENTS.md +125 -0
  227. package/src/components/kpi/index.ts +45 -0
  228. package/src/components/kpi/kpi-action.tsx +12 -0
  229. package/src/components/kpi/kpi-content.tsx +35 -0
  230. package/src/components/kpi/kpi-footer.tsx +52 -0
  231. package/src/components/kpi/kpi-group.tsx +70 -0
  232. package/src/components/kpi/kpi-header.tsx +25 -0
  233. package/src/components/kpi/kpi-icon.tsx +28 -0
  234. package/src/components/kpi/kpi-sparkline.tsx +145 -0
  235. package/src/components/kpi/kpi-stat.tsx +20 -0
  236. package/src/components/kpi/kpi-title.tsx +19 -0
  237. package/src/components/kpi/kpi-trend.tsx +105 -0
  238. package/src/components/kpi/kpi-value.tsx +32 -0
  239. package/src/components/kpi/kpi.context.tsx +111 -0
  240. package/src/components/kpi/kpi.tsx +154 -0
  241. package/src/components/kpi/kpi.types.ts +9 -0
  242. package/src/components/kpi/kpi.variants.test.ts +428 -0
  243. package/src/components/kpi/kpi.variants.ts +347 -0
  244. package/src/components/label/AGENTS.md +89 -0
  245. package/src/components/label/index.ts +10 -0
  246. package/src/components/label/label.tsx +97 -0
  247. package/src/components/label/label.variants.test.ts +176 -0
  248. package/src/components/label/label.variants.ts +123 -0
  249. package/src/components/menu/AGENTS.md +176 -0
  250. package/src/components/menu/index.ts +44 -0
  251. package/src/components/menu/menu-background.tsx +17 -0
  252. package/src/components/menu/menu-checkbox-item.tsx +57 -0
  253. package/src/components/menu/menu-content.tsx +303 -0
  254. package/src/components/menu/menu-item.tsx +162 -0
  255. package/src/components/menu/menu-label.tsx +14 -0
  256. package/src/components/menu/menu-radio-group.tsx +50 -0
  257. package/src/components/menu/menu-radio-item.tsx +57 -0
  258. package/src/components/menu/menu-separator.tsx +11 -0
  259. package/src/components/menu/menu-sub-content.tsx +72 -0
  260. package/src/components/menu/menu-sub-trigger.tsx +50 -0
  261. package/src/components/menu/menu-sub.tsx +75 -0
  262. package/src/components/menu/menu-trigger.tsx +57 -0
  263. package/src/components/menu/menu.context.tsx +214 -0
  264. package/src/components/menu/menu.tsx +88 -0
  265. package/src/components/menu/menu.types.ts +33 -0
  266. package/src/components/menu/menu.variants.test.ts +400 -0
  267. package/src/components/menu/menu.variants.ts +353 -0
  268. package/src/components/meter/AGENTS.md +135 -0
  269. package/src/components/meter/index.ts +43 -0
  270. package/src/components/meter/meter-fill.tsx +19 -0
  271. package/src/components/meter/meter-header.tsx +17 -0
  272. package/src/components/meter/meter-label.tsx +18 -0
  273. package/src/components/meter/meter-output.tsx +42 -0
  274. package/src/components/meter/meter-segments.tsx +83 -0
  275. package/src/components/meter/meter-track.tsx +16 -0
  276. package/src/components/meter/meter.context.tsx +62 -0
  277. package/src/components/meter/meter.tsx +286 -0
  278. package/src/components/meter/meter.types.ts +30 -0
  279. package/src/components/meter/meter.variants.test.ts +430 -0
  280. package/src/components/meter/meter.variants.ts +347 -0
  281. package/src/components/overlay/AGENTS.md +89 -0
  282. package/src/components/overlay/index.ts +27 -0
  283. package/src/components/overlay/overlay-portal.tsx +84 -0
  284. package/src/components/overlay/overlay-presence.test.ts +46 -0
  285. package/src/components/overlay/overlay-presence.ts +49 -0
  286. package/src/components/overlay/overlay-registry.test.ts +96 -0
  287. package/src/components/overlay/overlay-registry.ts +99 -0
  288. package/src/components/overlay/overlay-scrim.tsx +49 -0
  289. package/src/components/overlay/overlay.context.tsx +48 -0
  290. package/src/components/overlay/overlay.tsx +118 -0
  291. package/src/components/overlay/overlay.variants.test.ts +37 -0
  292. package/src/components/overlay/overlay.variants.ts +33 -0
  293. package/src/components/overlay/use-overlay-back-handler.ts +34 -0
  294. package/src/components/overlay/use-overlay-presence.ts +87 -0
  295. package/src/components/popover/AGENTS.md +138 -0
  296. package/src/components/popover/index.ts +48 -0
  297. package/src/components/popover/popover-anchor.tsx +49 -0
  298. package/src/components/popover/popover-arrow.tsx +89 -0
  299. package/src/components/popover/popover-close.tsx +73 -0
  300. package/src/components/popover/popover-content.tsx +235 -0
  301. package/src/components/popover/popover-description.tsx +18 -0
  302. package/src/components/popover/popover-title.tsx +33 -0
  303. package/src/components/popover/popover-trigger.tsx +56 -0
  304. package/src/components/popover/popover.context.tsx +71 -0
  305. package/src/components/popover/popover.position.test.ts +354 -0
  306. package/src/components/popover/popover.position.ts +286 -0
  307. package/src/components/popover/popover.tsx +123 -0
  308. package/src/components/popover/popover.variants.test.ts +135 -0
  309. package/src/components/popover/popover.variants.ts +85 -0
  310. package/src/components/popover/use-anchor-measure.ts +76 -0
  311. package/src/components/popover/use-anchored-content.ts +219 -0
  312. package/src/components/pressable/AGENTS.md +11 -1
  313. package/src/components/pressable/pressable.tsx +7 -2
  314. package/src/components/pressable/pressable.variants.test.ts +29 -0
  315. package/src/components/pressable/pressable.variants.ts +17 -0
  316. package/src/components/progress/AGENTS.md +120 -0
  317. package/src/components/progress/index.ts +40 -0
  318. package/src/components/progress/progress-fill.tsx +152 -0
  319. package/src/components/progress/progress-header.tsx +19 -0
  320. package/src/components/progress/progress-label.tsx +27 -0
  321. package/src/components/progress/progress-output.tsx +47 -0
  322. package/src/components/progress/progress-track.tsx +38 -0
  323. package/src/components/progress/progress.context.tsx +79 -0
  324. package/src/components/progress/progress.tsx +171 -0
  325. package/src/components/progress/progress.types.ts +23 -0
  326. package/src/components/progress/progress.variants.test.ts +335 -0
  327. package/src/components/progress/progress.variants.ts +284 -0
  328. package/src/components/progress-button/AGENTS.md +116 -0
  329. package/src/components/progress-button/index.ts +36 -0
  330. package/src/components/progress-button/progress-button-done.tsx +33 -0
  331. package/src/components/progress-button/progress-button-label.tsx +27 -0
  332. package/src/components/progress-button/progress-button.context.tsx +93 -0
  333. package/src/components/progress-button/progress-button.tsx +414 -0
  334. package/src/components/progress-button/progress-button.variants.test.ts +314 -0
  335. package/src/components/progress-button/progress-button.variants.ts +236 -0
  336. package/src/components/provider/AGENTS.md +38 -25
  337. package/src/components/provider/provider.tsx +38 -11
  338. package/src/components/radio/AGENTS.md +8 -10
  339. package/src/components/radio/radio.variants.test.ts +5 -6
  340. package/src/components/radio/radio.variants.ts +6 -7
  341. package/src/components/rating/AGENTS.md +143 -0
  342. package/src/components/rating/index.ts +43 -0
  343. package/src/components/rating/rating-output.tsx +39 -0
  344. package/src/components/rating/rating-star.tsx +70 -0
  345. package/src/components/rating/rating-stars.tsx +219 -0
  346. package/src/components/rating/rating.context.tsx +79 -0
  347. package/src/components/rating/rating.tsx +163 -0
  348. package/src/components/rating/rating.variants.test.ts +347 -0
  349. package/src/components/rating/rating.variants.ts +354 -0
  350. package/src/components/selection-mode/AGENTS.md +155 -0
  351. package/src/components/selection-mode/index.ts +43 -0
  352. package/src/components/selection-mode/selection-mode-action.tsx +65 -0
  353. package/src/components/selection-mode/selection-mode-bar.tsx +79 -0
  354. package/src/components/selection-mode/selection-mode-group.tsx +123 -0
  355. package/src/components/selection-mode/selection-mode-header.tsx +85 -0
  356. package/src/components/selection-mode/selection-mode-indicator.tsx +118 -0
  357. package/src/components/selection-mode/selection-mode-item.tsx +182 -0
  358. package/src/components/selection-mode/selection-mode.context.tsx +89 -0
  359. package/src/components/selection-mode/selection-mode.tsx +207 -0
  360. package/src/components/selection-mode/selection-mode.types.ts +36 -0
  361. package/src/components/selection-mode/selection-mode.variants.test.ts +292 -0
  362. package/src/components/selection-mode/selection-mode.variants.ts +329 -0
  363. package/src/components/skeleton/AGENTS.md +120 -0
  364. package/src/components/skeleton/index.ts +15 -0
  365. package/src/components/skeleton/skeleton-clock.ts +48 -0
  366. package/src/components/skeleton/skeleton-group.tsx +64 -0
  367. package/src/components/skeleton/skeleton-shimmer.tsx +80 -0
  368. package/src/components/skeleton/skeleton.context.tsx +45 -0
  369. package/src/components/skeleton/skeleton.tsx +227 -0
  370. package/src/components/skeleton/skeleton.variants.test.ts +245 -0
  371. package/src/components/skeleton/skeleton.variants.ts +222 -0
  372. package/src/components/slide-button/AGENTS.md +122 -0
  373. package/src/components/slide-button/index.ts +43 -0
  374. package/src/components/slide-button/slide-button-label.tsx +70 -0
  375. package/src/components/slide-button/slide-button-thumb.tsx +93 -0
  376. package/src/components/slide-button/slide-button.context.tsx +76 -0
  377. package/src/components/slide-button/slide-button.tsx +457 -0
  378. package/src/components/slide-button/slide-button.variants.test.ts +288 -0
  379. package/src/components/slide-button/slide-button.variants.ts +324 -0
  380. package/src/components/slider/index.ts +2 -0
  381. package/src/components/slider/slider-thumb.tsx +33 -1
  382. package/src/components/slider/slider.variants.test.ts +52 -0
  383. package/src/components/slider/slider.variants.ts +56 -2
  384. package/src/components/stack-card/AGENTS.md +169 -0
  385. package/src/components/stack-card/index.ts +37 -0
  386. package/src/components/stack-card/stack-card-action.tsx +66 -0
  387. package/src/components/stack-card/stack-card-actions.tsx +18 -0
  388. package/src/components/stack-card/stack-card-card.tsx +71 -0
  389. package/src/components/stack-card/stack-card-empty.tsx +30 -0
  390. package/src/components/stack-card/stack-card-slot.tsx +90 -0
  391. package/src/components/stack-card/stack-card-stamp.tsx +76 -0
  392. package/src/components/stack-card/stack-card.context.tsx +117 -0
  393. package/src/components/stack-card/stack-card.tsx +557 -0
  394. package/src/components/stack-card/stack-card.types.ts +26 -0
  395. package/src/components/stack-card/stack-card.variants.test.ts +355 -0
  396. package/src/components/stack-card/stack-card.variants.ts +360 -0
  397. package/src/components/steps/AGENTS.md +153 -0
  398. package/src/components/steps/index.ts +45 -0
  399. package/src/components/steps/steps-connector.tsx +31 -0
  400. package/src/components/steps/steps-description.tsx +26 -0
  401. package/src/components/steps/steps-indicator.tsx +106 -0
  402. package/src/components/steps/steps-item.tsx +225 -0
  403. package/src/components/steps/steps-panel.tsx +41 -0
  404. package/src/components/steps/steps-title.tsx +32 -0
  405. package/src/components/steps/steps-track.tsx +59 -0
  406. package/src/components/steps/steps.context.tsx +123 -0
  407. package/src/components/steps/steps.tsx +166 -0
  408. package/src/components/steps/steps.variants.test.ts +388 -0
  409. package/src/components/steps/steps.variants.ts +468 -0
  410. package/src/components/surface/AGENTS.md +82 -0
  411. package/src/components/surface/index.ts +15 -0
  412. package/src/components/surface/surface.context.tsx +56 -0
  413. package/src/components/surface/surface.tsx +87 -0
  414. package/src/components/surface/surface.variants.test.ts +227 -0
  415. package/src/components/surface/surface.variants.ts +165 -0
  416. package/src/components/swipe/AGENTS.md +140 -0
  417. package/src/components/swipe/index.ts +31 -0
  418. package/src/components/swipe/swipe-action.tsx +99 -0
  419. package/src/components/swipe/swipe-end.tsx +14 -0
  420. package/src/components/swipe/swipe-group.tsx +55 -0
  421. package/src/components/swipe/swipe-panel.tsx +55 -0
  422. package/src/components/swipe/swipe-start.tsx +15 -0
  423. package/src/components/swipe/swipe.context.tsx +135 -0
  424. package/src/components/swipe/swipe.tsx +393 -0
  425. package/src/components/swipe/swipe.types.ts +7 -0
  426. package/src/components/swipe/swipe.variants.test.ts +337 -0
  427. package/src/components/swipe/swipe.variants.ts +290 -0
  428. package/src/components/switch/switch.variants.ts +2 -2
  429. package/src/components/tabs/AGENTS.md +4 -3
  430. package/src/components/tabs/tabs.variants.test.ts +1 -1
  431. package/src/components/tabs/tabs.variants.ts +4 -6
  432. package/src/components/text/AGENTS.md +8 -2
  433. package/src/components/text/text.tsx +7 -0
  434. package/src/components/text/text.variants.test.ts +24 -1
  435. package/src/components/text/text.variants.ts +5 -0
  436. package/src/components/textarea/AGENTS.md +89 -0
  437. package/src/components/textarea/index.ts +16 -0
  438. package/src/components/textarea/textarea.tsx +157 -0
  439. package/src/components/textarea/textarea.variants.test.ts +219 -0
  440. package/src/components/textarea/textarea.variants.ts +171 -0
  441. package/src/components/toast/AGENTS.md +140 -0
  442. package/src/components/toast/index.ts +74 -0
  443. package/src/components/toast/toast-action.tsx +59 -0
  444. package/src/components/toast/toast-api.ts +32 -0
  445. package/src/components/toast/toast-close.tsx +44 -0
  446. package/src/components/toast/toast-content.tsx +11 -0
  447. package/src/components/toast/toast-description.tsx +11 -0
  448. package/src/components/toast/toast-indicator.tsx +34 -0
  449. package/src/components/toast/toast-item.tsx +308 -0
  450. package/src/components/toast/toast-title.tsx +13 -0
  451. package/src/components/toast/toast-viewport.tsx +135 -0
  452. package/src/components/toast/toast.context.tsx +69 -0
  453. package/src/components/toast/toast.store.test.ts +259 -0
  454. package/src/components/toast/toast.store.ts +338 -0
  455. package/src/components/toast/toast.timer.test.ts +64 -0
  456. package/src/components/toast/toast.timer.ts +51 -0
  457. package/src/components/toast/toast.tsx +132 -0
  458. package/src/components/toast/toast.variants.test.ts +393 -0
  459. package/src/components/toast/toast.variants.ts +304 -0
  460. package/src/components/toggle-button/AGENTS.md +106 -0
  461. package/src/components/toggle-button/index.ts +30 -0
  462. package/src/components/toggle-button/toggle-button-group.tsx +156 -0
  463. package/src/components/toggle-button/toggle-button-label.tsx +19 -0
  464. package/src/components/toggle-button/toggle-button.context.tsx +130 -0
  465. package/src/components/toggle-button/toggle-button.tsx +172 -0
  466. package/src/components/toggle-button/toggle-button.variants.test.ts +189 -0
  467. package/src/components/toggle-button/toggle-button.variants.ts +160 -0
  468. package/src/components/tooltip/AGENTS.md +133 -0
  469. package/src/components/tooltip/index.ts +28 -0
  470. package/src/components/tooltip/tooltip-arrow.tsx +40 -0
  471. package/src/components/tooltip/tooltip-content.tsx +179 -0
  472. package/src/components/tooltip/tooltip-description.tsx +19 -0
  473. package/src/components/tooltip/tooltip-text.tsx +21 -0
  474. package/src/components/tooltip/tooltip-title.tsx +20 -0
  475. package/src/components/tooltip/tooltip-trigger.tsx +103 -0
  476. package/src/components/tooltip/tooltip.context.tsx +68 -0
  477. package/src/components/tooltip/tooltip.tsx +213 -0
  478. package/src/components/tooltip/tooltip.variants.test.ts +231 -0
  479. package/src/components/tooltip/tooltip.variants.ts +177 -0
  480. package/src/hooks/use-calm-motion.tsx +31 -0
  481. package/src/lib/calm-motion.test.ts +17 -0
  482. package/src/lib/calm-motion.ts +23 -0
  483. package/src/lib/cn.parity.test.ts +62 -0
  484. package/src/lib/cn.test.ts +14 -0
  485. package/src/lib/cn.ts +11 -6
  486. package/src/no-gorhom.test.ts +54 -0
  487. package/src/styles/geometry.test.ts +1 -0
  488. package/src/styles/theme.css +4 -0
  489. package/src/styles/tokens.css +44 -0
  490. package/src/styles/tokens.test.ts +56 -0
  491. package/src/styles/tokens.ts +29 -1
  492. package/src/components/bottom-sheet/bottom-sheet-portal.tsx +0 -72
  493. package/src/components/bottom-sheet/bottom-sheet.context.tsx +0 -135
@@ -0,0 +1,70 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
3
+ import { Text, type TextProps } from "../text";
4
+ import { useSlideButtonPart } from "./slide-button.context";
5
+ import {
6
+ resolveSlideLabelGutter,
7
+ resolveSlideTrailOpacity,
8
+ resolveSlideTrailWidth,
9
+ SLIDE_BUTTON_INSET,
10
+ slideButtonVariants,
11
+ } from "./slide-button.variants";
12
+
13
+ export type SlideButtonLabelProps = Omit<TextProps, "children" | "className"> & {
14
+ children: ReactNode;
15
+ className?: string;
16
+ };
17
+
18
+ /**
19
+ * What the slide does, centred in the whole rail.
20
+ *
21
+ * **It does not fade or move.** The handle passes over it. A label that faded as
22
+ * the handle advanced would leave the control saying nothing for the second half
23
+ * of the gesture — the half where the hand most wants to know what it is about to
24
+ * confirm.
25
+ *
26
+ * **It changes colour where the trail covers it.** A second copy, in the trail's
27
+ * own foreground, is clipped to the trail's width. A state variant's trail is
28
+ * its full colour, and its soft label colour on that fill is the same hue on the
29
+ * same hue — the words would vanish exactly where the trail reached them.
30
+ *
31
+ * **It keeps a handle's width clear on both sides.** Symmetric, so it stays
32
+ * centred in the rail; wide enough that a long label truncates before it runs
33
+ * under the resting handle.
34
+ *
35
+ * Its text is also the rail's accessibility label, read off by the root, so the
36
+ * screen reader announces the same words the eye reads. The rail is the
37
+ * accessible element, so neither copy is read on its own.
38
+ */
39
+ export function SlideButtonLabel({ className, ...props }: SlideButtonLabelProps): ReactElement {
40
+ const { variant, size, handleWidth, offset, travel } = useSlideButtonPart("SlideButton.Label");
41
+ const slots = slideButtonVariants({ size, variant });
42
+
43
+ const frameStyle = useAnimatedStyle(() => ({
44
+ paddingHorizontal: resolveSlideLabelGutter({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET }),
45
+ }));
46
+
47
+ const clipStyle = useAnimatedStyle(() => ({
48
+ opacity: handleWidth.value > 0 ? resolveSlideTrailOpacity(offset.value) : 0,
49
+ width: resolveSlideTrailWidth({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET, offset: offset.value }),
50
+ }));
51
+
52
+ const onTrailStyle = useAnimatedStyle(() => ({
53
+ paddingHorizontal: resolveSlideLabelGutter({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET }),
54
+ width: travel.value + handleWidth.value + SLIDE_BUTTON_INSET * 2,
55
+ }));
56
+
57
+ return (
58
+ <>
59
+ <Animated.View className={slots.labelFrame()} pointerEvents="none" style={frameStyle}>
60
+ <Text className={slots.label({ className })} numberOfLines={1} {...props} />
61
+ </Animated.View>
62
+ <Animated.View className={slots.labelClip()} pointerEvents="none" style={clipStyle}>
63
+ <Animated.View className={slots.labelOnTrailFrame()} style={onTrailStyle}>
64
+ <Text className={slots.labelOnTrail({ className })} numberOfLines={1} {...props} />
65
+ </Animated.View>
66
+ </Animated.View>
67
+ </>
68
+ );
69
+ }
70
+ SlideButtonLabel.displayName = "DelacourUI.SlideButton.Label";
@@ -0,0 +1,93 @@
1
+ import { type ReactElement, type ReactNode, useMemo } from "react";
2
+ import type { ViewProps } from "react-native";
3
+ import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import { useThemeColor } from "../../hooks/use-theme-color";
5
+ import { IconCheckmark1Small, IconChevronLeft, IconChevronRight } from "../../icons/central";
6
+ import { Icon, IconDefaultsProvider } from "../icon";
7
+ import { useSlideButtonPart } from "./slide-button.context";
8
+ import { SLIDE_BUTTON_GLYPH_TOKEN, slideButtonVariants } from "./slide-button.variants";
9
+
10
+ export type SlideButtonThumbProps = Omit<ViewProps, "children" | "style"> & {
11
+ /** Replaces the chevron. An `Icon` inherits the handle's glyph size and colour. */
12
+ children?: ReactNode;
13
+ className?: string;
14
+ };
15
+
16
+ /**
17
+ * The handle the finger drags.
18
+ *
19
+ * Composed in automatically when the children hold none, and drawn last however
20
+ * the children were ordered, so it always passes *over* the label.
21
+ *
22
+ * **It holds no gesture of its own.** One `Gesture.Pan()` on the rail drives it,
23
+ * so a drag that lands on the rail beside the handle still moves it — the same
24
+ * trade `Switch.Thumb` and `Slider.Thumb` make.
25
+ *
26
+ * **One animated style, one node**: width, visibility and position together,
27
+ * because two `useAnimatedStyle`s on one view fight for the same props.
28
+ *
29
+ * Invisible until the rail has been measured — a handle drawn before then has no
30
+ * width and sits at a garbage offset for a frame.
31
+ *
32
+ * With no children it draws a chevron pointing the way it travels (mirrored
33
+ * under RTL), which crosses into a tick when the slide confirms. Children replace
34
+ * both: a custom glyph is the caller's to change.
35
+ */
36
+ export function SlideButtonThumb({ className, children, ...props }: SlideButtonThumbProps): ReactElement {
37
+ const { size, variant, isRTL, offset, handleWidth } = useSlideButtonPart("SlideButton.Thumb");
38
+ const direction = isRTL ? -1 : 1;
39
+
40
+ const thumbStyle = useAnimatedStyle(() => ({
41
+ opacity: handleWidth.value > 0 ? 1 : 0,
42
+ transform: [{ translateX: offset.value * direction }],
43
+ width: handleWidth.value,
44
+ }));
45
+
46
+ const slots = slideButtonVariants({ size, variant });
47
+ const glyphColor = useThemeColor(SLIDE_BUTTON_GLYPH_TOKEN);
48
+ const glyphClassName = slots.thumbGlyph();
49
+ const iconDefaults = useMemo(
50
+ () => ({ className: glyphClassName, color: glyphColor ?? "" }),
51
+ [glyphClassName, glyphColor]
52
+ );
53
+
54
+ return (
55
+ <Animated.View className={slots.thumb({ className })} style={thumbStyle} {...props}>
56
+ <IconDefaultsProvider value={iconDefaults}>{children ?? <SlideButtonGlyph />}</IconDefaultsProvider>
57
+ </Animated.View>
58
+ );
59
+ }
60
+ SlideButtonThumb.displayName = "DelacourUI.SlideButton.Thumb";
61
+
62
+ /**
63
+ * The default glyph: a chevron toward the travel that crosses into a tick.
64
+ *
65
+ * Both marks are always mounted, stacked, and swap by opacity and a quarter turn
66
+ * off one shared value — so the crossing runs on the UI thread and never waits on
67
+ * a re-render to start.
68
+ */
69
+ function SlideButtonGlyph(): ReactElement {
70
+ const { isRTL, glyph } = useSlideButtonPart("SlideButton.Thumb");
71
+ const direction = isRTL ? -1 : 1;
72
+
73
+ const chevronStyle = useAnimatedStyle(() => ({
74
+ opacity: 1 - glyph.value,
75
+ transform: [{ rotate: `${glyph.value * 90 * direction}deg` }],
76
+ }));
77
+ const tickStyle = useAnimatedStyle(() => ({
78
+ opacity: glyph.value,
79
+ transform: [{ rotate: `${(glyph.value - 1) * 90 * direction}deg` }],
80
+ }));
81
+
82
+ return (
83
+ <>
84
+ <Animated.View className="absolute" style={chevronStyle}>
85
+ <Icon icon={isRTL ? IconChevronLeft : IconChevronRight} />
86
+ </Animated.View>
87
+ <Animated.View className="absolute" style={tickStyle}>
88
+ <Icon icon={IconCheckmark1Small} />
89
+ </Animated.View>
90
+ </>
91
+ );
92
+ }
93
+ SlideButtonGlyph.displayName = "DelacourUI.SlideButton.Thumb.Glyph";
@@ -0,0 +1,76 @@
1
+ import { createContext, type ReactElement, type ReactNode, use } from "react";
2
+ import type { SharedValue } from "react-native-reanimated";
3
+ import type { SlideButtonSize, SlideButtonVariant } from "./slide-button.variants";
4
+
5
+ export type SlideButtonContextValue = {
6
+ variant: SlideButtonVariant;
7
+ size: SlideButtonSize;
8
+ /** Whether the slide has been confirmed, on the **JS thread** — what accessibility reads. */
9
+ isCompleted: boolean;
10
+ isDisabled: boolean;
11
+ /** Whether the layout runs right to left, so the handle travels leftwards. */
12
+ isRTL: boolean;
13
+ /**
14
+ * How far the handle has travelled from its resting end, in points, on the
15
+ * **UI thread**. Always positive — the direction is applied where it is drawn.
16
+ */
17
+ offset: SharedValue<number>;
18
+ /** How far the handle can travel, in points. `0` until the rail has been measured. */
19
+ travel: SharedValue<number>;
20
+ /** The handle's width, from the rail's measured height. `0` until then. */
21
+ handleWidth: SharedValue<number>;
22
+ /** The chevron-to-tick crossing: `0` shows the chevron, `1` the tick. */
23
+ glyph: SharedValue<number>;
24
+ };
25
+
26
+ const SlideButtonContext = createContext<SlideButtonContextValue | null>(null);
27
+
28
+ /**
29
+ * Supplies one slide button's state and shared values to its own parts.
30
+ *
31
+ * Lives in its own module, importing nothing but React and types, so a part can
32
+ * read it without importing `./slide-button` — that import would close a cycle.
33
+ */
34
+ export function SlideButtonProvider({
35
+ value,
36
+ children,
37
+ }: {
38
+ value: SlideButtonContextValue;
39
+ children: ReactNode;
40
+ }): ReactElement {
41
+ return <SlideButtonContext value={value}>{children}</SlideButtonContext>;
42
+ }
43
+ SlideButtonProvider.displayName = "DelacourUI.SlideButton.Provider";
44
+
45
+ /** The enclosing slide button's state, or null outside a `<SlideButton>`. */
46
+ export function useSlideButtonContext(): SlideButtonContextValue | null {
47
+ return use(SlideButtonContext);
48
+ }
49
+
50
+ /**
51
+ * Reads the enclosing slide button's state.
52
+ *
53
+ * Lets a custom handle or label match the control — read `offset` against
54
+ * `travel` to draw anything that follows the drag. Throws outside a
55
+ * `<SlideButton>`; use {@link useSlideButtonContext} where it is optional.
56
+ */
57
+ export function useSlideButton(): SlideButtonContextValue {
58
+ const context = useSlideButtonContext();
59
+ if (!context) {
60
+ throw new Error("useSlideButton must be called inside a <SlideButton>.");
61
+ }
62
+ return context;
63
+ }
64
+
65
+ /**
66
+ * The enclosing slide button's state, for a compound part that cannot work without one.
67
+ *
68
+ * Internal: deliberately not re-exported from `index.ts`.
69
+ */
70
+ export function useSlideButtonPart(component: string): SlideButtonContextValue {
71
+ const context = useSlideButtonContext();
72
+ if (!context) {
73
+ throw new Error(`${component} must be rendered inside a <SlideButton>.`);
74
+ }
75
+ return context;
76
+ }
@@ -0,0 +1,457 @@
1
+ import {
2
+ Children,
3
+ isValidElement,
4
+ type ReactElement,
5
+ type ReactNode,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from "react";
11
+ import { type AccessibilityActionEvent, I18nManager, type LayoutChangeEvent, type ViewProps } from "react-native";
12
+ import { Gesture, GestureDetector } from "react-native-gesture-handler";
13
+ import Animated, {
14
+ cancelAnimation,
15
+ type SharedValue,
16
+ useAnimatedStyle,
17
+ useReducedMotion,
18
+ useSharedValue,
19
+ withSpring,
20
+ withTiming,
21
+ } from "react-native-reanimated";
22
+ import { scheduleOnRN } from "react-native-worklets";
23
+ import { useControllableState } from "../../hooks/use-controllable-state";
24
+ import { type HapticFeedback, playHaptic } from "../pressable";
25
+ import { type SlideButtonContextValue, SlideButtonProvider } from "./slide-button.context";
26
+ import {
27
+ isSlideArmed,
28
+ resolveSlideHandleWidth,
29
+ resolveSlideRelease,
30
+ resolveSlideThreshold,
31
+ resolveSlideTrailOpacity,
32
+ resolveSlideTrailWidth,
33
+ resolveSlideTravel,
34
+ SLIDE_BUTTON_ACTIVE_OFFSET_X,
35
+ SLIDE_BUTTON_DEFAULT_AUTO_RESET_MS,
36
+ SLIDE_BUTTON_DEFAULT_SIZE,
37
+ SLIDE_BUTTON_DEFAULT_VARIANT,
38
+ SLIDE_BUTTON_FAIL_OFFSET_Y,
39
+ SLIDE_BUTTON_GLYPH_MS,
40
+ SLIDE_BUTTON_INSET,
41
+ SLIDE_BUTTON_LOOKAHEAD,
42
+ SLIDE_BUTTON_REDUCED_MOTION_MS,
43
+ SLIDE_BUTTON_SPRING,
44
+ type SlideButtonSize,
45
+ type SlideButtonVariant,
46
+ slideButtonVariants,
47
+ } from "./slide-button.variants";
48
+ import { SlideButtonLabel } from "./slide-button-label";
49
+ import { SlideButtonThumb } from "./slide-button-thumb";
50
+
51
+ export type SlideButtonProps = Omit<ViewProps, "children" | "style"> & {
52
+ children: ReactNode;
53
+ /** Classes for the rail. */
54
+ className?: string;
55
+ variant?: SlideButtonVariant;
56
+ size?: SlideButtonSize;
57
+ /** Stretches the rail across its parent. Otherwise it is a fixed 288pt. */
58
+ isFullWidth?: boolean;
59
+ /** How far along the travel a release has to reach, as a fraction from 0.1 to 1. Default 0.9. */
60
+ threshold?: number;
61
+ /**
62
+ * Called once when the slide confirms. Return a promise and a rejection takes
63
+ * the handle home again, so a failed request does not look confirmed.
64
+ */
65
+ onComplete?: () => void;
66
+ /** Controlled state. Pass nothing and the button holds its own. */
67
+ isCompleted?: boolean;
68
+ /** Starting state while uncontrolled. */
69
+ defaultCompleted?: boolean;
70
+ onCompletedChange?: (isCompleted: boolean) => void;
71
+ /** Takes the handle home on its own, `autoResetDelay` after it confirms. */
72
+ isAutoReset?: boolean;
73
+ /** In milliseconds. Default 1000. */
74
+ autoResetDelay?: number;
75
+ /** Fades the control and refuses the drag — the handle does not move at all. */
76
+ isDisabled?: boolean;
77
+ /** Played as a drag crosses the threshold; a success knock follows on commit. Off by default. */
78
+ haptic?: false | HapticFeedback;
79
+ /** What the screen reader's activate action is called. Imperative, naming the outcome. Default "Confirm". */
80
+ accessibilityActionLabel?: string;
81
+ };
82
+
83
+ function SlideButtonRoot({
84
+ children,
85
+ className,
86
+ variant = SLIDE_BUTTON_DEFAULT_VARIANT,
87
+ size = SLIDE_BUTTON_DEFAULT_SIZE,
88
+ isFullWidth = false,
89
+ threshold,
90
+ onComplete,
91
+ isCompleted,
92
+ defaultCompleted = false,
93
+ onCompletedChange,
94
+ isAutoReset = false,
95
+ autoResetDelay = SLIDE_BUTTON_DEFAULT_AUTO_RESET_MS,
96
+ isDisabled = false,
97
+ haptic = false,
98
+ accessibilityActionLabel = "Confirm",
99
+ accessibilityLabel,
100
+ ...props
101
+ }: SlideButtonProps): ReactElement {
102
+ const [completed, setCompleted] = useControllableState({
103
+ defaultValue: defaultCompleted,
104
+ onChange: onCompletedChange,
105
+ value: isCompleted,
106
+ });
107
+
108
+ const isRTL = I18nManager.isRTL;
109
+ const direction = isRTL ? -1 : 1;
110
+ const isReducedMotion = useReducedMotion();
111
+ const resolvedThreshold = resolveSlideThreshold(threshold);
112
+
113
+ const offset = useSharedValue(0);
114
+ const travel = useSharedValue(0);
115
+ const handleWidth = useSharedValue(0);
116
+ const glyph = useSharedValue(completed ? 1 : 0);
117
+ // Where the handle was when the drag took hold, so picking it up mid-spring
118
+ // does not snap it to the finger.
119
+ const grabbed = useSharedValue(0);
120
+ // Whether the current drag has crossed the threshold — the arming haptic
121
+ // fires on the crossing, not on every frame past it.
122
+ const armed = useSharedValue(false);
123
+ // Whether the handle belongs at the far end: confirmed, or requested and not
124
+ // yet answered. A pinned handle takes no drag and follows a re-layout.
125
+ const pinned = useSharedValue(completed);
126
+
127
+ // A request is in flight: the drag confirmed, `onComplete` ran, and a
128
+ // controlled parent has not said `isCompleted` yet. A ref, because nothing
129
+ // renders differently for it — the handle already sits at the end.
130
+ const isPending = useRef(false);
131
+
132
+ const moveTo = useCallback(
133
+ (target: number) => {
134
+ offset.value = isReducedMotion
135
+ ? withTiming(target, { duration: SLIDE_BUTTON_REDUCED_MOTION_MS })
136
+ : withSpring(target, SLIDE_BUTTON_SPRING);
137
+ },
138
+ [isReducedMotion, offset]
139
+ );
140
+
141
+ const goHome = useCallback(() => {
142
+ pinned.value = false;
143
+ moveTo(0);
144
+ glyph.value = withTiming(0, { duration: SLIDE_BUTTON_GLYPH_MS });
145
+ }, [glyph, moveTo, pinned]);
146
+
147
+ const onCompleteRef = useRef(onComplete);
148
+ onCompleteRef.current = onComplete;
149
+
150
+ const reject = useCallback(() => {
151
+ isPending.current = false;
152
+ setCompleted(false);
153
+ goHome();
154
+ }, [goHome, setCompleted]);
155
+
156
+ const complete = useCallback(() => {
157
+ isPending.current = true;
158
+ setCompleted(true);
159
+ // Typed `void` so any function fits; read as `unknown` to find a promise in it.
160
+ const result: unknown = onCompleteRef.current?.();
161
+ if (isPromiseLike(result)) result.then(undefined, reject);
162
+ }, [reject, setCompleted]);
163
+
164
+ useEffect(() => {
165
+ if (completed) {
166
+ isPending.current = false;
167
+ pinned.value = true;
168
+ moveTo(travel.value);
169
+ glyph.value = withTiming(1, { duration: SLIDE_BUTTON_GLYPH_MS });
170
+ return;
171
+ }
172
+ // Completion was requested and has not been answered. The handle stays at
173
+ // the end rather than springing back, which would read as a refusal.
174
+ if (isPending.current) return;
175
+ goHome();
176
+ }, [completed, glyph, goHome, moveTo, pinned, travel]);
177
+
178
+ useEffect(() => () => cancelAnimation(offset), [offset]);
179
+
180
+ useEffect(() => {
181
+ if (!completed || !isAutoReset) return;
182
+ const timer = setTimeout(() => setCompleted(false), autoResetDelay);
183
+ return () => clearTimeout(timer);
184
+ }, [autoResetDelay, completed, isAutoReset, setCompleted]);
185
+
186
+ const handleLayout = useCallback(
187
+ (event: LayoutChangeEvent) => {
188
+ const { width, height } = event.nativeEvent.layout;
189
+ const nextHandle = resolveSlideHandleWidth({ inset: SLIDE_BUTTON_INSET, railHeight: height });
190
+ const nextTravel = resolveSlideTravel({ handleWidth: nextHandle, inset: SLIDE_BUTTON_INSET, railWidth: width });
191
+ handleWidth.value = nextHandle;
192
+ travel.value = nextTravel;
193
+ if (pinned.value) offset.value = nextTravel;
194
+ else if (offset.value > nextTravel) offset.value = nextTravel;
195
+ },
196
+ [handleWidth, offset, pinned, travel]
197
+ );
198
+
199
+ const gesture = useMemo(() => {
200
+ const settle = (target: number, velocity: number) => {
201
+ "worklet";
202
+ offset.value = isReducedMotion
203
+ ? withTiming(target, { duration: SLIDE_BUTTON_REDUCED_MOTION_MS })
204
+ : withSpring(target, { ...SLIDE_BUTTON_SPRING, velocity });
205
+ };
206
+
207
+ // Disabled still claims the drag and then does nothing with it. A disabled
208
+ // recognizer lets the touch through to whatever sits behind — inside a
209
+ // stack with a full-screen back swipe, a drag on a refused slide navigated
210
+ // back.
211
+ return Gesture.Pan()
212
+ .activeOffsetX([-SLIDE_BUTTON_ACTIVE_OFFSET_X, SLIDE_BUTTON_ACTIVE_OFFSET_X])
213
+ .failOffsetY([-SLIDE_BUTTON_FAIL_OFFSET_Y, SLIDE_BUTTON_FAIL_OFFSET_Y])
214
+ .onStart(() => {
215
+ "worklet";
216
+ if (isDisabled || pinned.value) return;
217
+ cancelAnimation(offset);
218
+ grabbed.value = offset.value;
219
+ armed.value = false;
220
+ })
221
+ .onUpdate((event) => {
222
+ "worklet";
223
+ const span = travel.value;
224
+ if (isDisabled || pinned.value || span <= 0) return;
225
+
226
+ // No easing on the way out: the handle tracks the finger exactly.
227
+ const next = Math.min(span, Math.max(0, grabbed.value + event.translationX * direction));
228
+ offset.value = next;
229
+
230
+ // The tick plays on the crossing, not on every frame past it, and
231
+ // re-arms if the drag drops back under.
232
+ const isArmed = isSlideArmed({ offset: next, threshold: resolvedThreshold, travel: span });
233
+ if (isArmed === armed.value) return;
234
+ armed.value = isArmed;
235
+ if (isArmed && haptic !== false) playHaptic(haptic);
236
+ })
237
+ .onEnd((event, success) => {
238
+ "worklet";
239
+ if (isDisabled || pinned.value) return;
240
+ armed.value = false;
241
+ const velocity = event.velocityX * direction;
242
+ const release = resolveSlideRelease({
243
+ lookahead: SLIDE_BUTTON_LOOKAHEAD,
244
+ offset: offset.value,
245
+ threshold: resolvedThreshold,
246
+ travel: travel.value,
247
+ velocity,
248
+ });
249
+
250
+ // A pan cancelled by a scroll taking over goes home whatever its position.
251
+ if (!success || release === "return") {
252
+ settle(0, velocity);
253
+ return;
254
+ }
255
+
256
+ pinned.value = true;
257
+ settle(travel.value, velocity);
258
+ if (haptic !== false) playHaptic("success");
259
+ scheduleOnRN(complete);
260
+ });
261
+ }, [
262
+ armed,
263
+ complete,
264
+ direction,
265
+ grabbed,
266
+ haptic,
267
+ isDisabled,
268
+ isReducedMotion,
269
+ offset,
270
+ pinned,
271
+ resolvedThreshold,
272
+ travel,
273
+ ]);
274
+
275
+ const activate = useCallback(() => {
276
+ if (isDisabled || pinned.value) return;
277
+ pinned.value = true;
278
+ moveTo(travel.value);
279
+ complete();
280
+ }, [complete, isDisabled, moveTo, pinned, travel]);
281
+
282
+ const handleAccessibilityAction = useCallback(
283
+ (event: AccessibilityActionEvent) => {
284
+ if (event.nativeEvent.actionName === "activate") activate();
285
+ },
286
+ [activate]
287
+ );
288
+
289
+ const accessibilityActions = useMemo(
290
+ () => [{ label: accessibilityActionLabel, name: "activate" }],
291
+ [accessibilityActionLabel]
292
+ );
293
+
294
+ const context = useMemo<SlideButtonContextValue>(
295
+ () => ({
296
+ glyph,
297
+ handleWidth,
298
+ isCompleted: completed,
299
+ isDisabled,
300
+ isRTL,
301
+ offset,
302
+ size,
303
+ travel,
304
+ variant,
305
+ }),
306
+ [completed, glyph, handleWidth, isDisabled, offset, size, travel, variant]
307
+ );
308
+
309
+ const content = useMemo(() => withThumb(children), [children]);
310
+ const label = accessibilityLabel ?? labelText(children);
311
+ const slots = slideButtonVariants({ isDisabled, isFullWidth, size, variant });
312
+
313
+ return (
314
+ <SlideButtonProvider value={context}>
315
+ <GestureDetector gesture={gesture}>
316
+ <Animated.View
317
+ accessibilityActions={accessibilityActions}
318
+ accessibilityLabel={label}
319
+ accessibilityRole="button"
320
+ accessibilityState={{ checked: completed, disabled: isDisabled }}
321
+ accessible
322
+ className={slots.root({ className })}
323
+ onAccessibilityAction={handleAccessibilityAction}
324
+ onAccessibilityTap={activate}
325
+ onLayout={handleLayout}
326
+ {...props}
327
+ >
328
+ <SlideButtonTrail className={slots.trail()} handleWidth={handleWidth} offset={offset} />
329
+ {content}
330
+ </Animated.View>
331
+ </GestureDetector>
332
+ </SlideButtonProvider>
333
+ );
334
+ }
335
+
336
+ /**
337
+ * The fill behind the handle, from the rail's start edge to the handle's middle.
338
+ *
339
+ * It ends *under* the handle rather than at its rear edge: a stadium's rear is a
340
+ * curve, and a trail that stopped at its straight edge would leave a sliver of
341
+ * rail showing above and below that curve. Ending under the handle means the two
342
+ * read as one shape — the handle dragging its colour along.
343
+ *
344
+ * Internal: the root renders it, so it lives here rather than in a part file.
345
+ */
346
+ function SlideButtonTrail({
347
+ className,
348
+ offset,
349
+ handleWidth,
350
+ }: {
351
+ className: string;
352
+ offset: SharedValue<number>;
353
+ handleWidth: SharedValue<number>;
354
+ }): ReactElement {
355
+ const style = useAnimatedStyle(() => ({
356
+ opacity: handleWidth.value > 0 ? resolveSlideTrailOpacity(offset.value) : 0,
357
+ width: resolveSlideTrailWidth({ handleWidth: handleWidth.value, inset: SLIDE_BUTTON_INSET, offset: offset.value }),
358
+ }));
359
+ return <Animated.View className={className} style={style} />;
360
+ }
361
+ SlideButtonTrail.displayName = "DelacourUI.SlideButton.Trail";
362
+
363
+ /**
364
+ * Composes a `SlideButton.Thumb` in when the children hold none, and puts it
365
+ * last however it was written, so the handle always passes over the label —
366
+ * `Switch`'s rule for its thumb, for the same reason.
367
+ */
368
+ function withThumb(children: ReactNode): ReactNode {
369
+ const items = Children.toArray(children);
370
+ const isThumb = items.map((child) => isPart(child, SlideButtonThumb));
371
+
372
+ if (!isThumb.includes(true)) return [...items, <SlideButtonThumb key="thumb" />];
373
+
374
+ return [...items.filter((_, index) => !isThumb[index]), ...items.filter((_, index) => isThumb[index])];
375
+ }
376
+
377
+ /**
378
+ * The label's text, for the rail's accessibility label.
379
+ *
380
+ * Only plain text is read. A label holding anything richer needs an explicit
381
+ * `accessibilityLabel` on the root.
382
+ */
383
+ function labelText(children: ReactNode): string | undefined {
384
+ for (const child of Children.toArray(children)) {
385
+ if (!isPart(child, SlideButtonLabel) || !isValidElement<{ children?: ReactNode }>(child)) continue;
386
+ const parts = Children.toArray(child.props.children).filter(
387
+ (part): part is string | number => typeof part === "string" || typeof part === "number"
388
+ );
389
+ if (parts.length > 0) return parts.join("");
390
+ }
391
+ return undefined;
392
+ }
393
+
394
+ /** Whether `onComplete` handed back something to wait on. */
395
+ function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
396
+ return typeof value === "object" && value !== null && typeof (value as { then?: unknown }).then === "function";
397
+ }
398
+
399
+ /**
400
+ * Whether a child is a given part — by identity first, then by `displayName`,
401
+ * which survives React Compiler and a second module instance where identity does
402
+ * not. See `isSwitchThumbElement` for the afternoon that rule cost.
403
+ */
404
+ function isPart(child: ReactNode, part: { displayName?: string }): boolean {
405
+ if (!isValidElement(child)) return false;
406
+ if (child.type === part) return true;
407
+
408
+ const type = child.type as { displayName?: string } | null;
409
+ return type?.displayName !== undefined && type.displayName === part.displayName;
410
+ }
411
+
412
+ /**
413
+ * A control confirmed by dragging a handle across a rail — "Slide to ship".
414
+ *
415
+ * For an action that deserves deliberation where a dialog would be too much
416
+ * ceremony: a stray tap cannot reach it, and neither can habit. The handle tracks
417
+ * the finger exactly; only the release is sprung. A release past `threshold`
418
+ * (0.9 by default) confirms, with a small look-ahead so a committed flick near
419
+ * the end counts — but a flick from halfway never does, and `threshold={1}`
420
+ * demands the far end.
421
+ *
422
+ * **State works either way.** Uncontrolled, the handle confirms and stays at the
423
+ * end, ticked. Controlled, a confirmed slide reports `onCompletedChange(true)` and
424
+ * the handle waits at the end until `isCompleted` answers — so slow work shows as
425
+ * a held handle, not a bounce. `isAutoReset` takes it home after `autoResetDelay`.
426
+ *
427
+ * The label is centred in the whole rail and never fades. A `SlideButton.Thumb` is
428
+ * composed in when none is written; write one to put a different glyph on the handle.
429
+ *
430
+ * Under RTL the handle rests at the right and travels left. A screen reader's
431
+ * activate confirms it outright, named by `accessibilityActionLabel`.
432
+ *
433
+ * @example
434
+ * <SlideButton onComplete={ship}>
435
+ * <SlideButton.Label>Slide to ship</SlideButton.Label>
436
+ * </SlideButton>
437
+ *
438
+ * @example
439
+ * <SlideButton variant="destructive" threshold={1} accessibilityActionLabel="Delete account" onComplete={remove}>
440
+ * <SlideButton.Label>Slide to delete</SlideButton.Label>
441
+ * </SlideButton>
442
+ *
443
+ * @example
444
+ * <SlideButton isFullWidth haptic="selection" onComplete={unlock}>
445
+ * <SlideButton.Label>Slide to unlock</SlideButton.Label>
446
+ * <SlideButton.Thumb>
447
+ * <Icon icon={IconUnlocked} />
448
+ * </SlideButton.Thumb>
449
+ * </SlideButton>
450
+ */
451
+ export const SlideButton = Object.assign(SlideButtonRoot, {
452
+ /** What the slide does, centred in the rail. Never fades. */
453
+ Label: SlideButtonLabel,
454
+ /** The handle. Composed in automatically; write it out to change its glyph. */
455
+ Thumb: SlideButtonThumb,
456
+ displayName: "DelacourUI.SlideButton",
457
+ });