@teamofsilicons/silicon-ui 0.0.0-stage → 0.2.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 (354) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +81 -2
  3. package/check.mjs +236 -0
  4. package/components/accordion/accordion.d.ts +13 -0
  5. package/components/accordion/accordion.js +35 -0
  6. package/components/accordion/accordion.module.css +15 -0
  7. package/components/action-button/action-button.d.ts +11 -0
  8. package/components/action-button/action-button.js +57 -0
  9. package/components/action-button/action-button.module.css +15 -0
  10. package/components/activity-heatmap/activity-heatmap.d.ts +37 -0
  11. package/components/activity-heatmap/activity-heatmap.js +303 -0
  12. package/components/activity-heatmap/activity-heatmap.module.css +83 -0
  13. package/components/alert/alert.d.ts +13 -0
  14. package/components/alert/alert.js +79 -0
  15. package/components/alert/alert.module.css +15 -0
  16. package/components/animated-counter/animated-counter.d.ts +13 -0
  17. package/components/animated-counter/animated-counter.js +73 -0
  18. package/components/animated-counter/animated-counter.module.css +12 -0
  19. package/components/announcement-bar/announcement-bar.d.ts +53 -0
  20. package/components/announcement-bar/announcement-bar.js +187 -0
  21. package/components/announcement-bar/announcement-bar.module.css +60 -0
  22. package/components/avatar/avatar.d.ts +8 -0
  23. package/components/avatar/avatar.js +18 -0
  24. package/components/avatar/avatar.module.css +60 -0
  25. package/components/avatar-group/avatar-group.d.ts +13 -0
  26. package/components/avatar-group/avatar-group.js +24 -0
  27. package/components/avatar-group/avatar-group.module.css +27 -0
  28. package/components/badge/badge.d.ts +10 -0
  29. package/components/badge/badge.js +66 -0
  30. package/components/badge/badge.module.css +52 -0
  31. package/components/bar-chart/bar-chart.d.ts +32 -0
  32. package/components/bar-chart/bar-chart.js +327 -0
  33. package/components/bar-chart/bar-chart.module.css +49 -0
  34. package/components/billing-toggle/billing-toggle.d.ts +35 -0
  35. package/components/billing-toggle/billing-toggle.js +83 -0
  36. package/components/billing-toggle/billing-toggle.module.css +139 -0
  37. package/components/bottom-sheet/bottom-sheet.d.ts +27 -0
  38. package/components/bottom-sheet/bottom-sheet.js +386 -0
  39. package/components/bottom-sheet/bottom-sheet.module.css +69 -0
  40. package/components/breadcrumb/breadcrumb.d.ts +13 -0
  41. package/components/breadcrumb/breadcrumb.js +19 -0
  42. package/components/breadcrumb/breadcrumb.module.css +12 -0
  43. package/components/brush-chart/brush-chart.d.ts +45 -0
  44. package/components/brush-chart/brush-chart.js +426 -0
  45. package/components/brush-chart/brush-chart.module.css +69 -0
  46. package/components/button/button.d.ts +10 -0
  47. package/components/button/button.js +97 -0
  48. package/components/button/button.module.css +39 -0
  49. package/components/button-group/button-group.d.ts +59 -0
  50. package/components/button-group/button-group.js +280 -0
  51. package/components/button-group/button-group.module.css +87 -0
  52. package/components/button-link/button-link.d.ts +24 -0
  53. package/components/button-link/button-link.js +11 -0
  54. package/components/button-link/button-link.module.css +7 -0
  55. package/components/calendar/calendar.d.ts +22 -0
  56. package/components/calendar/calendar.js +253 -0
  57. package/components/calendar/calendar.module.css +51 -0
  58. package/components/card/card.d.ts +19 -0
  59. package/components/card/card.js +108 -0
  60. package/components/card/card.module.css +51 -0
  61. package/components/card-stack/card-stack.d.ts +34 -0
  62. package/components/card-stack/card-stack.js +291 -0
  63. package/components/card-stack/card-stack.module.css +63 -0
  64. package/components/carousel/carousel.d.ts +28 -0
  65. package/components/carousel/carousel.js +332 -0
  66. package/components/carousel/carousel.module.css +42 -0
  67. package/components/chat-thread/chat-thread.d.ts +135 -0
  68. package/components/chat-thread/chat-thread.js +406 -0
  69. package/components/chat-thread/chat-thread.module.css +242 -0
  70. package/components/checkbox/checkbox.d.ts +7 -0
  71. package/components/checkbox/checkbox.js +23 -0
  72. package/components/checkbox/checkbox.module.css +15 -0
  73. package/components/chip-group/chip-group.d.ts +24 -0
  74. package/components/chip-group/chip-group.js +196 -0
  75. package/components/chip-group/chip-group.module.css +38 -0
  76. package/components/code-block/code-block.d.ts +11 -0
  77. package/components/code-block/code-block.js +133 -0
  78. package/components/code-block/code-block.module.css +43 -0
  79. package/components/color-picker/color-picker.d.ts +40 -0
  80. package/components/color-picker/color-picker.js +449 -0
  81. package/components/color-picker/color-picker.module.css +111 -0
  82. package/components/combobox/combobox.d.ts +19 -0
  83. package/components/combobox/combobox.js +167 -0
  84. package/components/combobox/combobox.module.css +28 -0
  85. package/components/command-palette/command-palette.d.ts +20 -0
  86. package/components/command-palette/command-palette.js +151 -0
  87. package/components/command-palette/command-palette.module.css +94 -0
  88. package/components/comment-thread/comment-thread.d.ts +120 -0
  89. package/components/comment-thread/comment-thread.js +343 -0
  90. package/components/comment-thread/comment-thread.module.css +195 -0
  91. package/components/confirm-morph/confirm-morph.d.ts +65 -0
  92. package/components/confirm-morph/confirm-morph.js +355 -0
  93. package/components/confirm-morph/confirm-morph.module.css +146 -0
  94. package/components/context-menu/context-menu.d.ts +18 -0
  95. package/components/context-menu/context-menu.js +148 -0
  96. package/components/context-menu/context-menu.module.css +17 -0
  97. package/components/copy-button/copy-button.d.ts +10 -0
  98. package/components/copy-button/copy-button.js +69 -0
  99. package/components/copy-button/copy-button.module.css +21 -0
  100. package/components/countdown/countdown.d.ts +31 -0
  101. package/components/countdown/countdown.js +177 -0
  102. package/components/countdown/countdown.module.css +49 -0
  103. package/components/date-picker/date-picker.d.ts +17 -0
  104. package/components/date-picker/date-picker.js +100 -0
  105. package/components/date-picker/date-picker.module.css +34 -0
  106. package/components/date-range-picker/date-range-picker.d.ts +44 -0
  107. package/components/date-range-picker/date-range-picker.js +534 -0
  108. package/components/date-range-picker/date-range-picker.module.css +214 -0
  109. package/components/dialog/dialog.d.ts +11 -0
  110. package/components/dialog/dialog.js +52 -0
  111. package/components/dialog/dialog.module.css +36 -0
  112. package/components/donut-chart/donut-chart.d.ts +46 -0
  113. package/components/donut-chart/donut-chart.js +382 -0
  114. package/components/donut-chart/donut-chart.module.css +48 -0
  115. package/components/drawer/drawer.d.ts +20 -0
  116. package/components/drawer/drawer.js +111 -0
  117. package/components/drawer/drawer.module.css +97 -0
  118. package/components/dropdown-menu/dropdown-menu.d.ts +16 -0
  119. package/components/dropdown-menu/dropdown-menu.js +39 -0
  120. package/components/dropdown-menu/dropdown-menu.module.css +38 -0
  121. package/components/elastic-slider/elastic-slider.d.ts +39 -0
  122. package/components/elastic-slider/elastic-slider.js +261 -0
  123. package/components/elastic-slider/elastic-slider.module.css +40 -0
  124. package/components/empty-state/empty-state.d.ts +12 -0
  125. package/components/empty-state/empty-state.js +69 -0
  126. package/components/empty-state/empty-state.module.css +13 -0
  127. package/components/expandable-card/expandable-card.d.ts +13 -0
  128. package/components/expandable-card/expandable-card.js +89 -0
  129. package/components/expandable-card/expandable-card.module.css +22 -0
  130. package/components/expanding-button-group/expanding-button-group.d.ts +41 -0
  131. package/components/expanding-button-group/expanding-button-group.js +258 -0
  132. package/components/expanding-button-group/expanding-button-group.module.css +44 -0
  133. package/components/expanding-search/expanding-search.d.ts +38 -0
  134. package/components/expanding-search/expanding-search.js +242 -0
  135. package/components/expanding-search/expanding-search.module.css +52 -0
  136. package/components/file-dropzone/file-dropzone.d.ts +44 -0
  137. package/components/file-dropzone/file-dropzone.js +379 -0
  138. package/components/file-dropzone/file-dropzone.module.css +115 -0
  139. package/components/file-upload/file-upload.d.ts +22 -0
  140. package/components/file-upload/file-upload.js +192 -0
  141. package/components/file-upload/file-upload.module.css +49 -0
  142. package/components/filter-toolbar/filter-toolbar.d.ts +48 -0
  143. package/components/filter-toolbar/filter-toolbar.js +446 -0
  144. package/components/filter-toolbar/filter-toolbar.module.css +70 -0
  145. package/components/floating-button-group/floating-button-group.d.ts +51 -0
  146. package/components/floating-button-group/floating-button-group.js +215 -0
  147. package/components/floating-button-group/floating-button-group.module.css +74 -0
  148. package/components/gauge/gauge.d.ts +18 -0
  149. package/components/gauge/gauge.js +101 -0
  150. package/components/gauge/gauge.module.css +22 -0
  151. package/components/hold-to-confirm/hold-to-confirm.d.ts +25 -0
  152. package/components/hold-to-confirm/hold-to-confirm.js +165 -0
  153. package/components/hold-to-confirm/hold-to-confirm.module.css +20 -0
  154. package/components/hover-card/hover-card.d.ts +38 -0
  155. package/components/hover-card/hover-card.js +145 -0
  156. package/components/hover-card/hover-card.module.css +20 -0
  157. package/components/image-compare/image-compare.d.ts +27 -0
  158. package/components/image-compare/image-compare.js +267 -0
  159. package/components/image-compare/image-compare.module.css +25 -0
  160. package/components/in-view-title/in-view-title.d.ts +17 -0
  161. package/components/in-view-title/in-view-title.js +70 -0
  162. package/components/in-view-title/in-view-title.module.css +13 -0
  163. package/components/inline-edit/inline-edit.d.ts +27 -0
  164. package/components/inline-edit/inline-edit.js +315 -0
  165. package/components/inline-edit/inline-edit.module.css +73 -0
  166. package/components/input/input.d.ts +7 -0
  167. package/components/input/input.js +60 -0
  168. package/components/input/input.module.css +22 -0
  169. package/components/json-viewer/json-viewer.d.ts +48 -0
  170. package/components/json-viewer/json-viewer.js +463 -0
  171. package/components/json-viewer/json-viewer.module.css +92 -0
  172. package/components/line-chart/line-chart.d.ts +58 -0
  173. package/components/line-chart/line-chart.js +383 -0
  174. package/components/line-chart/line-chart.module.css +60 -0
  175. package/components/mention-input/mention-input.d.ts +72 -0
  176. package/components/mention-input/mention-input.js +417 -0
  177. package/components/mention-input/mention-input.module.css +134 -0
  178. package/components/metric-card/metric-card.d.ts +8 -0
  179. package/components/metric-card/metric-card.js +44 -0
  180. package/components/metric-card/metric-card.module.css +18 -0
  181. package/components/money-input/money-input.d.ts +48 -0
  182. package/components/money-input/money-input.js +503 -0
  183. package/components/money-input/money-input.module.css +77 -0
  184. package/components/morph-select/morph-select.d.ts +50 -0
  185. package/components/morph-select/morph-select.js +395 -0
  186. package/components/morph-select/morph-select.module.css +88 -0
  187. package/components/multi-select/multi-select.d.ts +18 -0
  188. package/components/multi-select/multi-select.js +105 -0
  189. package/components/multi-select/multi-select.module.css +37 -0
  190. package/components/notification-center/notification-center.d.ts +24 -0
  191. package/components/notification-center/notification-center.js +146 -0
  192. package/components/notification-center/notification-center.module.css +79 -0
  193. package/components/number-field/number-field.d.ts +41 -0
  194. package/components/number-field/number-field.js +455 -0
  195. package/components/number-field/number-field.module.css +71 -0
  196. package/components/otp-input/otp-input.d.ts +13 -0
  197. package/components/otp-input/otp-input.js +129 -0
  198. package/components/otp-input/otp-input.module.css +28 -0
  199. package/components/pagination/pagination.d.ts +7 -0
  200. package/components/pagination/pagination.js +60 -0
  201. package/components/pagination/pagination.module.css +16 -0
  202. package/components/password-field/password-field.d.ts +14 -0
  203. package/components/password-field/password-field.js +51 -0
  204. package/components/password-field/password-field.module.css +27 -0
  205. package/components/password-strength/password-strength.d.ts +107 -0
  206. package/components/password-strength/password-strength.js +161 -0
  207. package/components/password-strength/password-strength.module.css +57 -0
  208. package/components/phone-input/phone-input.d.ts +73 -0
  209. package/components/phone-input/phone-input.js +612 -0
  210. package/components/phone-input/phone-input.module.css +98 -0
  211. package/components/popover/popover.d.ts +6 -0
  212. package/components/popover/popover.js +19 -0
  213. package/components/popover/popover.module.css +32 -0
  214. package/components/progress/progress.d.ts +8 -0
  215. package/components/progress/progress.js +42 -0
  216. package/components/progress/progress.module.css +1 -0
  217. package/components/radio-cards/radio-cards.d.ts +35 -0
  218. package/components/radio-cards/radio-cards.js +110 -0
  219. package/components/radio-cards/radio-cards.module.css +84 -0
  220. package/components/radio-group/radio-group.d.ts +13 -0
  221. package/components/radio-group/radio-group.js +55 -0
  222. package/components/radio-group/radio-group.module.css +30 -0
  223. package/components/resizable-panels/resizable-panels.d.ts +37 -0
  224. package/components/resizable-panels/resizable-panels.js +338 -0
  225. package/components/resizable-panels/resizable-panels.module.css +40 -0
  226. package/components/rich-text-editor/rich-text-editor.d.ts +55 -0
  227. package/components/rich-text-editor/rich-text-editor.js +1733 -0
  228. package/components/rich-text-editor/rich-text-editor.module.css +201 -0
  229. package/components/ridgeline/ridgeline.d.ts +37 -0
  230. package/components/ridgeline/ridgeline.js +346 -0
  231. package/components/ridgeline/ridgeline.module.css +58 -0
  232. package/components/scroll-area/scroll-area.d.ts +40 -0
  233. package/components/scroll-area/scroll-area.js +153 -0
  234. package/components/scroll-area/scroll-area.module.css +79 -0
  235. package/components/search-field/search-field.d.ts +7 -0
  236. package/components/search-field/search-field.js +20 -0
  237. package/components/search-field/search-field.module.css +17 -0
  238. package/components/segmented-control/segmented-control.d.ts +16 -0
  239. package/components/segmented-control/segmented-control.js +56 -0
  240. package/components/segmented-control/segmented-control.module.css +15 -0
  241. package/components/select/select.d.ts +15 -0
  242. package/components/select/select.js +36 -0
  243. package/components/select/select.module.css +35 -0
  244. package/components/shortcut-recorder/shortcut-recorder.d.ts +108 -0
  245. package/components/shortcut-recorder/shortcut-recorder.js +380 -0
  246. package/components/shortcut-recorder/shortcut-recorder.module.css +85 -0
  247. package/components/signature-pad/signature-pad.d.ts +72 -0
  248. package/components/signature-pad/signature-pad.js +450 -0
  249. package/components/signature-pad/signature-pad.module.css +84 -0
  250. package/components/skeleton/skeleton.d.ts +13 -0
  251. package/components/skeleton/skeleton.js +53 -0
  252. package/components/skeleton/skeleton.module.css +11 -0
  253. package/components/slider/slider.d.ts +45 -0
  254. package/components/slider/slider.js +347 -0
  255. package/components/slider/slider.module.css +45 -0
  256. package/components/slope-chart/slope-chart.d.ts +38 -0
  257. package/components/slope-chart/slope-chart.js +183 -0
  258. package/components/slope-chart/slope-chart.module.css +51 -0
  259. package/components/slot-text/slot-text.d.ts +26 -0
  260. package/components/slot-text/slot-text.js +119 -0
  261. package/components/slot-text/slot-text.module.css +19 -0
  262. package/components/sortable-data-table/sortable-data-table.d.ts +38 -0
  263. package/components/sortable-data-table/sortable-data-table.js +240 -0
  264. package/components/sortable-data-table/sortable-data-table.module.css +116 -0
  265. package/components/sparkline/sparkline.d.ts +19 -0
  266. package/components/sparkline/sparkline.js +239 -0
  267. package/components/sparkline/sparkline.module.css +26 -0
  268. package/components/split-button/split-button.d.ts +19 -0
  269. package/components/split-button/split-button.js +33 -0
  270. package/components/split-button/split-button.module.css +51 -0
  271. package/components/stepper/stepper.d.ts +36 -0
  272. package/components/stepper/stepper.js +108 -0
  273. package/components/stepper/stepper.module.css +74 -0
  274. package/components/streamgraph/streamgraph.d.ts +51 -0
  275. package/components/streamgraph/streamgraph.js +376 -0
  276. package/components/streamgraph/streamgraph.module.css +55 -0
  277. package/components/swipe-actions/swipe-actions.d.ts +35 -0
  278. package/components/swipe-actions/swipe-actions.js +330 -0
  279. package/components/swipe-actions/swipe-actions.module.css +64 -0
  280. package/components/switch/switch.d.ts +7 -0
  281. package/components/switch/switch.js +45 -0
  282. package/components/switch/switch.module.css +12 -0
  283. package/components/tabs/tabs.d.ts +8 -0
  284. package/components/tabs/tabs.js +138 -0
  285. package/components/tabs/tabs.module.css +27 -0
  286. package/components/tag-input/tag-input.d.ts +15 -0
  287. package/components/tag-input/tag-input.js +150 -0
  288. package/components/tag-input/tag-input.module.css +34 -0
  289. package/components/text-morph/text-morph.d.ts +13 -0
  290. package/components/text-morph/text-morph.js +69 -0
  291. package/components/text-morph/text-morph.module.css +8 -0
  292. package/components/text-reveal/text-reveal.d.ts +14 -0
  293. package/components/text-reveal/text-reveal.js +20 -0
  294. package/components/text-reveal/text-reveal.module.css +13 -0
  295. package/components/text-shimmer/text-shimmer.d.ts +18 -0
  296. package/components/text-shimmer/text-shimmer.js +90 -0
  297. package/components/text-shimmer/text-shimmer.module.css +9 -0
  298. package/components/textarea/textarea.d.ts +7 -0
  299. package/components/textarea/textarea.js +56 -0
  300. package/components/textarea/textarea.module.css +22 -0
  301. package/components/theme-switch/theme-switch.d.ts +11 -0
  302. package/components/theme-switch/theme-switch.js +37 -0
  303. package/components/theme-switch/theme-switch.module.css +31 -0
  304. package/components/time-picker/time-picker.d.ts +13 -0
  305. package/components/time-picker/time-picker.js +95 -0
  306. package/components/time-picker/time-picker.module.css +23 -0
  307. package/components/timeline/timeline.d.ts +47 -0
  308. package/components/timeline/timeline.js +171 -0
  309. package/components/timeline/timeline.module.css +51 -0
  310. package/components/toast/toast.d.ts +8 -0
  311. package/components/toast/toast.js +118 -0
  312. package/components/toast/toast.module.css +100 -0
  313. package/components/toast-stack/toast-stack.d.ts +57 -0
  314. package/components/toast-stack/toast-stack.js +399 -0
  315. package/components/toast-stack/toast-stack.module.css +61 -0
  316. package/components/tooltip/tooltip.d.ts +8 -0
  317. package/components/tooltip/tooltip.js +62 -0
  318. package/components/tooltip/tooltip.module.css +18 -0
  319. package/components/tree-view/tree-view.d.ts +17 -0
  320. package/components/tree-view/tree-view.js +147 -0
  321. package/components/tree-view/tree-view.module.css +22 -0
  322. package/components/treemap/treemap.d.ts +35 -0
  323. package/components/treemap/treemap.js +375 -0
  324. package/components/treemap/treemap.module.css +59 -0
  325. package/components/usage-meter/usage-meter.d.ts +24 -0
  326. package/components/usage-meter/usage-meter.js +296 -0
  327. package/components/usage-meter/usage-meter.module.css +61 -0
  328. package/components/user-menu/user-menu.d.ts +62 -0
  329. package/components/user-menu/user-menu.js +316 -0
  330. package/components/user-menu/user-menu.module.css +97 -0
  331. package/components/waffle-chart/waffle-chart.d.ts +39 -0
  332. package/components/waffle-chart/waffle-chart.js +244 -0
  333. package/components/waffle-chart/waffle-chart.module.css +52 -0
  334. package/components.json +1330 -0
  335. package/eslint.js +53 -0
  336. package/lib/escape.d.ts +19 -0
  337. package/lib/escape.js +50 -0
  338. package/lib/media.d.ts +353 -0
  339. package/lib/media.js +65 -0
  340. package/lib/menu-highlight.d.ts +28 -0
  341. package/lib/menu-highlight.js +43 -0
  342. package/lib/motion-tokens.d.ts +60 -0
  343. package/lib/motion-tokens.js +25 -0
  344. package/lib/return-focus.d.ts +9 -0
  345. package/lib/return-focus.js +53 -0
  346. package/lib/silicon-provider.d.ts +60 -0
  347. package/lib/silicon-provider.js +47 -0
  348. package/lib/use-copy-feedback.d.ts +8 -0
  349. package/lib/use-copy-feedback.js +36 -0
  350. package/lib/use-today.d.ts +2 -0
  351. package/lib/use-today.js +29 -0
  352. package/package.json +76 -4
  353. package/styles.css +218 -0
  354. package/styles.css.d.ts +1 -0
