@allxsmith/bestax-bulma 5.26.6 → 5.27.1

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 (342) hide show
  1. package/dist/bestax.css +1 -1
  2. package/dist/bestax.css.map +1 -1
  3. package/dist/esm/columns/Column.js +48 -0
  4. package/dist/esm/columns/Column.js.map +1 -0
  5. package/dist/esm/columns/Columns.js +53 -0
  6. package/dist/esm/columns/Columns.js.map +1 -0
  7. package/dist/esm/components/Avatar.js +299 -0
  8. package/dist/esm/components/Avatar.js.map +1 -0
  9. package/dist/esm/components/Avatars.js +81 -0
  10. package/dist/esm/components/Avatars.js.map +1 -0
  11. package/dist/esm/components/Badge.js +105 -0
  12. package/dist/esm/components/Badge.js.map +1 -0
  13. package/dist/esm/components/Breadcrumb.js +36 -0
  14. package/dist/esm/components/Breadcrumb.js.map +1 -0
  15. package/dist/esm/components/Card.js +252 -0
  16. package/dist/esm/components/Card.js.map +1 -0
  17. package/dist/esm/components/Carousel.js +351 -0
  18. package/dist/esm/components/Carousel.js.map +1 -0
  19. package/dist/esm/components/Collapse.js +193 -0
  20. package/dist/esm/components/Collapse.js.map +1 -0
  21. package/dist/esm/components/Collapses.js +99 -0
  22. package/dist/esm/components/Collapses.js.map +1 -0
  23. package/dist/esm/components/Dialog.js +230 -0
  24. package/dist/esm/components/Dialog.js.map +1 -0
  25. package/dist/esm/components/Dropdown.js +362 -0
  26. package/dist/esm/components/Dropdown.js.map +1 -0
  27. package/dist/esm/components/Loading.js +87 -0
  28. package/dist/esm/components/Loading.js.map +1 -0
  29. package/dist/esm/components/Menu.js +117 -0
  30. package/dist/esm/components/Menu.js.map +1 -0
  31. package/dist/esm/components/Message.js +56 -0
  32. package/dist/esm/components/Message.js.map +1 -0
  33. package/dist/esm/components/Modal.js +354 -0
  34. package/dist/esm/components/Modal.js.map +1 -0
  35. package/dist/esm/components/Navbar.js +287 -0
  36. package/dist/esm/components/Navbar.js.map +1 -0
  37. package/dist/esm/components/Pagination.js +191 -0
  38. package/dist/esm/components/Pagination.js.map +1 -0
  39. package/dist/esm/components/Panel.js +100 -0
  40. package/dist/esm/components/Panel.js.map +1 -0
  41. package/dist/esm/components/Popover.js +298 -0
  42. package/dist/esm/components/Popover.js.map +1 -0
  43. package/dist/esm/components/Reveal.js +157 -0
  44. package/dist/esm/components/Reveal.js.map +1 -0
  45. package/dist/esm/components/Sidebar.js +209 -0
  46. package/dist/esm/components/Sidebar.js.map +1 -0
  47. package/dist/esm/components/Steps.js +128 -0
  48. package/dist/esm/components/Steps.js.map +1 -0
  49. package/dist/esm/components/Tabs.js +335 -0
  50. package/dist/esm/components/Tabs.js.map +1 -0
  51. package/dist/esm/components/Toast.js +322 -0
  52. package/dist/esm/components/Toast.js.map +1 -0
  53. package/dist/esm/components/Tooltip.js +202 -0
  54. package/dist/esm/components/Tooltip.js.map +1 -0
  55. package/dist/esm/components/collapsesContext.js +12 -0
  56. package/dist/esm/components/collapsesContext.js.map +1 -0
  57. package/dist/esm/elements/Block.js +29 -0
  58. package/dist/esm/elements/Block.js.map +1 -0
  59. package/dist/esm/elements/Box.js +32 -0
  60. package/dist/esm/elements/Box.js.map +1 -0
  61. package/dist/esm/elements/Button.js +139 -0
  62. package/dist/esm/elements/Button.js.map +1 -0
  63. package/dist/esm/elements/Buttons.js +42 -0
  64. package/dist/esm/elements/Buttons.js.map +1 -0
  65. package/dist/esm/elements/Code.js +25 -0
  66. package/dist/esm/elements/Code.js.map +1 -0
  67. package/dist/esm/elements/Content.js +33 -0
  68. package/dist/esm/elements/Content.js.map +1 -0
  69. package/dist/esm/elements/Delete.js +35 -0
  70. package/dist/esm/elements/Delete.js.map +1 -0
  71. package/dist/esm/elements/Divider.js +27 -0
  72. package/dist/esm/elements/Divider.js.map +1 -0
  73. package/dist/esm/elements/Emphasis.js +25 -0
  74. package/dist/esm/elements/Emphasis.js.map +1 -0
  75. package/dist/esm/elements/Figure.js +58 -0
  76. package/dist/esm/elements/Figure.js.map +1 -0
  77. package/dist/esm/elements/Icon.js +256 -0
  78. package/dist/esm/elements/Icon.js.map +1 -0
  79. package/dist/esm/elements/IconText.js +36 -0
  80. package/dist/esm/elements/IconText.js.map +1 -0
  81. package/dist/esm/elements/Image.js +46 -0
  82. package/dist/esm/elements/Image.js.map +1 -0
  83. package/dist/esm/elements/Link.js +36 -0
  84. package/dist/esm/elements/Link.js.map +1 -0
  85. package/dist/esm/elements/LinkButton.js +37 -0
  86. package/dist/esm/elements/LinkButton.js.map +1 -0
  87. package/dist/esm/elements/ListItem.js +28 -0
  88. package/dist/esm/elements/ListItem.js.map +1 -0
  89. package/dist/esm/elements/Loader.js +37 -0
  90. package/dist/esm/elements/Loader.js.map +1 -0
  91. package/dist/esm/elements/Notification.js +316 -0
  92. package/dist/esm/elements/Notification.js.map +1 -0
  93. package/dist/esm/elements/OrderedList.js +31 -0
  94. package/dist/esm/elements/OrderedList.js.map +1 -0
  95. package/dist/esm/elements/Paragraph.js +28 -0
  96. package/dist/esm/elements/Paragraph.js.map +1 -0
  97. package/dist/esm/elements/Pre.js +25 -0
  98. package/dist/esm/elements/Pre.js.map +1 -0
  99. package/dist/esm/elements/Progress.js +33 -0
  100. package/dist/esm/elements/Progress.js.map +1 -0
  101. package/dist/esm/elements/Skeleton.js +30 -0
  102. package/dist/esm/elements/Skeleton.js.map +1 -0
  103. package/dist/esm/elements/Span.js +28 -0
  104. package/dist/esm/elements/Span.js.map +1 -0
  105. package/dist/esm/elements/Strong.js +25 -0
  106. package/dist/esm/elements/Strong.js.map +1 -0
  107. package/dist/esm/elements/SubTitle.js +51 -0
  108. package/dist/esm/elements/SubTitle.js.map +1 -0
  109. package/dist/esm/elements/Table.js +51 -0
  110. package/dist/esm/elements/Table.js.map +1 -0
  111. package/dist/esm/elements/Tag.js +49 -0
  112. package/dist/esm/elements/Tag.js.map +1 -0
  113. package/dist/esm/elements/Tags.js +34 -0
  114. package/dist/esm/elements/Tags.js.map +1 -0
  115. package/dist/esm/elements/Tbody.js +26 -0
  116. package/dist/esm/elements/Tbody.js.map +1 -0
  117. package/dist/esm/elements/Td.js +61 -0
  118. package/dist/esm/elements/Td.js.map +1 -0
  119. package/dist/esm/elements/Tfoot.js +26 -0
  120. package/dist/esm/elements/Tfoot.js.map +1 -0
  121. package/dist/esm/elements/Th.js +34 -0
  122. package/dist/esm/elements/Th.js.map +1 -0
  123. package/dist/esm/elements/Thead.js +26 -0
  124. package/dist/esm/elements/Thead.js.map +1 -0
  125. package/dist/esm/elements/Title.js +44 -0
  126. package/dist/esm/elements/Title.js.map +1 -0
  127. package/dist/esm/elements/Tr.js +31 -0
  128. package/dist/esm/elements/Tr.js.map +1 -0
  129. package/dist/esm/elements/UnorderedList.js +31 -0
  130. package/dist/esm/elements/UnorderedList.js.map +1 -0
  131. package/dist/esm/elements/iconProps.js +49 -0
  132. package/dist/esm/elements/iconProps.js.map +1 -0
  133. package/dist/esm/form/Autocomplete.js +279 -0
  134. package/dist/esm/form/Autocomplete.js.map +1 -0
  135. package/dist/esm/form/Checkbox.js +111 -0
  136. package/dist/esm/form/Checkbox.js.map +1 -0
  137. package/dist/esm/form/Checkboxes.js +89 -0
  138. package/dist/esm/form/Checkboxes.js.map +1 -0
  139. package/dist/esm/form/Control.js +79 -0
  140. package/dist/esm/form/Control.js.map +1 -0
  141. package/dist/esm/form/DateInput.js +87 -0
  142. package/dist/esm/form/DateInput.js.map +1 -0
  143. package/dist/esm/form/DateInputBase.js +263 -0
  144. package/dist/esm/form/DateInputBase.js.map +1 -0
  145. package/dist/esm/form/DateRangeInput.js +76 -0
  146. package/dist/esm/form/DateRangeInput.js.map +1 -0
  147. package/dist/esm/form/DateRangeInputBase.js +301 -0
  148. package/dist/esm/form/DateRangeInputBase.js.map +1 -0
  149. package/dist/esm/form/DateTimeInput.js +87 -0
  150. package/dist/esm/form/DateTimeInput.js.map +1 -0
  151. package/dist/esm/form/DateTimeInputBase.js +333 -0
  152. package/dist/esm/form/DateTimeInputBase.js.map +1 -0
  153. package/dist/esm/form/Field.js +153 -0
  154. package/dist/esm/form/Field.js.map +1 -0
  155. package/dist/esm/form/File.js +109 -0
  156. package/dist/esm/form/File.js.map +1 -0
  157. package/dist/esm/form/FormContext.js +170 -0
  158. package/dist/esm/form/FormContext.js.map +1 -0
  159. package/dist/esm/form/Input.js +88 -0
  160. package/dist/esm/form/Input.js.map +1 -0
  161. package/dist/esm/form/InputBase.js +40 -0
  162. package/dist/esm/form/InputBase.js.map +1 -0
  163. package/dist/esm/form/Numberinput.js +199 -0
  164. package/dist/esm/form/Numberinput.js.map +1 -0
  165. package/dist/esm/form/Radio.js +108 -0
  166. package/dist/esm/form/Radio.js.map +1 -0
  167. package/dist/esm/form/Radios.js +91 -0
  168. package/dist/esm/form/Radios.js.map +1 -0
  169. package/dist/esm/form/Rate.js +279 -0
  170. package/dist/esm/form/Rate.js.map +1 -0
  171. package/dist/esm/form/Select.js +81 -0
  172. package/dist/esm/form/Select.js.map +1 -0
  173. package/dist/esm/form/SelectBase.js +54 -0
  174. package/dist/esm/form/SelectBase.js.map +1 -0
  175. package/dist/esm/form/Slider.js +459 -0
  176. package/dist/esm/form/Slider.js.map +1 -0
  177. package/dist/esm/form/Switch.js +96 -0
  178. package/dist/esm/form/Switch.js.map +1 -0
  179. package/dist/esm/form/Taginput.js +352 -0
  180. package/dist/esm/form/Taginput.js.map +1 -0
  181. package/dist/esm/form/TextArea.js +77 -0
  182. package/dist/esm/form/TextArea.js.map +1 -0
  183. package/dist/esm/form/TextAreaBase.js +45 -0
  184. package/dist/esm/form/TextAreaBase.js.map +1 -0
  185. package/dist/esm/form/TimeInput.js +83 -0
  186. package/dist/esm/form/TimeInput.js.map +1 -0
  187. package/dist/esm/form/TimeInputBase.js +246 -0
  188. package/dist/esm/form/TimeInputBase.js.map +1 -0
  189. package/dist/esm/form/_pickerInternals/Calendar.js +760 -0
  190. package/dist/esm/form/_pickerInternals/Calendar.js.map +1 -0
  191. package/dist/esm/form/_pickerInternals/PickerPopover.js +95 -0
  192. package/dist/esm/form/_pickerInternals/PickerPopover.js.map +1 -0
  193. package/dist/esm/form/_pickerInternals/TimeWheels.js +721 -0
  194. package/dist/esm/form/_pickerInternals/TimeWheels.js.map +1 -0
  195. package/dist/esm/form/_pickerInternals/audioTick.js +109 -0
  196. package/dist/esm/form/_pickerInternals/audioTick.js.map +1 -0
  197. package/dist/esm/form/_pickerInternals/dateUtils.js +267 -0
  198. package/dist/esm/form/_pickerInternals/dateUtils.js.map +1 -0
  199. package/dist/esm/form/_pickerInternals/formatters.js +322 -0
  200. package/dist/esm/form/_pickerInternals/formatters.js.map +1 -0
  201. package/dist/esm/form/_pickerInternals/haptics.js +38 -0
  202. package/dist/esm/form/_pickerInternals/haptics.js.map +1 -0
  203. package/dist/esm/form/_pickerInternals/nativeInputSupport.js +25 -0
  204. package/dist/esm/form/_pickerInternals/nativeInputSupport.js.map +1 -0
  205. package/dist/esm/form/_pickerInternals/pickerTypes.js +50 -0
  206. package/dist/esm/form/_pickerInternals/pickerTypes.js.map +1 -0
  207. package/dist/esm/form/_pickerInternals/segmentMap.js +315 -0
  208. package/dist/esm/form/_pickerInternals/segmentMap.js.map +1 -0
  209. package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js +49 -0
  210. package/dist/esm/form/_pickerInternals/useNativeMobilePicker.js.map +1 -0
  211. package/dist/esm/form/_pickerInternals/useSegmentedEntry.js +427 -0
  212. package/dist/esm/form/_pickerInternals/useSegmentedEntry.js.map +1 -0
  213. package/dist/esm/form/controlLoading.js +18 -0
  214. package/dist/esm/form/controlLoading.js.map +1 -0
  215. package/dist/esm/form/useAutoLabelId.js +54 -0
  216. package/dist/esm/form/useAutoLabelId.js.map +1 -0
  217. package/dist/esm/grid/Cell.js +35 -0
  218. package/dist/esm/grid/Cell.js.map +1 -0
  219. package/dist/esm/grid/Grid.js +51 -0
  220. package/dist/esm/grid/Grid.js.map +1 -0
  221. package/dist/esm/helpers/ClientOnly.js +33 -0
  222. package/dist/esm/helpers/ClientOnly.js.map +1 -0
  223. package/dist/esm/helpers/Config.js +60 -0
  224. package/dist/esm/helpers/Config.js.map +1 -0
  225. package/dist/esm/helpers/Theme.js +947 -0
  226. package/dist/esm/helpers/Theme.js.map +1 -0
  227. package/dist/esm/helpers/anchorAttrs.js +72 -0
  228. package/dist/esm/helpers/anchorAttrs.js.map +1 -0
  229. package/dist/esm/helpers/bulmaClassHelpers.js +415 -0
  230. package/dist/esm/helpers/bulmaClassHelpers.js.map +1 -0
  231. package/dist/esm/helpers/buttonType.js +24 -0
  232. package/dist/esm/helpers/buttonType.js.map +1 -0
  233. package/dist/esm/helpers/classNames.js +137 -0
  234. package/dist/esm/helpers/classNames.js.map +1 -0
  235. package/dist/esm/helpers/colorDeprecations.js +50 -0
  236. package/dist/esm/helpers/colorDeprecations.js.map +1 -0
  237. package/dist/esm/helpers/devWarnings.js +27 -0
  238. package/dist/esm/helpers/devWarnings.js.map +1 -0
  239. package/dist/esm/helpers/inertProps.js +27 -0
  240. package/dist/esm/helpers/inertProps.js.map +1 -0
  241. package/dist/esm/helpers/mergeBulmaStyles.js +20 -0
  242. package/dist/esm/helpers/mergeBulmaStyles.js.map +1 -0
  243. package/dist/esm/helpers/polymorphic.js +18 -0
  244. package/dist/esm/helpers/polymorphic.js.map +1 -0
  245. package/dist/esm/helpers/portal.js +76 -0
  246. package/dist/esm/helpers/portal.js.map +1 -0
  247. package/dist/esm/helpers/positionStacks.js +45 -0
  248. package/dist/esm/helpers/positionStacks.js.map +1 -0
  249. package/dist/esm/helpers/scrollLock.js +38 -0
  250. package/dist/esm/helpers/scrollLock.js.map +1 -0
  251. package/dist/esm/helpers/shadowDom.js +71 -0
  252. package/dist/esm/helpers/shadowDom.js.map +1 -0
  253. package/dist/esm/helpers/statusRegion.js +282 -0
  254. package/dist/esm/helpers/statusRegion.js.map +1 -0
  255. package/dist/esm/helpers/useAnchoredPosition.js +86 -0
  256. package/dist/esm/helpers/useAnchoredPosition.js.map +1 -0
  257. package/dist/esm/helpers/useBulmaClasses.js +158 -0
  258. package/dist/esm/helpers/useBulmaClasses.js.map +1 -0
  259. package/dist/esm/helpers/useClientLayoutEffect.js +13 -0
  260. package/dist/esm/helpers/useClientLayoutEffect.js.map +1 -0
  261. package/dist/esm/helpers/useColorClasses.js +84 -0
  262. package/dist/esm/helpers/useColorClasses.js.map +1 -0
  263. package/dist/esm/helpers/useFlexboxClasses.js +92 -0
  264. package/dist/esm/helpers/useFlexboxClasses.js.map +1 -0
  265. package/dist/esm/helpers/useFocusTrap.js +470 -0
  266. package/dist/esm/helpers/useFocusTrap.js.map +1 -0
  267. package/dist/esm/helpers/useIsHydrated.js +31 -0
  268. package/dist/esm/helpers/useIsHydrated.js.map +1 -0
  269. package/dist/esm/helpers/useOtherClasses.js +115 -0
  270. package/dist/esm/helpers/useOtherClasses.js.map +1 -0
  271. package/dist/esm/helpers/useSpacingClasses.js +82 -0
  272. package/dist/esm/helpers/useSpacingClasses.js.map +1 -0
  273. package/dist/esm/helpers/useTypographyClasses.js +93 -0
  274. package/dist/esm/helpers/useTypographyClasses.js.map +1 -0
  275. package/dist/esm/helpers/useVisibilityClasses.js +134 -0
  276. package/dist/esm/helpers/useVisibilityClasses.js.map +1 -0
  277. package/dist/esm/helpers/withSubComponents.js +24 -0
  278. package/dist/esm/helpers/withSubComponents.js.map +1 -0
  279. package/dist/esm/layout/Container.js +48 -0
  280. package/dist/esm/layout/Container.js.map +1 -0
  281. package/dist/esm/layout/Footer.js +33 -0
  282. package/dist/esm/layout/Footer.js.map +1 -0
  283. package/dist/esm/layout/Hero.js +90 -0
  284. package/dist/esm/layout/Hero.js.map +1 -0
  285. package/dist/esm/layout/Level.js +110 -0
  286. package/dist/esm/layout/Level.js.map +1 -0
  287. package/dist/esm/layout/Media.js +86 -0
  288. package/dist/esm/layout/Media.js.map +1 -0
  289. package/dist/esm/layout/Section.js +30 -0
  290. package/dist/esm/layout/Section.js.map +1 -0
  291. package/dist/extras.css +1 -1
  292. package/dist/extras.css.map +1 -1
  293. package/dist/index.cjs +821 -102
  294. package/dist/index.cjs.map +1 -1
  295. package/dist/index.esm.js +115 -16845
  296. package/dist/index.esm.js.map +1 -1
  297. package/dist/types/components/Tabs.d.ts +25 -3
  298. package/dist/types/elements/Icon.d.ts +10 -7
  299. package/dist/types/form/DateInputBase.d.ts +8 -6
  300. package/dist/types/form/DateRangeInput.d.ts +91 -0
  301. package/dist/types/form/DateRangeInputBase.d.ts +172 -0
  302. package/dist/types/form/DateTimeInputBase.d.ts +10 -6
  303. package/dist/types/form/File.d.ts +18 -5
  304. package/dist/types/form/FormContext.d.ts +11 -0
  305. package/dist/types/form/TimeInputBase.d.ts +1 -1
  306. package/dist/types/form/_pickerInternals/PickerPopover.d.ts +1 -0
  307. package/dist/types/form/_pickerInternals/dateUtils.d.ts +7 -0
  308. package/dist/types/form/_pickerInternals/formatters.d.ts +14 -0
  309. package/dist/types/form/_pickerInternals/pickerTypes.d.ts +13 -0
  310. package/dist/types/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  311. package/dist/types/index.d.ts +4 -0
  312. package/dist/types-cjs/components/Tabs.d.ts +25 -3
  313. package/dist/types-cjs/elements/Icon.d.ts +10 -7
  314. package/dist/types-cjs/form/DateInputBase.d.ts +8 -6
  315. package/dist/types-cjs/form/DateRangeInput.d.ts +91 -0
  316. package/dist/types-cjs/form/DateRangeInputBase.d.ts +172 -0
  317. package/dist/types-cjs/form/DateTimeInputBase.d.ts +10 -6
  318. package/dist/types-cjs/form/File.d.ts +18 -5
  319. package/dist/types-cjs/form/FormContext.d.ts +11 -0
  320. package/dist/types-cjs/form/TimeInputBase.d.ts +1 -1
  321. package/dist/types-cjs/form/_pickerInternals/PickerPopover.d.ts +1 -0
  322. package/dist/types-cjs/form/_pickerInternals/dateUtils.d.ts +7 -0
  323. package/dist/types-cjs/form/_pickerInternals/formatters.d.ts +14 -0
  324. package/dist/types-cjs/form/_pickerInternals/pickerTypes.d.ts +13 -0
  325. package/dist/types-cjs/form/_pickerInternals/useSegmentedEntry.d.ts +12 -3
  326. package/dist/types-cjs/index.d.ts +4 -0
  327. package/dist/versions/bestax-no-dark-mode.css +1 -1
  328. package/dist/versions/bestax-no-dark-mode.css.map +1 -1
  329. package/dist/versions/bestax-no-helpers-prefixed.css +1 -1
  330. package/dist/versions/bestax-no-helpers-prefixed.css.map +1 -1
  331. package/dist/versions/bestax-no-helpers.css +1 -1
  332. package/dist/versions/bestax-no-helpers.css.map +1 -1
  333. package/dist/versions/bestax-prefixed.css +1 -1
  334. package/dist/versions/bestax-prefixed.css.map +1 -1
  335. package/package.json +1 -1
  336. package/src/scss/components/_tabs.scss +44 -0
  337. package/src/scss/form/_dateinput.scss +87 -8
  338. package/src/scss/form/_daterangeinput.scss +63 -0
  339. package/src/scss/form/_datetimeinput.scss +7 -2
  340. package/src/scss/form/_file.scss +37 -0
  341. package/src/scss/form/_index.scss +2 -0
  342. package/src/scss/form/_picker-popover.scss +1 -4