@@ -0,0 +1,455 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AnimatePresence, animate, motion, useMotionValue, useReducedMotion, useTransform } from "motion/react";
4
+ import { Minus, Plus } from "lucide-react";
5
+ import { Fragment, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
6
+ import { motionTokens } from "../../lib/motion-tokens.js";
7
+ import styles from "./number-field.module.css";
8
+ const { spring, duration, blur } = motionTokens;
9
+ const enterEase = [...motionTokens.ease.enter];
10
+ const exitEase = [...motionTokens.ease.standard];
11
+ const DIGITS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
12
+ /** Pixels of label drag per step, the pause before a held control repeats, and the fastest repeat. */
13
+ const SCRUB_PX = 6, HOLD_DELAY = 400, HOLD_FASTEST = 40;
14
+ /** A held press keeps leaning on a limit at this cadence, how long the limit note stays, and how long an under-minimum draft waits
15
+ * before it warns (a short draft is often on its way to a larger number). */
16
+ const LIMIT_PUSH = 240, LIMIT_HINT_MS = 1500, UNDER_WARN_MS = 700;
17
+ /** Pushes that arrive within this window count as one continued effort, so each strains a little further, up to the cap. */
18
+ const PUSH_WINDOW = 700, PUSH_GAIN = .22, PUSH_CAP = 4;
19
+ /** A velocity kick on the value at a limit: it strains a few pixels toward the press and springs home. Motion drops velocity on
20
+ * time-defined springs, so the kick runs the morph spring expressed as stiffness and damping. */
21
+ const BUMP_VELOCITY = 130;
22
+ const kick = (() => { const root = (2 * Math.PI) / (spring.morph.visualDuration * 1.2); return { type: "spring", stiffness: root * root, damping: 2 * (1 - spring.morph.bounce) * root }; })();
23
+ const decimalsOf = (value) => (String(value).split(".")[1] ?? "").length;
24
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
25
+ const affixText = (affix, value) => typeof affix === "function" ? affix(value) : affix ?? "";
26
+ /** iOS style resistance: travel past a limit gives less and less, and never more than `limit` pixels. */
27
+ const rubber = (distance, limit = 10) => Math.sign(distance) * (1 - 1 / (Math.abs(distance) * .55 / limit + 1)) * limit;
28
+ /** Split a formatted number into columns keyed by place value, so 9 → 10 keeps the ones column the ones column. */
29
+ function partsOf(value, format) {
30
+ const parts = format.formatToParts(value);
31
+ let place = parts.reduce((count, part) => count + (part.type === "integer" ? part.value.length : 0), 0);
32
+ let fraction = 0;
33
+ return parts.flatMap((part, index) => {
34
+ if (part.type === "integer")
35
+ return [...part.value].map(char => ({ key: `i${--place}`, digit: Number(char) }));
36
+ if (part.type === "fraction")
37
+ return [...part.value].map(char => ({ key: `f${fraction++}`, digit: Number(char) }));
38
+ return [{ key: part.type === "group" ? `g${place}` : part.type === "decimal" ? "d" : part.type === "minusSign" ? "m" : `${part.type}${index}`, text: part.value }];
39
+ });
40
+ }
41
+ /* A column or character opens its width while it rises in the direction of change, and closes on a critically damped spring so it never passes zero.
42
+ Scale and opacity ride the same spring as width, so a digit grows with its slot instead of landing on its neighbours. */
43
+ const slot = {
44
+ enter: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * .3}em`, filter: `blur(${blur.soft}px)` }),
45
+ center: { width: "auto", scale: 1, opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { width: spring.morph, scale: spring.morph, opacity: spring.morph, y: spring.snappy, filter: { duration: .3, ease: exitEase } } },
46
+ exit: (direction) => ({ width: 0, scale: .6, opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { width: spring.smooth, scale: spring.smooth, y: { duration: duration.fast, ease: exitEase }, opacity: { duration: duration.instant }, filter: { duration: duration.instant } } }),
47
+ };
48
+ /* Reduced motion keeps a short fade and no travel. While typing, columns follow the draft at once so they never lag the caret.
49
+ Every resting state matches `slot`, so a page hydrated with reduced motion renders the same styles as the server. */
50
+ const rest = (opacity) => ({ width: "auto", scale: 1, opacity, y: 0, filter: "none" });
51
+ const still = { enter: rest(0), center: { ...rest(1), transition: { duration: duration.instant } }, exit: { width: 0, opacity: 0, transition: { duration: 0 } } };
52
+ const cut = { enter: rest(1), center: { ...rest(1), transition: { duration: 0 } }, exit: { width: 0, opacity: 0, transition: { duration: 0 } } };
53
+ /** One digit on the wheel. Its distance from the wheel position sets where it sits, how clear it is, and whether it shows. */
54
+ function Glyph({ position, digit }) {
55
+ // Every style reads the wheel directly: a chained transform can update a frame late and leave the wheel blank on a jump.
56
+ const offset = (current) => ((((digit - current) % 10) + 15) % 10) - 5;
57
+ const y = useTransform(position, current => `${offset(current) * 1.05}em`);
58
+ // An eased falloff keeps a turning digit legible through the middle of the roll instead of washing out.
59
+ const opacity = useTransform(position, current => Math.max(0, 1 - Math.abs(offset(current)) ** 1.5 * 1.1));
60
+ const visibility = useTransform(position, current => Math.abs(offset(current)) >= 1 ? "hidden" : "visible");
61
+ const filter = useTransform(position, current => { const distance = Math.abs(offset(current)); return distance < .02 || distance >= 1 ? "none" : `blur(${(distance * blur.soft * .75).toFixed(2)}px)`; });
62
+ return _jsx(motion.span, { className: styles.glyph, style: { y, opacity, filter, visibility }, children: digit });
63
+ }
64
+ /** An odometer wheel. It turns the way the whole number moved, wraps 9 → 0, and retargets mid spin when steps arrive quickly. */
65
+ function Wheel({ digit, direction, instant }) {
66
+ const reduced = useReducedMotion();
67
+ const position = useMotionValue(digit);
68
+ const wheel = useRef({ digit, target: digit });
69
+ useLayoutEffect(() => {
70
+ const state = wheel.current;
71
+ if (state.digit === digit)
72
+ return;
73
+ // Turn the way the number moved, unless that is more than half a turn; then take the short way, so a big clamp never spins.
74
+ let delta = direction > 0 ? (digit - state.digit + 10) % 10 : -((state.digit - digit + 10) % 10);
75
+ if (Math.abs(delta) > 5)
76
+ delta -= Math.sign(delta) * 10;
77
+ state.target += delta;
78
+ state.digit = digit;
79
+ if (instant || reduced)
80
+ position.jump(state.target);
81
+ else
82
+ animate(position, state.target, spring.snappy);
83
+ }, [digit, direction, instant, position, reduced]);
84
+ return _jsxs(_Fragment, { children: [_jsx("span", { className: styles.sizer, children: "0" }), DIGITS.map(item => _jsx(Glyph, { position: position, digit: item }, item))] });
85
+ }
86
+ function Digits({ value, format, direction, instant }) {
87
+ const reduced = useReducedMotion();
88
+ const variants = instant ? cut : reduced ? still : slot;
89
+ return _jsx(AnimatePresence, { initial: false, custom: direction, children: partsOf(value, format).map(part => _jsx(motion.span, { className: "digit" in part ? styles.column : styles.symbol, custom: direction, variants: variants, initial: "enter", animate: "center", exit: "exit", children: "digit" in part ? _jsx(Wheel, { digit: part.digit, direction: direction, instant: instant }) : part.text }, part.key)) });
90
+ }
91
+ /** Prefix and suffix characters are keyed by position, so "seat" → "seats" only opens the new "s" and the rest holds still. */
92
+ function AffixText({ text, direction, className }) {
93
+ const reduced = useReducedMotion();
94
+ return _jsx("span", { className: className, children: _jsx(AnimatePresence, { initial: false, custom: direction, children: [...text].map((char, index) => _jsx(motion.span, { className: styles.char, custom: direction, variants: reduced ? still : slot, initial: "enter", animate: "center", exit: "exit", children: char }, `${index}:${char}`)) }) });
95
+ }
96
+ const numberIn = (word) => { const digits = word?.replace(/[^\d.-]/g, ""); return digits && /\d/.test(digits) && Number.isFinite(Number(digits)) ? Number(digits) : null; };
97
+ /** A number word rolls down when it shrank and up when it grew; any other changed word rises. */
98
+ const directionsBetween = (from, to) => { const before = from.split(" "); return to.split(" ").map((word, index) => { const a = numberIn(before[index]), b = numberIn(word); return a !== null && b !== null && b < a ? -1 : 1; }); };
99
+ const wordRise = {
100
+ enter: (direction) => ({ opacity: 0, y: `${direction * .3}em`, filter: `blur(${blur.soft}px)` }),
101
+ // Opacity follows the slot's width spring, so a longer word never shows before its room has opened.
102
+ center: { opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" }, transition: { duration: .22, ease: enterEase, opacity: spring.morph } },
103
+ exit: (direction) => ({ opacity: 0, y: `${direction * -.3}em`, filter: `blur(${blur.subtle}px)`, transition: { duration: .14, ease: exitEase } }),
104
+ };
105
+ const wordFade = { enter: { opacity: 0, y: 0, filter: "none" }, center: { opacity: 1, y: 0, filter: "none", transition: { duration: duration.instant } }, exit: { opacity: 0, transition: { duration: 0 } } };
106
+ /** A word's width follows its new copy on a spring, so a price gaining a digit eases the rest of the line along instead of shoving it. */
107
+ function WordSlot({ word, direction }) {
108
+ const reduced = useReducedMotion();
109
+ const sizerRef = useRef(null);
110
+ const width = useMotionValue("auto");
111
+ const measured = useRef(false);
112
+ useLayoutEffect(() => {
113
+ const node = sizerRef.current;
114
+ if (!node || typeof ResizeObserver === "undefined")
115
+ return;
116
+ const observer = new ResizeObserver(() => {
117
+ if (!measured.current || reduced)
118
+ width.jump(node.offsetWidth);
119
+ else
120
+ animate(width, node.offsetWidth, spring.morph);
121
+ measured.current = true;
122
+ });
123
+ observer.observe(node);
124
+ return () => observer.disconnect();
125
+ }, [reduced, width]);
126
+ return _jsxs(motion.span, { className: styles.wordSlot, style: { width }, children: [_jsx("span", { ref: sizerRef, className: styles.wordSizer, children: word }), _jsx(AnimatePresence, { initial: false, mode: "popLayout", custom: direction, children: _jsx(motion.span, { className: styles.word, custom: direction, variants: reduced ? wordFade : wordRise, initial: "enter", animate: "center", exit: "exit", children: word }, word) })] });
127
+ }
128
+ /** Changed words rise in and unblur while unchanged words hold still. Assistive tech reads the plain copy. */
129
+ function MotionText({ text }) {
130
+ const [trail, setTrail] = useState({ text, directions: [] });
131
+ if (trail.text !== text)
132
+ setTrail({ text, directions: directionsBetween(trail.text, text) });
133
+ const directions = trail.text === text ? trail.directions : directionsBetween(trail.text, text);
134
+ const words = text.split(" ");
135
+ // Spaces live between the slots, so a word that is still opening its width never runs into the next one.
136
+ return _jsxs(_Fragment, { children: [_jsx("span", { className: styles.srOnly, children: text }), _jsx("span", { className: styles.words, "aria-hidden": "true", children: words.map((word, index) => _jsxs(Fragment, { children: [index > 0 && " ", _jsx(WordSlot, { word: word, direction: directions[index] ?? 1 })] }, index)) })] });
137
+ }
138
+ /** Helper copy: the row opens its height on a spring, then the words settle in. */
139
+ function FieldMessage({ id, text, className }) {
140
+ return _jsx(AnimatePresence, { initial: false, children: text ? _jsx(MessageRow, { id: id, text: text, className: className }, "message") : null });
141
+ }
142
+ /** The row tracks the measured copy, so a longer message that wraps opens its next line instead of snapping. */
143
+ function MessageRow({ id, text, className }) {
144
+ const reduced = useReducedMotion();
145
+ const copyRef = useRef(null);
146
+ const [height, setHeight] = useState("auto");
147
+ useLayoutEffect(() => {
148
+ const node = copyRef.current;
149
+ if (!node || typeof ResizeObserver === "undefined")
150
+ return;
151
+ const observer = new ResizeObserver(([entry]) => setHeight(entry?.borderBoxSize?.[0]?.blockSize ?? node.offsetHeight));
152
+ observer.observe(node);
153
+ return () => observer.disconnect();
154
+ }, []);
155
+ return _jsx(motion.span, { className: styles.messageSlot, initial: { height: 0, opacity: 0 }, animate: { height, opacity: 1 }, exit: { height: 0, opacity: 0, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.instant } } }, transition: reduced ? { duration: 0 } : { height: spring.smooth, opacity: { duration: duration.fast } }, children: _jsx(motion.span, { ref: copyRef, id: id, className: className, initial: reduced ? false : { y: "0.35em", filter: `blur(${blur.soft}px)` }, animate: { y: 0, filter: "blur(0px)" }, transition: { duration: reduced ? 0 : duration.standard, ease: enterEase }, children: _jsx(MotionText, { text: text }) }) });
156
+ }
157
+ /** The limit note rises in from the side it guards (up for a maximum, down for a minimum) and fades out in place. */
158
+ function LimitNote({ id, edge, text }) {
159
+ const reduced = useReducedMotion();
160
+ return _jsx(AnimatePresence, { initial: false, children: edge !== 0 && _jsx(motion.span, { id: id, className: styles.limit, initial: reduced ? { opacity: 0 } : { opacity: 0, y: `${edge * .45}em`, filter: `blur(${blur.subtle}px)` }, animate: { opacity: 1, y: 0, filter: "blur(0px)", transitionEnd: { filter: "none" } }, exit: { opacity: 0, transition: { duration: reduced ? duration.instant : duration.standard, ease: exitEase } }, transition: reduced ? { duration: duration.instant } : { y: spring.snappy, opacity: { duration: duration.fast }, filter: { duration: duration.fast } }, children: text }, "limit") });
161
+ }
162
+ /** Pointer presses hold to repeat; keyboard and assistive clicks (detail 0) take one step. */
163
+ function StepButton({ toward, label, disabled, controls, limit, pressed, iconRef, onPress, onRelease, onActivate }) {
164
+ const Icon = toward > 0 ? Plus : Minus;
165
+ // A limit dims the button but keeps it focusable, so a press there strains the value instead of dropping focus.
166
+ // Mouse presses keep focus where it was: an open draft commits with the step, and the stepper never steals the ring.
167
+ return _jsx("button", { type: "button", className: styles.step, disabled: disabled, "aria-controls": controls, "aria-label": `${toward > 0 ? "Increase" : "Decrease"} ${label}`, "aria-disabled": limit || undefined, "data-pressed": pressed || undefined, onPointerDown: event => { if (event.button === 0)
168
+ onPress(); }, onPointerUp: onRelease, onPointerLeave: onRelease, onPointerCancel: onRelease, onMouseDown: event => event.preventDefault(), onClick: event => { if (event.detail === 0)
169
+ onActivate(); }, onContextMenu: event => event.preventDefault(), children: _jsx("span", { ref: iconRef, className: styles.icon, children: _jsx(Icon, { size: 16, strokeWidth: 1.75, "aria-hidden": "true" }) }) });
170
+ }
171
+ /**
172
+ * A bounded number with odometer digits. Buttons and arrow keys repeat and speed up while held, PageUp and PageDown take
173
+ * large steps, Home and End jump to the limits, and `scrub` lets the label be dragged. Typing edits a plain draft that
174
+ * applies live while valid; the rolling digits return once it commits on Enter, blur, or the next step.
175
+ */
176
+ export function NumberField({ label, value, defaultValue = 0, onValueChange, min = 0, max = Number.MAX_SAFE_INTEGER, step: stepProp = 1, largeStep, description, disabled, id, prefix, suffix, scrub = false, locale = "en-US", formatOptions, size = "md", limitHint = true }) {
177
+ const reduced = useReducedMotion();
178
+ const generatedId = useId();
179
+ const inputId = id ?? generatedId;
180
+ const hintId = description ? `${inputId}-description` : undefined;
181
+ const limitId = `${inputId}-limit`;
182
+ const step = stepProp > 0 ? stepProp : 1;
183
+ const [internal, setInternal] = useState(defaultValue);
184
+ const current = value ?? internal;
185
+ const [draft, setDraft] = useState("");
186
+ const [editing, setEditing] = useState(false);
187
+ const [pressed, setPressed] = useState(0);
188
+ const [scrubbing, setScrubbing] = useState(false);
189
+ const [announcement, setAnnouncement] = useState("");
190
+ /** The limit a press just met, shown for a moment; and whether a draft under the minimum has sat long enough to warn. */
191
+ const [pushed, setPushed] = useState(0);
192
+ const [underWarn, setUnderWarn] = useState(false);
193
+ const controlRef = useRef(null);
194
+ const minusIcon = useRef(null);
195
+ const plusIcon = useRef(null);
196
+ const pushedTimer = useRef(undefined);
197
+ const underTimer = useRef(undefined);
198
+ const strainTimer = useRef(undefined);
199
+ const effort = useRef({ edge: 0, count: 0, at: 0 });
200
+ const inputRef = useRef(null);
201
+ const groupRef = useRef(null);
202
+ const latest = useRef(current);
203
+ const editStart = useRef(current);
204
+ const hold = useRef(undefined);
205
+ const drag = useRef(null);
206
+ const suppressClick = useRef(false);
207
+ const shiftFrom = useRef(null);
208
+ const selectNext = useRef(false);
209
+ const live = useRef(() => false);
210
+ const bumpY = useMotionValue(0);
211
+ const scrubX = useMotionValue(0);
212
+ const shiftX = useMotionValue(0);
213
+ const x = useTransform(() => scrubX.get() + shiftX.get());
214
+ const base = Number.isFinite(min) ? min : 0;
215
+ const decimals = Math.max(decimalsOf(step), decimalsOf(base));
216
+ const minFraction = formatOptions?.minimumFractionDigits ?? decimals;
217
+ const maxFraction = Math.max(minFraction, formatOptions?.maximumFractionDigits ?? decimals);
218
+ const grouping = formatOptions?.useGrouping ?? true;
219
+ const format = useMemo(() => new Intl.NumberFormat(locale, { minimumFractionDigits: minFraction, maximumFractionDigits: maxFraction, useGrouping: grouping, numberingSystem: "latn" }), [locale, minFraction, maxFraction, grouping]);
220
+ const symbols = useMemo(() => { const parts = new Intl.NumberFormat(locale).formatToParts(-1234.5); return { group: parts.find(part => part.type === "group")?.value ?? ",", decimal: parts.find(part => part.type === "decimal")?.value ?? "." }; }, [locale]);
221
+ const round = (next) => Number(next.toFixed(decimals)) || 0;
222
+ const clamp = (next) => Math.min(max, Math.max(min, next));
223
+ const snap = (next) => round(base + Math.round((next - base) / step) * step);
224
+ /** Off-grid values move to the next grid line in the direction of travel, then whole steps from there. */
225
+ const stepFrom = (from, steps) => { const index = (from - base) / step; return round(base + ((steps > 0 ? Math.floor(index + 1e-7) : Math.ceil(index - 1e-7)) + steps) * step); };
226
+ const spoken = (next) => `${affixText(prefix, next)}${format.format(next)}${affixText(suffix, next)}`.trim();
227
+ function parse(text) {
228
+ const normalized = text.split(symbols.group).join("").replace(symbols.decimal, ".").replace(/[^\d.-]/g, "");
229
+ if (!/\d/.test(normalized))
230
+ return null;
231
+ const parsed = Number(normalized);
232
+ return Number.isFinite(parsed) ? parsed : null;
233
+ }
234
+ const typed = editing ? parse(draft) : null;
235
+ const shown = typed ?? current;
236
+ const [trail, setTrail] = useState({ value: shown, direction: 1 });
237
+ if (trail.value !== shown)
238
+ setTrail({ value: shown, direction: shown > trail.value ? 1 : -1 });
239
+ const direction = trail.value === shown ? trail.direction : shown > trail.value ? 1 : -1;
240
+ useEffect(() => () => { window.clearTimeout(hold.current); window.clearTimeout(pushedTimer.current); window.clearTimeout(underTimer.current); window.clearTimeout(strainTimer.current); }, []);
241
+ /** A press past a limit. The value strains toward it and springs home, a little further with each push in a row (capped, like
242
+ * overscroll); the refused button shakes once; the note names the limit. Reduced motion keeps only a brief color change. */
243
+ function strain(edge, source) {
244
+ const now = performance.now(), push = effort.current;
245
+ push.count = push.edge === edge && now - push.at < PUSH_WINDOW ? push.count + 1 : 1;
246
+ push.edge = edge;
247
+ push.at = now;
248
+ const control = controlRef.current;
249
+ if (control) {
250
+ control.dataset.strain = edge > 0 ? "max" : "min";
251
+ window.clearTimeout(strainTimer.current);
252
+ strainTimer.current = window.setTimeout(() => { delete control.dataset.strain; }, 420);
253
+ }
254
+ if (!reduced) {
255
+ animate(bumpY, 0, { ...kick, velocity: -edge * BUMP_VELOCITY * (1 + Math.min(push.count - 1, PUSH_CAP) * PUSH_GAIN) });
256
+ const icon = (edge > 0 ? plusIcon : minusIcon).current;
257
+ if (icon && (source === "button" || source === "key"))
258
+ animate(icon, { x: [0, -2.5, 2.5, -1.5, 1, 0] }, { duration: .32, ease: "easeOut" });
259
+ }
260
+ if (limitHint) {
261
+ setPushed(edge);
262
+ window.clearTimeout(pushedTimer.current);
263
+ pushedTimer.current = window.setTimeout(() => setPushed(0), LIMIT_HINT_MS);
264
+ }
265
+ }
266
+ /** Every change lands here. A value past a limit clamps, strains toward it, and says which limit it met. */
267
+ function commitValue(next, source) {
268
+ if (!Number.isFinite(next))
269
+ return false;
270
+ const clamped = clamp(next);
271
+ const limit = Math.sign(next - clamped);
272
+ if (limit && source !== "scrub") {
273
+ strain(limit > 0 ? 1 : -1, source);
274
+ setAnnouncement(`${spoken(clamped)}, ${limit > 0 ? "maximum" : "minimum"}`);
275
+ }
276
+ else if (source === "button" && clamped !== latest.current)
277
+ setAnnouncement(spoken(clamped));
278
+ if (clamped === latest.current)
279
+ return false;
280
+ latest.current = clamped;
281
+ if (value === undefined)
282
+ setInternal(clamped);
283
+ onValueChange?.(clamped);
284
+ return true;
285
+ }
286
+ const nudge = (toward, steps, source) => commitValue(stepFrom(latest.current, toward * steps), source);
287
+ useLayoutEffect(() => { latest.current = current; live.current = nudge; });
288
+ /** The group glides back to center when typing changes its width, instead of hopping half a character per key. */
289
+ function captureShift() {
290
+ const group = groupRef.current;
291
+ if (group && shiftFrom.current === null)
292
+ shiftFrom.current = group.getBoundingClientRect().left - scrubX.get() - shiftX.get();
293
+ }
294
+ useLayoutEffect(() => {
295
+ const from = shiftFrom.current, group = groupRef.current;
296
+ shiftFrom.current = null;
297
+ // After a commit the caret collapses to the end, so no selection box sits over the rolling digits.
298
+ if (selectNext.current) {
299
+ selectNext.current = false;
300
+ const input = inputRef.current;
301
+ if (input && document.activeElement === input) {
302
+ const end = input.value.length;
303
+ input.setSelectionRange(end, end);
304
+ }
305
+ }
306
+ if (from === null || !group)
307
+ return;
308
+ const delta = from - (group.getBoundingClientRect().left - scrubX.get() - shiftX.get());
309
+ if (Math.abs(delta) < .5)
310
+ return;
311
+ shiftX.set(shiftX.get() + delta);
312
+ if (reduced)
313
+ shiftX.jump(0);
314
+ else
315
+ animate(shiftX, 0, spring.morph);
316
+ });
317
+ function clearUnder() {
318
+ window.clearTimeout(underTimer.current);
319
+ setUnderWarn(false);
320
+ }
321
+ function commitDraft() {
322
+ if (!editing)
323
+ return;
324
+ captureShift();
325
+ setEditing(false);
326
+ clearUnder();
327
+ const parsed = parse(draft);
328
+ if (parsed !== null)
329
+ commitValue(snap(parsed), "type");
330
+ }
331
+ function stopHold() {
332
+ window.clearTimeout(hold.current);
333
+ hold.current = undefined;
334
+ setPressed(0);
335
+ }
336
+ /** One step now; after a pause, repeats that speed up gently until release. Held into a limit, it keeps leaning on it at a
337
+ * steady cadence, each push straining a little further, until the press lets go. */
338
+ function startHold(toward, amount, source) {
339
+ stopHold();
340
+ commitDraft();
341
+ const steps = Math.max(1, Math.round(amount / step));
342
+ if (source === "button")
343
+ setPressed(toward);
344
+ let count = 0;
345
+ const tick = () => {
346
+ const moved = live.current(toward, steps, source);
347
+ hold.current = window.setTimeout(tick, moved ? Math.max(HOLD_FASTEST, 150 * .86 ** ++count) : LIMIT_PUSH);
348
+ };
349
+ hold.current = window.setTimeout(tick, nudge(toward, steps, source) ? HOLD_DELAY : LIMIT_PUSH + 120);
350
+ }
351
+ function onKeyDown(event) {
352
+ const large = largeStep ?? step * 10;
353
+ const move = { ArrowUp: [1, event.shiftKey ? large : step], ArrowDown: [-1, event.shiftKey ? large : step], PageUp: [1, large], PageDown: [-1, large] }[event.key];
354
+ if (move) {
355
+ event.preventDefault();
356
+ if (!event.repeat)
357
+ startHold(move[0], move[1], "key");
358
+ return;
359
+ }
360
+ // Home and End reach the limits; while a draft is open they move the caret as in any text field.
361
+ if (!editing && ((event.key === "Home" && Number.isFinite(min)) || (event.key === "End" && max < Number.MAX_SAFE_INTEGER))) {
362
+ event.preventDefault();
363
+ commitValue(event.key === "Home" ? min : max, "key");
364
+ return;
365
+ }
366
+ if (event.key === "Enter") {
367
+ event.preventDefault();
368
+ if (editing) {
369
+ selectNext.current = true;
370
+ commitDraft();
371
+ }
372
+ else
373
+ event.currentTarget.select();
374
+ }
375
+ if (event.key === "Escape" && editing) {
376
+ event.preventDefault();
377
+ captureShift();
378
+ setEditing(false);
379
+ clearUnder();
380
+ selectNext.current = true;
381
+ commitValue(editStart.current, "type");
382
+ }
383
+ }
384
+ function onChange(text) {
385
+ const allowed = new RegExp(`[^0-9${escape(symbols.group)}${decimals || maxFraction ? escape(symbols.decimal) : ""}${min < 0 ? "\\-" : ""}]`, "g");
386
+ const next = text.replace(allowed, "");
387
+ if (!editing)
388
+ editStart.current = current;
389
+ captureShift();
390
+ setDraft(next);
391
+ setEditing(true);
392
+ // Valid drafts apply as they are typed, so anything that depends on the value follows along.
393
+ const parsed = parse(next);
394
+ if (parsed !== null && parsed >= min && parsed <= max && snap(parsed) === parsed)
395
+ commitValue(parsed, "type");
396
+ // Past the maximum warns at once; under the minimum waits, since "1" is often the start of "12".
397
+ clearUnder();
398
+ if (parsed !== null && parsed < min)
399
+ underTimer.current = window.setTimeout(() => setUnderWarn(true), UNDER_WARN_MS);
400
+ }
401
+ function onScrubStart(event) {
402
+ suppressClick.current = false;
403
+ if (!scrub || disabled || event.button !== 0)
404
+ return;
405
+ commitDraft();
406
+ drag.current = { pointer: event.pointerId, x: event.clientX, from: latest.current, active: false };
407
+ event.currentTarget.setPointerCapture(event.pointerId);
408
+ }
409
+ function onScrubMove(event) {
410
+ const state = drag.current;
411
+ if (!state || state.pointer !== event.pointerId)
412
+ return;
413
+ const dx = event.clientX - state.x;
414
+ if (!state.active) {
415
+ if (Math.abs(dx) < 3)
416
+ return;
417
+ state.active = true;
418
+ setScrubbing(true);
419
+ }
420
+ const travel = dx / SCRUB_PX, steps = Math.trunc(travel);
421
+ commitValue(steps ? stepFrom(state.from, steps) : state.from, "scrub");
422
+ // Past a limit the value follows the pointer with resistance, then springs back on release.
423
+ const raw = state.from + travel * step;
424
+ const over = raw > max ? (raw - max) / step * SCRUB_PX : raw < min ? (raw - min) / step * SCRUB_PX : 0;
425
+ scrubX.set(reduced ? 0 : rubber(over));
426
+ }
427
+ function onScrubEnd(event) {
428
+ const state = drag.current;
429
+ if (!state || state.pointer !== event.pointerId)
430
+ return;
431
+ drag.current = null;
432
+ if (!state.active)
433
+ return;
434
+ suppressClick.current = true;
435
+ setScrubbing(false);
436
+ animate(scrubX, 0, reduced ? { duration: 0 } : spring.snappy);
437
+ setAnnouncement(spoken(latest.current));
438
+ }
439
+ const stepper = (toward) => ({ toward, label, disabled, controls: inputId, limit: toward > 0 ? shown >= max : shown <= min, pressed: pressed === toward });
440
+ /** A typed value past a limit holds a calm warning until it commits and springs back. */
441
+ const outside = typed === null ? 0 : typed > max ? 1 : typed < min && underWarn ? -1 : 0;
442
+ const noteEdge = limitHint ? outside || pushed : 0;
443
+ const noteLimit = noteEdge > 0 ? max : min;
444
+ const noteText = !noteEdge ? "" : typeof limitHint === "function" ? limitHint(noteEdge > 0 ? "max" : "min", noteLimit) : `${noteEdge > 0 ? "Max" : "Min"} ${spoken(noteLimit)}`;
445
+ const describedBy = [hintId, outside && limitHint ? limitId : undefined].filter(Boolean).join(" ") || undefined;
446
+ return _jsxs("div", { className: styles.field, "data-size": size, children: [_jsxs("div", { className: styles.head, children: [_jsx("label", { htmlFor: inputId, className: styles.label, "data-scrub": (scrub && !disabled) || undefined, onPointerDown: onScrubStart, onPointerMove: onScrubMove, onPointerUp: onScrubEnd, onPointerCancel: onScrubEnd, onClick: event => { if (suppressClick.current) {
447
+ event.preventDefault();
448
+ suppressClick.current = false;
449
+ } }, children: label }), _jsx(LimitNote, { id: limitId, edge: noteEdge, text: noteText })] }), _jsxs("div", { ref: controlRef, className: styles.control, "data-scrubbing": scrubbing || undefined, "data-disabled": disabled || undefined, "data-warn": outside ? (outside > 0 ? "max" : "min") : undefined, children: [_jsx(StepButton, { ...stepper(-1), iconRef: minusIcon, onPress: () => startHold(-1, step, "button"), onRelease: stopHold, onActivate: () => { commitDraft(); nudge(-1, 1, "button"); } }), _jsx("div", { className: styles.valueWrap, onMouseDown: event => { if (event.target !== inputRef.current)
450
+ event.preventDefault(); }, onClick: event => { if (!disabled && event.target !== inputRef.current) {
451
+ inputRef.current?.focus();
452
+ inputRef.current?.select();
453
+ } }, children: _jsxs(motion.span, { ref: groupRef, className: styles.value, style: { x, y: bumpY }, children: [_jsx(AffixText, { text: affixText(prefix, shown), direction: direction, className: styles.affix }), _jsxs("span", { className: styles.number, children: [_jsx("span", { className: styles.display, "data-hidden": editing || undefined, "aria-hidden": "true", children: _jsx(Digits, { value: shown, format: format, direction: direction, instant: editing }) }), editing && _jsx("span", { className: styles.mirror, "aria-hidden": "true", children: draft }), _jsx("input", { ref: inputRef, id: inputId, className: styles.input, type: "text", role: "spinbutton", inputMode: min < 0 ? "text" : decimals || maxFraction ? "decimal" : "numeric", autoComplete: "off", spellCheck: false, value: editing ? draft : format.format(current), disabled: disabled, "aria-describedby": describedBy, "aria-invalid": outside ? true : undefined, "aria-valuenow": current, "aria-valuetext": spoken(current), "aria-valuemin": Number.isFinite(min) ? min : undefined, "aria-valuemax": max < Number.MAX_SAFE_INTEGER ? max : undefined, onChange: event => onChange(event.currentTarget.value), onKeyDown: onKeyDown, onKeyUp: event => { if (/^(Arrow(Up|Down)|Page(Up|Down))$/.test(event.key))
454
+ stopHold(); }, onBlur: () => { stopHold(); commitDraft(); } })] }), _jsx(AffixText, { text: affixText(suffix, shown), direction: direction, className: styles.affix })] }) }), _jsx(StepButton, { ...stepper(1), iconRef: plusIcon, onPress: () => startHold(1, step, "button"), onRelease: stopHold, onActivate: () => { commitDraft(); nudge(1, 1, "button"); } })] }), _jsx(FieldMessage, { id: hintId, text: description, className: styles.hint }), _jsx("span", { className: styles.srOnly, "aria-live": "polite", "aria-atomic": "true", children: announcement })] });
455
+ }
@@ -0,0 +1,71 @@
1
+ /* No row gap: grid tracks clamp a negative margin at zero, so a closed message row would still pay the gap. Each row carries its own space instead. */
2
+ .field { --nf-height: var(--control-height-md); --nf-step: 34px; --nf-inset: 3px; --nf-text: var(--text-base); --nf-width: var(--number-field-width, 196px); --nf-limit: color-mix(in oklab, var(--warning) 72%, var(--foreground)); display: grid; min-width: 0; }
3
+ .field[data-size="sm"] { --nf-height: var(--control-height-sm); --nf-step: 28px; --nf-inset: 3px; --nf-text: var(--text-sm); --nf-width: var(--number-field-width, 164px); }
4
+ .field[data-size="lg"] { --nf-height: var(--control-height-lg); --nf-step: 40px; --nf-inset: 4px; --nf-text: var(--text-lg); --nf-width: var(--number-field-width, 228px); }
5
+ :root[data-theme="dark"] .field { --nf-limit: color-mix(in oklab, var(--warning) 88%, var(--foreground)); }
6
+ /* The label row matches the control's width, so the limit note sits over the stepper it answers. */
7
+ .head { display: flex; width: min(100%, var(--nf-width)); align-items: baseline; justify-content: space-between; gap: var(--space-group); margin-bottom: var(--space-related); }
8
+ .label { min-width: 0; color: var(--foreground); font-size: var(--text-sm); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); }
9
+ .field[data-size="sm"] .label { font-size: var(--text-xs); }
10
+ .limit { flex: none; color: var(--nf-limit); font-size: var(--text-xs); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); font-variant-numeric: tabular-nums; white-space: nowrap; }
11
+ /* A scrubbable label says so with its cursor. Vertical pans still scroll the page on touch. */
12
+ .label[data-scrub] { cursor: ew-resize; touch-action: pan-y; user-select: none; -webkit-user-select: none; }
13
+ /* A hairline shell. Focus darkens the border only (the library draws no rings); a typed value past a limit tints it with the warning
14
+ role instead, calm rather than alarming. Set --number-field-width to resize the shell. */
15
+ .control { display: flex; align-items: center; width: min(100%, var(--nf-width)); min-height: var(--nf-height); gap: 2px; border: 1px solid var(--border); border-radius: var(--radius-control); padding: var(--nf-inset); background: var(--surface); transition: border-color var(--duration-fast) var(--ease-standard), background-color var(--duration-standard) var(--ease-standard), opacity var(--duration-fast) var(--ease-standard); }
16
+ @media (hover: hover) and (pointer: fine) { .control:hover, .field:has(.label[data-scrub]:hover) .control { border-color: var(--border-strong); } }
17
+ .control[data-scrubbing] { border-color: var(--foreground); }
18
+ .control:has(input:focus) { border-color: var(--foreground); }
19
+ .control[data-warn], .control[data-warn]:hover { border-color: color-mix(in oklab, var(--warning) 55%, var(--border)); background: color-mix(in oklab, var(--warning) 5%, var(--surface)); }
20
+ .control[data-warn]:has(input:focus) { border-color: color-mix(in oklab, var(--warning) 80%, var(--border)); }
21
+ .control[data-disabled] { opacity: .5; background: var(--surface-muted); }
22
+ /* Radius steps in by the shell's inset, so the pressed fill sits concentric with the shell. */
23
+ .step { display: grid; width: var(--nf-step); height: var(--nf-step); flex: 0 0 var(--nf-step); place-items: center; border: 0; border-radius: calc(var(--radius-control) - var(--nf-inset) - 1px); padding: 0; background: transparent; color: var(--text-secondary); cursor: pointer; touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; transition: background-color var(--duration-fast) var(--ease-standard), color var(--duration-fast) var(--ease-standard), opacity var(--duration-standard) var(--ease-standard); }
24
+ .icon { display: grid; place-items: center; }
25
+ .icon svg { transition: transform var(--duration-spring) var(--ease-spring); }
26
+ @media (hover: hover) and (pointer: fine) { .step:hover:not(:disabled, [aria-disabled="true"]) { background: var(--surface-muted); color: var(--foreground); } }
27
+ /* Quick press, spring release: the fill deepens and the glyph sinks. A held button stays pressed for as long as it repeats. */
28
+ .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) { background: color-mix(in oklab, var(--foreground) 10%, var(--surface)); color: var(--foreground); transition: background-color 60ms linear, color 60ms linear; }
29
+ .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) svg { transform: scale(.82); transition: transform 90ms var(--ease-standard); }
30
+ .step:focus-visible { outline: none; background: var(--surface-muted); color: var(--foreground); }
31
+ /* At a limit the button dims and refuses, but stays in the tab order; pressing it strains the value toward the limit and it shakes once. */
32
+ .step[aria-disabled="true"] { opacity: .32; cursor: not-allowed; }
33
+ .step:disabled { opacity: .35; cursor: not-allowed; }
34
+ .control[data-strain="max"] .step[aria-disabled="true"]:last-child, .control[data-strain="min"] .step[aria-disabled="true"]:first-child { opacity: .6; color: var(--nf-limit); transition: opacity 60ms linear, color 60ms linear; }
35
+ .valueWrap { position: relative; display: flex; min-width: 0; flex: 1; align-self: stretch; align-items: center; justify-content: center; cursor: text; }
36
+ .control[data-disabled] .valueWrap { cursor: not-allowed; }
37
+ /* The number, its prefix, and its suffix travel as one centered group. Tabular digits keep every column the same width. */
38
+ .value { display: inline-flex; align-items: center; white-space: nowrap; color: var(--foreground); font-size: var(--nf-text); transition: color var(--duration-standard) var(--ease-standard); font-weight: 500; line-height: var(--leading-body); letter-spacing: var(--tracking-body); font-variant-numeric: tabular-nums; }
39
+ .affix { display: inline-flex; color: var(--text-secondary); font-weight: 400; }
40
+ .char { display: inline-block; overflow-x: clip; white-space: pre; }
41
+ /* The rolling digits and the real input share one cell. The input only draws the caret and selection; its text is always painted by a span
42
+ (the digits, or the draft mirror while typing), so the value keeps one weight and position whether it is rolling or being edited. */
43
+ .number { position: relative; display: inline-grid; align-items: center; }
44
+ .display { display: inline-flex; grid-area: 1 / 1; }
45
+ .display[data-hidden] { position: absolute; top: 0; left: 0; visibility: hidden; }
46
+ .mirror { grid-area: 1 / 1; white-space: pre; }
47
+ .input { position: absolute; z-index: 1; top: 0; left: 0; width: calc(100% + 1em); height: 100%; margin: 0; border: 0; padding: 0; background: transparent; color: transparent; caret-color: var(--foreground); font: inherit; font-variant-numeric: inherit; letter-spacing: inherit; line-height: inherit; text-align: left; outline: none; }
48
+ .input:focus-visible { outline: none; }
49
+ .input::selection { background: color-mix(in oklab, var(--foreground) 16%, transparent); color: transparent; }
50
+ .input:disabled { cursor: not-allowed; }
51
+ /* Each digit is a wheel whose window reaches a little past the line box and feathers out, so a turning digit fades at the edge instead of being cut.
52
+ The window clips vertically only: a column opening its width shows its whole digit fading in, never a sliver. */
53
+ .column { --feather: .2em; position: relative; display: inline-block; overflow-x: visible; overflow-y: clip; margin-block: calc(var(--feather) * -1); padding-block: var(--feather); mask-image: linear-gradient(to bottom, transparent, #000 calc(var(--feather) * 1.5), #000 calc(100% - var(--feather) * 1.5), transparent); mask-repeat: repeat-x; mask-clip: no-clip; }
54
+ .sizer { visibility: hidden; }
55
+ .glyph { position: absolute; inset: var(--feather) 0; text-align: center; }
56
+ .symbol { display: inline-block; overflow-x: clip; white-space: pre; }
57
+ /* The row's space is padding inside the clipped slot, so the height animates from a true zero. */
58
+ .messageSlot { display: block; overflow: hidden; }
59
+ .hint { display: block; padding-top: var(--space-related); color: var(--text-muted); font-size: var(--text-xs); line-height: var(--leading-body); letter-spacing: var(--tracking-body); font-variant-numeric: tabular-nums; }
60
+ /* Words are measured against their slot when they pop out to leave, so the slot must be the positioned parent. */
61
+ .words { position: relative; display: block; }
62
+ .wordSlot { position: relative; display: inline-block; vertical-align: top; white-space: pre; }
63
+ .wordSizer { position: absolute; top: 0; left: 0; visibility: hidden; white-space: pre; }
64
+ .word { display: inline-block; white-space: pre; }
65
+ .srOnly { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
66
+ /* Reduced motion: no travel. The value changes at once and a press at a limit answers with a brief warning tint instead of a strain. */
67
+ @media (prefers-reduced-motion: reduce) {
68
+ .control, .step { transition: opacity var(--duration-instant) linear, border-color var(--duration-instant) linear, background-color var(--duration-instant) linear; }
69
+ .step:is(:active, [data-pressed]):not(:disabled, [aria-disabled="true"]) svg, .icon svg { transform: none; transition: none; }
70
+ .control[data-strain] .value { color: var(--nf-limit); transition: none; }
71
+ }
@@ -0,0 +1,13 @@
1
+ export interface OtpInputProps {
2
+ length?: number;
3
+ value?: string;
4
+ onChange?: (value: string) => void;
5
+ label: string;
6
+ description?: string;
7
+ error?: string;
8
+ autoFocus?: boolean;
9
+ disabled?: boolean;
10
+ inputMode?: "numeric" | "text";
11
+ className?: string;
12
+ }
13
+ export declare function OtpInput({ length, value, onChange, label, description, error, autoFocus, disabled, inputMode, className }: OtpInputProps): import("react").JSX.Element;