@@ -0,0 +1,721 @@
1
+ /* @allxsmith/bestax-bulma — AI agents: see AGENTS.md in the package root, or https://bestax.io/llms.txt */
2
+ import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
3
+ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
4
+ import { prefixedClassNames, usePrefixedClassNames, classNames } from '../../helpers/classNames.js';
5
+ import { useConfig } from '../../helpers/Config.js';
6
+ import { isPopoverToggleKey, mergeLabels } from './pickerTypes.js';
7
+ import { setTimeOfDay } from './dateUtils.js';
8
+ import { tickHaptic } from './haptics.js';
9
+ import { playAudioTick, unlockAudioTick } from './audioTick.js';
10
+
11
+ const wrap = (n, max) => ((n % max) + max) % max;
12
+ /**
13
+ * Walk forward (or backward) from `start` in `step` steps until `test` returns
14
+ * true or we've cycled all `max` candidates. Used to skip past unselectable
15
+ * values without freezing the wheel.
16
+ */
17
+ function nextValid(start, step, max, test) {
18
+ let candidate = wrap(start, max);
19
+ for (let i = 0; i < max; i++) {
20
+ if (test(candidate))
21
+ return candidate;
22
+ candidate = wrap(candidate + step, max);
23
+ }
24
+ return start;
25
+ }
26
+ const pad2 = (n) => String(n).padStart(2, '0');
27
+ const WheelInner = ({ values, index, onChange, formatLabel, formatAriaText, ariaLabel, visibleCount, itemHeight, disabled, size, color, onFocusPrev, onFocusNext, onCommit, wrap = false, disabledFor, audioTick = false, }, ref) => {
28
+ const { classPrefix } = useConfig();
29
+ const N = values.length;
30
+ const half = Math.floor(visibleCount / 2);
31
+ const containerHeight = visibleCount * itemHeight;
32
+ // Buffer around the visible window so adjacent items animate in/out smoothly.
33
+ const BUFFER = half + 2;
34
+ // Continuous wheel position in units of items. Math.round(position) is the
35
+ // "active" virtual index; the fractional part is the drag/momentum offset
36
+ // within an item slot. Using a single float (rather than virtualIdx +
37
+ // dragOffset) keeps drag and tick-update in lockstep so items move with the
38
+ // finger without a 40px overshoot when crossing item boundaries.
39
+ const [position, setPosition] = useState(index);
40
+ const positionRef = useRef(index);
41
+ positionRef.current = position;
42
+ const [isDragging, setIsDragging] = useState(false);
43
+ const rootRef = useRef(null);
44
+ // Ref to the stationary selection band — populated by the JSX below. The
45
+ // band briefly scales + brightens on each item tick so users perceive a
46
+ // body-felt "thunk" even on iOS where no haptic API exists.
47
+ const bandRef = useRef(null);
48
+ // Cached MediaQueryList for `prefers-reduced-motion`. We read this on
49
+ // every tick to decide whether to fire the band pulse; caching the MQL
50
+ // saves a `matchMedia` call per tick (which can fire at ~20Hz during a
51
+ // momentum flick).
52
+ const reducedMotionMqlRef = useRef(null);
53
+ if (reducedMotionMqlRef.current === null &&
54
+ typeof window !== 'undefined' &&
55
+ typeof window.matchMedia === 'function') {
56
+ reducedMotionMqlRef.current = window.matchMedia('(prefers-reduced-motion: reduce)');
57
+ }
58
+ // In-flight band pulse animation (Web Animations API). Tracked so we can
59
+ // cancel on unmount; the WAI's `composite: 'replace'` semantics handle
60
+ // re-trigger automatically without us needing to cancel between ticks.
61
+ const pulseAnimRef = useRef(null);
62
+ // True while the external `index` prop sync useEffect runs, so we don't
63
+ // fire a haptic blip when the parent updates the value programmatically.
64
+ const isSyncingRef = useRef(false);
65
+ // Tiny haptic/audio/visual blip per item tick. Haptic uses
66
+ // `navigator.vibrate` where available (Android Chrome / Firefox Android /
67
+ // Samsung Internet); iOS has no web-accessible haptic path as of May
68
+ // 2026, so the optional `audioTick` plays a low-frequency thunk as the
69
+ // closest UX substitute. The visual band-pulse fires on every tick on
70
+ // every platform (gated only by `prefers-reduced-motion`) so the
71
+ // selection band feels reactive — visual reinforcement of the tactile
72
+ // feedback. We skip all three during programmatic prop sync so a parent
73
+ // re-render doesn't blip.
74
+ const audioTickEnabledRef = useRef(audioTick);
75
+ audioTickEnabledRef.current = audioTick;
76
+ const playBandPulse = useCallback(() => {
77
+ const el = bandRef.current;
78
+ if (!el || typeof el.animate !== 'function')
79
+ return;
80
+ if (reducedMotionMqlRef.current?.matches)
81
+ return;
82
+ // Compound scale + brightness pulse, peaking at 40% through the 110ms
83
+ // duration. `composite: 'replace'` means a re-trigger mid-pulse cleanly
84
+ // restarts rather than queueing — important at fling rates where ticks
85
+ // can fire every ~50ms.
86
+ pulseAnimRef.current = el.animate([
87
+ { transform: 'scale(1)', filter: 'brightness(1)' },
88
+ {
89
+ transform: 'scale(1.04)',
90
+ filter: 'brightness(1.15)',
91
+ offset: 0.4,
92
+ },
93
+ { transform: 'scale(1)', filter: 'brightness(1)' },
94
+ ], { duration: 110, easing: 'ease-out', composite: 'replace' });
95
+ }, []);
96
+ const tickFeedback = useCallback(() => {
97
+ /* istanbul ignore next: the sync flag is only true synchronously inside the prop-sync effect, which sets position directly and never routes through commitPosition */
98
+ if (isSyncingRef.current)
99
+ return;
100
+ tickHaptic();
101
+ if (audioTickEnabledRef.current)
102
+ playAudioTick();
103
+ playBandPulse();
104
+ }, [playBandPulse]);
105
+ const dragStateRef = useRef({
106
+ pointerId: null,
107
+ startY: 0,
108
+ startPosition: 0,
109
+ captured: false,
110
+ samples: [],
111
+ });
112
+ // RAF id for the in-flight momentum / snap animation, so we can cancel when
113
+ // the user grabs the wheel again or the component unmounts.
114
+ const rafIdRef = useRef(null);
115
+ const virtualIdx = Math.round(position);
116
+ const cancelRaf = useCallback(() => {
117
+ if (rafIdRef.current !== null) {
118
+ cancelAnimationFrame(rafIdRef.current);
119
+ rafIdRef.current = null;
120
+ }
121
+ }, []);
122
+ useEffect(() => () => {
123
+ cancelRaf();
124
+ // Cancel any in-flight band pulse so it doesn't keep a reference to
125
+ // a detached DOM node. WAI Animation.cancel() is safe to call even
126
+ // if the animation has already finished.
127
+ pulseAnimRef.current?.cancel();
128
+ pulseAnimRef.current = null;
129
+ }, [cancelRaf]);
130
+ React.useImperativeHandle(ref, () => ({
131
+ focus: () => rootRef.current?.focus(),
132
+ startDrag: (pointerId, clientY) => {
133
+ /* istanbul ignore next: the empty-space forwarder (sole caller) already returns on the same disabled prop */
134
+ if (disabled)
135
+ return;
136
+ cancelRaf();
137
+ if (audioTickEnabledRef.current)
138
+ unlockAudioTick();
139
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
140
+ dragStateRef.current = {
141
+ pointerId,
142
+ startY: clientY,
143
+ startPosition: positionRef.current,
144
+ // Skip the drag-threshold gate: the parent forwarder has already
145
+ // decided this is a drag (a pointerdown landed on empty space,
146
+ // which has no click target).
147
+ captured: true,
148
+ samples: [{ t: now, y: clientY }],
149
+ };
150
+ try {
151
+ rootRef.current?.setPointerCapture(pointerId);
152
+ }
153
+ catch {
154
+ // setPointerCapture can throw if the pointer isn't currently
155
+ // active for this element; the existing onPointerMove handler
156
+ // still matches on pointerId so the drag will work either way.
157
+ }
158
+ setIsDragging(true);
159
+ },
160
+ }), [disabled, cancelRaf]);
161
+ // Resolve a virtualIdx into an actual values[] index (modulo wrap if `wrap`).
162
+ const toActual = useCallback((v) => {
163
+ if (wrap)
164
+ return ((v % N) + N) % N;
165
+ return Math.max(0, Math.min(N - 1, v));
166
+ }, [wrap, N]);
167
+ const clampPosition = useCallback((p) => {
168
+ if (wrap)
169
+ return p;
170
+ return Math.max(0, Math.min(N - 1, p));
171
+ }, [wrap, N]);
172
+ // Update position and fire onChange if the rounded value changed.
173
+ const commitPosition = useCallback((next) => {
174
+ const clamped = clampPosition(next);
175
+ const prevRound = Math.round(positionRef.current);
176
+ const nextRound = Math.round(clamped);
177
+ positionRef.current = clamped;
178
+ setPosition(clamped);
179
+ if (nextRound !== prevRound) {
180
+ onChange(toActual(nextRound));
181
+ tickFeedback();
182
+ }
183
+ }, [clampPosition, onChange, toActual, tickFeedback]);
184
+ // When the external `index` changes (e.g. from typed input), re-sync
185
+ // position to the closest virtual position that maps to it.
186
+ //
187
+ // Crucial subtlety: this effect re-runs every time the parent value
188
+ // changes — including the round-trip case where the user's own scroll
189
+ // commits a tick → fires onChange → parent re-renders with the new
190
+ // index → here. If we cancelled the in-flight momentum/snap RAF on
191
+ // every such echo, the snap would never get to land on an integer
192
+ // (the user's wheel would stop wherever the last tick fired). So we
193
+ // bail out early when the rounded position already matches the
194
+ // incoming index — the echo case — and only cancel + apply an actual
195
+ // jump when the parent has moved us somewhere we don't already round
196
+ // to.
197
+ useEffect(() => {
198
+ const prev = positionRef.current;
199
+ const wrappedRoundPrev = ((Math.round(prev) % N) + N) % N;
200
+ if (wrappedRoundPrev === index)
201
+ return;
202
+ cancelRaf();
203
+ isSyncingRef.current = true;
204
+ setPosition(prevPos => {
205
+ if (!wrap) {
206
+ positionRef.current = index;
207
+ return index;
208
+ }
209
+ const wrappedPrev = ((prevPos % N) + N) % N;
210
+ const forward = (index - wrappedPrev + N) % N;
211
+ const backward = forward - N;
212
+ const delta = Math.abs(forward) <= Math.abs(backward) ? forward : backward;
213
+ const next = prevPos + delta;
214
+ positionRef.current = next;
215
+ return next;
216
+ });
217
+ isSyncingRef.current = false;
218
+ }, [index, N, wrap, cancelRaf]);
219
+ const moveBy = useCallback((delta) => {
220
+ if (disabled)
221
+ return;
222
+ cancelRaf();
223
+ commitPosition(Math.round(positionRef.current) + delta);
224
+ }, [disabled, cancelRaf, commitPosition]);
225
+ // The keys are a spinbutton's: up raises the value and down lowers it. The
226
+ // values run down the wheel, so up brings the one below the band into it,
227
+ // the way a drag or a scroll that moves the items up does.
228
+ const onKeyDown = useCallback((e) => {
229
+ if (isPopoverToggleKey(e))
230
+ return;
231
+ switch (e.key) {
232
+ case 'ArrowUp':
233
+ e.preventDefault();
234
+ moveBy(1);
235
+ break;
236
+ case 'ArrowDown':
237
+ e.preventDefault();
238
+ moveBy(-1);
239
+ break;
240
+ case 'PageUp':
241
+ e.preventDefault();
242
+ moveBy(5);
243
+ break;
244
+ case 'PageDown':
245
+ e.preventDefault();
246
+ moveBy(-5);
247
+ break;
248
+ case 'Home':
249
+ e.preventDefault();
250
+ if (disabled)
251
+ break;
252
+ cancelRaf();
253
+ commitPosition(0);
254
+ break;
255
+ case 'End':
256
+ e.preventDefault();
257
+ if (disabled)
258
+ break;
259
+ cancelRaf();
260
+ commitPosition(N - 1);
261
+ break;
262
+ case 'ArrowLeft':
263
+ e.preventDefault();
264
+ onFocusPrev?.();
265
+ break;
266
+ case 'ArrowRight':
267
+ e.preventDefault();
268
+ onFocusNext?.();
269
+ break;
270
+ case 'Enter':
271
+ e.preventDefault();
272
+ onCommit?.();
273
+ break;
274
+ }
275
+ }, [
276
+ moveBy,
277
+ disabled,
278
+ commitPosition,
279
+ cancelRaf,
280
+ N,
281
+ onFocusPrev,
282
+ onFocusNext,
283
+ onCommit,
284
+ ]);
285
+ // Animate position to the nearest integer with ease-out cubic. Used after a
286
+ // drag / momentum settles, so the wheel always rests aligned to an item.
287
+ const animateSnap = useCallback(() => {
288
+ cancelRaf();
289
+ const start = positionRef.current;
290
+ const target = Math.round(start);
291
+ if (start === target)
292
+ return;
293
+ const duration = 180;
294
+ const startTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
295
+ const tick = (now) => {
296
+ const t = Math.min(1, (now - startTime) / duration);
297
+ const eased = 1 - Math.pow(1 - t, 3);
298
+ const next = start + (target - start) * eased;
299
+ commitPosition(next);
300
+ if (t < 1) {
301
+ rafIdRef.current = requestAnimationFrame(tick);
302
+ }
303
+ else {
304
+ rafIdRef.current = null;
305
+ }
306
+ };
307
+ rafIdRef.current = requestAnimationFrame(tick);
308
+ }, [cancelRaf, commitPosition]);
309
+ // Kinetic momentum after a flick. Uses exponential friction so the velocity
310
+ // tapers smoothly. Updates `position` in items/ms; commitPosition handles
311
+ // the onChange ticks as virtualIdx crosses integers.
312
+ const startMomentum = useCallback((initialPxPerMs) => {
313
+ cancelRaf();
314
+ // Drag-up (negative dy/dt in screen coords) should INCREASE position
315
+ // (later values come into view), so flip the sign.
316
+ let velocity = -initialPxPerMs / itemHeight; // items / ms
317
+ // Cap initial velocity so a frantic swipe doesn't blast through the
318
+ // whole list. Empirically tuned to match iOS feel.
319
+ const MAX_VEL = 0.05; // items/ms (≈ 50 items/sec)
320
+ if (velocity > MAX_VEL)
321
+ velocity = MAX_VEL;
322
+ if (velocity < -MAX_VEL)
323
+ velocity = -MAX_VEL;
324
+ const STOP_VEL = 0.0015; // items/ms (≈ 1.5 items/sec)
325
+ // Friction coefficient per millisecond — chosen so velocity halves
326
+ // every ~120ms.
327
+ const FRICTION_PER_MS = Math.pow(0.5, 1 / 120);
328
+ let lastTime = typeof performance !== 'undefined' ? performance.now() : Date.now();
329
+ const tick = (now) => {
330
+ const dt = Math.max(1, now - lastTime);
331
+ lastTime = now;
332
+ let next = positionRef.current + velocity * dt;
333
+ // Boundary handling for non-wrapping wheels: clamp and zero the
334
+ // velocity so we don't keep ticking against the wall.
335
+ if (!wrap) {
336
+ if (next <= 0) {
337
+ next = 0;
338
+ velocity = 0;
339
+ }
340
+ else if (next >= N - 1) {
341
+ next = N - 1;
342
+ velocity = 0;
343
+ }
344
+ }
345
+ commitPosition(next);
346
+ velocity *= Math.pow(FRICTION_PER_MS, dt);
347
+ if (Math.abs(velocity) < STOP_VEL) {
348
+ animateSnap();
349
+ return;
350
+ }
351
+ rafIdRef.current = requestAnimationFrame(tick);
352
+ };
353
+ rafIdRef.current = requestAnimationFrame(tick);
354
+ }, [cancelRaf, commitPosition, itemHeight, wrap, N, animateSnap]);
355
+ // Mouse wheel: accumulate deltaY across events and step when the
356
+ // accumulator crosses SCROLL_STEP_PX. Trackpads emit dozens of small
357
+ // events per swipe; without accumulation a single gesture flies the wheel.
358
+ useEffect(() => {
359
+ const el = rootRef.current;
360
+ if (!el || disabled)
361
+ return undefined;
362
+ const SCROLL_STEP_PX = 60;
363
+ const QUIET_MS = 250;
364
+ let accumulated = 0;
365
+ let resetTimer = null;
366
+ const resetAccumulator = () => {
367
+ accumulated = 0;
368
+ };
369
+ const onWheel = (e) => {
370
+ e.preventDefault();
371
+ // Reset accumulator if scroll direction reversed mid-gesture.
372
+ if (accumulated !== 0 && Math.sign(accumulated) !== Math.sign(e.deltaY)) {
373
+ accumulated = 0;
374
+ }
375
+ accumulated += e.deltaY;
376
+ while (Math.abs(accumulated) >= SCROLL_STEP_PX) {
377
+ if (accumulated > 0) {
378
+ moveBy(1);
379
+ accumulated -= SCROLL_STEP_PX;
380
+ }
381
+ else {
382
+ moveBy(-1);
383
+ accumulated += SCROLL_STEP_PX;
384
+ }
385
+ }
386
+ if (resetTimer)
387
+ clearTimeout(resetTimer);
388
+ resetTimer = setTimeout(resetAccumulator, QUIET_MS);
389
+ };
390
+ el.addEventListener('wheel', onWheel, { passive: false });
391
+ return () => {
392
+ el.removeEventListener('wheel', onWheel);
393
+ if (resetTimer)
394
+ clearTimeout(resetTimer);
395
+ };
396
+ }, [moveBy, disabled]);
397
+ // Pointer drag.
398
+ // We DELAY capturing the pointer until the user moves more than
399
+ // DRAG_THRESHOLD_PX. Without this, capturing on pointerdown would redirect
400
+ // mouseup → wheel root, which suppresses the click event on the item button
401
+ // the user pressed (so single-clicks on neighbouring items would no-op).
402
+ const DRAG_THRESHOLD_PX = 4;
403
+ const onPointerDown = (e) => {
404
+ if (disabled)
405
+ return;
406
+ cancelRaf();
407
+ if (audioTickEnabledRef.current) {
408
+ // Resume the AudioContext from inside the user gesture so the first
409
+ // tick of this drag can play (iOS Safari requires this; other browsers
410
+ // are forgiving but it's a cheap idempotent call).
411
+ unlockAudioTick();
412
+ }
413
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
414
+ dragStateRef.current = {
415
+ pointerId: e.pointerId,
416
+ startY: e.clientY,
417
+ startPosition: positionRef.current,
418
+ captured: false,
419
+ samples: [{ t: now, y: e.clientY }],
420
+ };
421
+ };
422
+ const onPointerMove = (e) => {
423
+ const s = dragStateRef.current;
424
+ if (s.pointerId !== e.pointerId)
425
+ return;
426
+ const dy = e.clientY - s.startY;
427
+ if (!s.captured) {
428
+ if (Math.abs(dy) < DRAG_THRESHOLD_PX)
429
+ return;
430
+ rootRef.current?.setPointerCapture(e.pointerId);
431
+ s.captured = true;
432
+ setIsDragging(true);
433
+ }
434
+ const now = typeof performance !== 'undefined' ? performance.now() : Date.now();
435
+ s.samples.push({ t: now, y: e.clientY });
436
+ // Keep the velocity sample window short so the post-flick velocity
437
+ // reflects the *recent* finger speed, not the whole-drag average.
438
+ const VELOCITY_WINDOW_MS = 100;
439
+ while (s.samples.length > 2 && now - s.samples[0].t > VELOCITY_WINDOW_MS) {
440
+ s.samples.shift();
441
+ }
442
+ // Continuous position: every itemHeight worth of finger drag advances
443
+ // the wheel by 1 unit. Pulling the finger DOWN (positive dy) shows
444
+ // EARLIER values, which means position decreases.
445
+ commitPosition(s.startPosition - dy / itemHeight);
446
+ };
447
+ const onPointerUp = (e) => {
448
+ const s = dragStateRef.current;
449
+ if (s.pointerId !== e.pointerId)
450
+ return;
451
+ if (s.captured) {
452
+ rootRef.current?.releasePointerCapture(e.pointerId);
453
+ // Drags don't fire click → focus would be left on whichever child
454
+ // received pointerdown. Pull it back to the wheel root so :focus
455
+ // emphasis still reads correctly after a drag.
456
+ rootRef.current?.focus();
457
+ // Compute fling velocity from the recent sample window.
458
+ const samples = s.samples;
459
+ let pxPerMs = 0;
460
+ if (samples.length >= 2) {
461
+ const first = samples[0];
462
+ const last = samples[samples.length - 1];
463
+ const dt = last.t - first.t;
464
+ if (dt > 0)
465
+ pxPerMs = (last.y - first.y) / dt;
466
+ }
467
+ // Threshold below which we just snap to the nearest item; above which
468
+ // we kick off momentum.
469
+ const FLING_THRESHOLD = 0.25; // px/ms ≈ 250 px/sec
470
+ if (Math.abs(pxPerMs) > FLING_THRESHOLD) {
471
+ startMomentum(pxPerMs);
472
+ }
473
+ else {
474
+ animateSnap();
475
+ }
476
+ }
477
+ s.pointerId = null;
478
+ s.captured = false;
479
+ s.samples = [];
480
+ setIsDragging(false);
481
+ };
482
+ const rootClass = prefixedClassNames(classPrefix, 'timeinput-wheel', {
483
+ [`is-${color}`]: !!color,
484
+ [`is-${size}`]: !!size,
485
+ 'is-disabled': disabled,
486
+ 'is-dragging': isDragging,
487
+ });
488
+ const itemClass = (selected) => prefixedClassNames(classPrefix, 'timeinput-wheel-item', {
489
+ 'is-selected': selected,
490
+ [`is-${color}`]: selected && !!color,
491
+ });
492
+ const bandClass = prefixedClassNames(classPrefix, 'timeinput-wheel-band', {
493
+ [`is-${color}`]: !!color,
494
+ });
495
+ // Build the buffered window of items to render around the current position.
496
+ const items = [];
497
+ for (let off = -BUFFER; off <= BUFFER; off++) {
498
+ const vIdx = virtualIdx + off;
499
+ if (!wrap && (vIdx < 0 || vIdx >= N))
500
+ continue;
501
+ const aIdx = toActual(vIdx);
502
+ items.push({ vIdx, aIdx });
503
+ }
504
+ const ariaText = formatAriaText
505
+ ? formatAriaText(values[toActual(virtualIdx)])
506
+ : formatLabel(values[toActual(virtualIdx)]);
507
+ // Center of the wheel viewport, in px from the top.
508
+ const centreTop = containerHeight / 2 - itemHeight / 2;
509
+ return (jsxs("div", { ref: rootRef, role: "spinbutton", "aria-label": ariaLabel, "aria-valuemin": 0, "aria-valuemax": N - 1, "aria-valuenow": toActual(virtualIdx), "aria-valuetext": ariaText, tabIndex: disabled ? -1 : 0, className: rootClass, style: { height: containerHeight }, onKeyDown: onKeyDown, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerUp, children: [jsx("div", { ref: bandRef, className: bandClass, "aria-hidden": "true", style: {
510
+ position: 'absolute',
511
+ left: 0,
512
+ right: 0,
513
+ top: centreTop,
514
+ height: itemHeight,
515
+ pointerEvents: 'none',
516
+ } }), items.map(({ vIdx, aIdx }) => {
517
+ // Continuous offset in items: items at the centre have offset ≈ 0;
518
+ // top of viewport is negative, bottom positive.
519
+ const offCentreItems = vIdx - position;
520
+ const top = centreTop + offCentreItems * itemHeight;
521
+ const selected = vIdx === virtualIdx;
522
+ const itemDisabled = disabled || (disabledFor ? disabledFor(values[aIdx]) : false);
523
+ return (jsx("button", { type: "button", "aria-hidden": "true", tabIndex: -1, className: itemClass(selected), style: {
524
+ position: 'absolute',
525
+ left: 0,
526
+ right: 0,
527
+ height: itemHeight,
528
+ top: `${top}px`,
529
+ }, disabled: itemDisabled,
530
+ // Keep a press from focusing the item, which assistive technology
531
+ // can't see; the click below hands focus to the wheel instead.
532
+ onMouseDown: e => e.preventDefault(), onClick: () => {
533
+ moveBy(vIdx - virtualIdx);
534
+ // Click focuses the <button> child by default; pull focus back
535
+ // to the wheel root so the column emphasis (:focus rules) match.
536
+ rootRef.current?.focus();
537
+ }, children: formatLabel(values[aIdx]) }, vIdx));
538
+ })] }));
539
+ };
540
+ const Wheel = React.forwardRef(WheelInner);
541
+ const TimeWheels = ({ value, onChange, hourFormat = '24', enableSeconds = false, incrementHours = 1, incrementMinutes = 1, incrementSeconds = 1, unselectableTimes, size, color, disabled, className, id, labels, visibleCount = 5, itemHeight = 32, audioTick = false, onCommit, }) => {
542
+ const t = mergeLabels(labels);
543
+ const rootClass = usePrefixedClassNames('timeinput', {
544
+ [`is-${color}`]: !!color,
545
+ [`is-${size}`]: !!size,
546
+ 'is-disabled': disabled,
547
+ });
548
+ const wheelsClass = usePrefixedClassNames('timeinput-wheels');
549
+ const sepClass = usePrefixedClassNames('timeinput-separator');
550
+ // Use the prefixed class through the same helper the wheels themselves do,
551
+ // so the selector matches the actual rendered class regardless of whether
552
+ // the consumer configured `classPrefix` via ConfigProvider. Building the
553
+ // selector with `${classPrefix}timeinput-wheel` is wrong when classPrefix
554
+ // is undefined (defaults to the empty prefix) — it would stringify to
555
+ // "undefinedtimeinput-wheel".
556
+ const wheelSingleClass = usePrefixedClassNames('timeinput-wheel');
557
+ const wheelsContainerRef = useRef(null);
558
+ const isPm = value.hours >= 12;
559
+ const isTimeAllowed = useCallback((h, m, s) => {
560
+ if (!unselectableTimes)
561
+ return true;
562
+ const probe = setTimeOfDay(new Date(), {
563
+ hours: h,
564
+ minutes: m,
565
+ seconds: s,
566
+ milliseconds: 0,
567
+ });
568
+ return !unselectableTimes(probe);
569
+ }, [unselectableTimes]);
570
+ // Per-column "is this value disabled?" predicates. We compute them against
571
+ // the current value's other parts, so e.g. a predicate that only blocks
572
+ // 12:00–12:59 leaves all minutes selectable when the user moves the hour
573
+ // off 12. Returning `undefined` (rather than a function) when there is no
574
+ // unselectableTimes lets the Wheel skip the disabled-attribute branch.
575
+ const isHourDisabled = useCallback((display) => {
576
+ let h24 = display;
577
+ if (hourFormat === '12') {
578
+ const base = display % 12;
579
+ h24 = isPm ? base + 12 : base;
580
+ }
581
+ return !isTimeAllowed(h24, value.minutes, value.seconds ?? 0);
582
+ }, [hourFormat, isPm, isTimeAllowed, value.minutes, value.seconds]);
583
+ const isMinuteDisabled = useCallback((m) => !isTimeAllowed(value.hours, m, value.seconds ?? 0), [isTimeAllowed, value.hours, value.seconds]);
584
+ const isSecondDisabled = useCallback((s) => !isTimeAllowed(value.hours, value.minutes, s), [isTimeAllowed, value.hours, value.minutes]);
585
+ const hourDisabledFor = unselectableTimes ? isHourDisabled : undefined;
586
+ const minuteDisabledFor = unselectableTimes ? isMinuteDisabled : undefined;
587
+ const secondDisabledFor = unselectableTimes ? isSecondDisabled : undefined;
588
+ const commit = useCallback((next) => {
589
+ const merged = {
590
+ hours: next.hours ?? value.hours,
591
+ minutes: next.minutes ?? value.minutes,
592
+ seconds: enableSeconds
593
+ ? (next.seconds ?? value.seconds ?? 0)
594
+ : undefined,
595
+ };
596
+ if (!isTimeAllowed(merged.hours, merged.minutes, merged.seconds ?? 0)) {
597
+ return;
598
+ }
599
+ onChange(merged);
600
+ }, [value, enableSeconds, isTimeAllowed, onChange]);
601
+ // Build the value lists per column.
602
+ const hourValues = useMemo(() => {
603
+ const step = Math.max(1, incrementHours);
604
+ if (hourFormat === '12') {
605
+ const out = [];
606
+ for (let h = 1; h <= 12; h += step)
607
+ out.push(h);
608
+ return out;
609
+ }
610
+ const out = [];
611
+ for (let h = 0; h < 24; h += step)
612
+ out.push(h);
613
+ return out;
614
+ }, [hourFormat, incrementHours]);
615
+ const minuteValues = useMemo(() => {
616
+ const step = Math.max(1, incrementMinutes);
617
+ const out = [];
618
+ for (let m = 0; m < 60; m += step)
619
+ out.push(m);
620
+ return out;
621
+ }, [incrementMinutes]);
622
+ const secondValues = useMemo(() => {
623
+ const step = Math.max(1, incrementSeconds);
624
+ const out = [];
625
+ for (let s = 0; s < 60; s += step)
626
+ out.push(s);
627
+ return out;
628
+ }, [incrementSeconds]);
629
+ // Map current value → wheel index.
630
+ const displayedHours = hourFormat === '12'
631
+ ? value.hours % 12 === 0
632
+ ? 12
633
+ : value.hours % 12
634
+ : value.hours;
635
+ const hourIndex = Math.max(0, hourValues.indexOf(displayedHours));
636
+ const minuteIndex = Math.max(0, minuteValues.indexOf(value.minutes));
637
+ const secondIndex = Math.max(0, secondValues.indexOf(value.seconds ?? 0));
638
+ const ampmIndex = isPm ? 1 : 0;
639
+ // Refs for column focus management.
640
+ const hoursRef = useRef(null);
641
+ const minutesRef = useRef(null);
642
+ const secondsRef = useRef(null);
643
+ const ampmRef = useRef(null);
644
+ // Build the focus order at runtime.
645
+ const columnOrder = [];
646
+ columnOrder.push(hoursRef);
647
+ columnOrder.push(minutesRef);
648
+ if (enableSeconds)
649
+ columnOrder.push(secondsRef);
650
+ if (hourFormat === '12')
651
+ columnOrder.push(ampmRef);
652
+ const focusAt = (i) => columnOrder[i]?.current?.focus();
653
+ const indexOf = (r) => columnOrder.indexOf(r);
654
+ const focusPrevOf = (r) => () => focusAt(indexOf(r) - 1);
655
+ const focusNextOf = (r) => () => focusAt(indexOf(r) + 1);
656
+ // Hour change — translate displayed value back into 24h.
657
+ const onHourIndex = (next) => {
658
+ const display = hourValues[Math.max(0, Math.min(hourValues.length - 1, next))];
659
+ let next24 = display;
660
+ if (hourFormat === '12') {
661
+ const baseHour = display % 12;
662
+ next24 = isPm ? baseHour + 12 : baseHour;
663
+ }
664
+ // Skip past any blocked hour using nextValid.
665
+ const allowed = nextValid(next24, next24 >= value.hours ? 1 : -1, 24, h => isTimeAllowed(h, value.minutes, value.seconds ?? 0));
666
+ commit({ hours: allowed });
667
+ };
668
+ const onMinuteIndex = (next) => {
669
+ const m = minuteValues[Math.max(0, Math.min(minuteValues.length - 1, next))];
670
+ const allowed = nextValid(m, m >= value.minutes ? 1 : -1, 60, mm => isTimeAllowed(value.hours, mm, value.seconds ?? 0));
671
+ commit({ minutes: allowed });
672
+ };
673
+ const onSecondIndex = (next) => {
674
+ const s = secondValues[Math.max(0, Math.min(secondValues.length - 1, next))];
675
+ const allowed = nextValid(s, s >= (value.seconds ?? 0) ? 1 : -1, 60, ss => isTimeAllowed(value.hours, value.minutes, ss));
676
+ commit({ seconds: allowed });
677
+ };
678
+ const onAmpmIndex = (next) => {
679
+ if (next === ampmIndex)
680
+ return;
681
+ commit({ hours: (value.hours + 12) % 24 });
682
+ };
683
+ // Forward drags that originate on the empty space inside the wheels row
684
+ // (left of the leftmost wheel, right of the rightmost wheel, on the
685
+ // separator) to whichever wheel column is nearest to the pointer X — so
686
+ // the picker behaves like the iOS native one, where the whole picker
687
+ // surface is grabbable, not just the column itself.
688
+ const onWheelsContainerPointerDown = (e) => {
689
+ if (disabled)
690
+ return;
691
+ // If the pointerdown landed on a real wheel column, the wheel's own
692
+ // handler takes over — don't double-dispatch.
693
+ if (e.target.closest(`.${wheelSingleClass}`))
694
+ return;
695
+ const container = wheelsContainerRef.current;
696
+ /* istanbul ignore next: the handler is attached to the very element this ref points to, so it can't be null while events fire */
697
+ if (!container)
698
+ return;
699
+ const wheels = container.querySelectorAll(`.${wheelSingleClass}`);
700
+ if (wheels.length === 0)
701
+ return;
702
+ let nearestIdx = 0;
703
+ let nearestDist = Infinity;
704
+ const x = e.clientX;
705
+ wheels.forEach((el, i) => {
706
+ const r = el.getBoundingClientRect();
707
+ const cx = (r.left + r.right) / 2;
708
+ const d = Math.abs(x - cx);
709
+ if (d < nearestDist) {
710
+ nearestDist = d;
711
+ nearestIdx = i;
712
+ }
713
+ });
714
+ columnOrder[nearestIdx]?.current?.startDrag(e.pointerId, e.clientY);
715
+ };
716
+ return (jsx("div", { id: id, className: classNames(rootClass, className), "aria-disabled": disabled, children: jsxs("div", { className: wheelsClass, ref: wheelsContainerRef, onPointerDown: onWheelsContainerPointerDown, children: [jsx(Wheel, { ref: hoursRef, values: hourValues, index: hourIndex, onChange: onHourIndex, formatLabel: pad2, ariaLabel: t.hours, visibleCount: visibleCount, itemHeight: itemHeight, disabled: disabled, size: size, color: color, wrap: true, onCommit: onCommit, onFocusPrev: focusPrevOf(hoursRef), onFocusNext: focusNextOf(hoursRef), disabledFor: hourDisabledFor, audioTick: audioTick }), jsx("div", { className: sepClass, "aria-hidden": "true", children: ":" }), jsx(Wheel, { ref: minutesRef, values: minuteValues, index: minuteIndex, onChange: onMinuteIndex, formatLabel: pad2, ariaLabel: t.minutes, visibleCount: visibleCount, itemHeight: itemHeight, disabled: disabled, size: size, color: color, wrap: true, onCommit: onCommit, onFocusPrev: focusPrevOf(minutesRef), onFocusNext: focusNextOf(minutesRef), disabledFor: minuteDisabledFor, audioTick: audioTick }), enableSeconds && (jsxs(Fragment, { children: [jsx("div", { className: sepClass, "aria-hidden": "true", children: ":" }), jsx(Wheel, { ref: secondsRef, values: secondValues, index: secondIndex, onChange: onSecondIndex, formatLabel: pad2, ariaLabel: t.seconds, visibleCount: visibleCount, itemHeight: itemHeight, disabled: disabled, size: size, color: color, wrap: true, onCommit: onCommit, onFocusPrev: focusPrevOf(secondsRef), onFocusNext: focusNextOf(secondsRef), disabledFor: secondDisabledFor, audioTick: audioTick })] })), hourFormat === '12' && (jsx(Wheel, { ref: ampmRef, values: ['AM', 'PM'], index: ampmIndex, onChange: onAmpmIndex, formatLabel: v => v, ariaLabel: t.ampm, visibleCount: visibleCount, itemHeight: itemHeight, disabled: disabled, size: size, color: color, onCommit: onCommit, onFocusPrev: focusPrevOf(ampmRef), onFocusNext: focusNextOf(ampmRef), audioTick: audioTick }))] }) }));
717
+ };
718
+ TimeWheels.displayName = 'TimeWheels';
719
+
720
+ export { TimeWheels };
721
+ //# sourceMappingURL=TimeWheels.js.map