@astryxdesign/core 0.7.7-canary.c0929c8 → 0.7.7-canary.c19ca12

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 (472) hide show
  1. package/dist/AlertDialog/AlertDialog.d.ts +4 -1
  2. package/dist/AlertDialog/AlertDialog.d.ts.map +1 -1
  3. package/dist/AlertDialog/AlertDialog.js +36 -6
  4. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts +13 -0
  5. package/dist/AppShell/useCloseMobileDrawerOnActivate.d.ts.map +1 -0
  6. package/dist/AppShell/useCloseMobileDrawerOnActivate.js +45 -0
  7. package/dist/BottomSheet/BottomSheet.d.ts.map +1 -1
  8. package/dist/BottomSheet/BottomSheet.js +79 -9
  9. package/dist/BottomSheet/BottomSheetPanel.d.ts.map +1 -1
  10. package/dist/BottomSheet/BottomSheetPanel.js +23 -4
  11. package/dist/BottomSheet/bottomSheetStack.d.ts.map +1 -1
  12. package/dist/BottomSheet/bottomSheetStack.js +5 -4
  13. package/dist/BottomSheet/useSheetGestures.d.ts.map +1 -1
  14. package/dist/BottomSheet/useSheetGestures.js +3 -1
  15. package/dist/Breadcrumbs/BreadcrumbItem.d.ts +2 -1
  16. package/dist/Breadcrumbs/BreadcrumbItem.d.ts.map +1 -1
  17. package/dist/Breadcrumbs/BreadcrumbItem.js +7 -3
  18. package/dist/Button/Button.d.ts.map +1 -1
  19. package/dist/Button/Button.js +47 -3
  20. package/dist/CheckboxInput/CheckboxInput.d.ts.map +1 -1
  21. package/dist/CheckboxInput/CheckboxInput.js +13 -2
  22. package/dist/CheckboxList/CheckboxList.d.ts.map +1 -1
  23. package/dist/CheckboxList/CheckboxList.js +7 -1
  24. package/dist/CheckboxList/CheckboxListContext.d.ts +6 -0
  25. package/dist/CheckboxList/CheckboxListContext.d.ts.map +1 -1
  26. package/dist/CheckboxList/CheckboxListItem.d.ts.map +1 -1
  27. package/dist/CheckboxList/CheckboxListItem.js +10 -2
  28. package/dist/ComplexSelector/ComplexSelector.d.ts +4 -1
  29. package/dist/ComplexSelector/ComplexSelector.d.ts.map +1 -1
  30. package/dist/ComplexSelector/ComplexSelector.js +14 -4
  31. package/dist/ContextMenu/ContextMenu.js +2 -2
  32. package/dist/DateInput/DateInput.d.ts.map +1 -1
  33. package/dist/DateInput/DateInput.js +5 -2
  34. package/dist/DateInput/MonthScroller.d.ts.map +1 -1
  35. package/dist/DateInput/MonthScroller.js +13 -27
  36. package/dist/DateInput/Wheel.d.ts.map +1 -1
  37. package/dist/DateInput/Wheel.js +22 -41
  38. package/dist/DateTimeInput/DateTimeInput.d.ts.map +1 -1
  39. package/dist/DateTimeInput/DateTimeInput.js +5 -2
  40. package/dist/Dialog/Dialog.d.ts.map +1 -1
  41. package/dist/Dialog/Dialog.js +23 -45
  42. package/dist/Drawer/Drawer.d.ts.map +1 -1
  43. package/dist/Drawer/Drawer.js +15 -11
  44. package/dist/Drawer/useDrawerDialogPresence.d.ts.map +1 -1
  45. package/dist/Drawer/useDrawerDialogPresence.js +5 -1
  46. package/dist/Field/Field.js +2 -2
  47. package/dist/Field/PanelSearchInput.d.ts +9 -1
  48. package/dist/Field/PanelSearchInput.d.ts.map +1 -1
  49. package/dist/Field/PanelSearchInput.js +3 -1
  50. package/dist/HoverCard/HoverCard.d.ts +4 -3
  51. package/dist/HoverCard/HoverCard.d.ts.map +1 -1
  52. package/dist/HoverCard/HoverCard.js +27 -2
  53. package/dist/HoverCard/useHoverCard.d.ts +5 -4
  54. package/dist/HoverCard/useHoverCard.d.ts.map +1 -1
  55. package/dist/HoverCard/useHoverCard.js +17 -31
  56. package/dist/Icon/Icon.d.ts +5 -1
  57. package/dist/Icon/Icon.d.ts.map +1 -1
  58. package/dist/Icon/Icon.js +5 -2
  59. package/dist/InfoTip/InfoTip.d.ts +63 -0
  60. package/dist/InfoTip/InfoTip.d.ts.map +1 -0
  61. package/dist/InfoTip/InfoTip.js +126 -0
  62. package/dist/InfoTip/index.d.ts +10 -0
  63. package/dist/InfoTip/index.d.ts.map +1 -0
  64. package/dist/InfoTip/index.js +12 -0
  65. package/dist/Layer/layerAnimations.stylex.d.ts +7 -8
  66. package/dist/Layer/layerAnimations.stylex.d.ts.map +1 -1
  67. package/dist/Layer/layerAnimations.stylex.js +25 -20
  68. package/dist/Layer/useBackdropPress.d.ts +27 -0
  69. package/dist/Layer/useBackdropPress.d.ts.map +1 -0
  70. package/dist/Layer/useBackdropPress.js +53 -0
  71. package/dist/Layer/usePressPin.d.ts +20 -10
  72. package/dist/Layer/usePressPin.d.ts.map +1 -1
  73. package/dist/Layer/usePressPin.js +66 -18
  74. package/dist/Lightbox/Lightbox.d.ts.map +1 -1
  75. package/dist/Lightbox/Lightbox.js +18 -0
  76. package/dist/MobileNav/MobileNav.js +0 -1
  77. package/dist/MultiSelector/hooks.d.ts.map +1 -1
  78. package/dist/MultiSelector/hooks.js +2 -4
  79. package/dist/Outline/Outline.d.ts +2 -4
  80. package/dist/Outline/Outline.d.ts.map +1 -1
  81. package/dist/Outline/Outline.js +4 -52
  82. package/dist/Outline/useOutlineFromMarkdown.d.ts.map +1 -1
  83. package/dist/Outline/useOutlineFromMarkdown.js +1 -0
  84. package/dist/Outline/useScrollSpy.js +3 -3
  85. package/dist/PowerSearch/PowerSearch.d.ts +36 -3
  86. package/dist/PowerSearch/PowerSearch.d.ts.map +1 -1
  87. package/dist/PowerSearch/PowerSearch.js +571 -302
  88. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts +100 -0
  89. package/dist/PowerSearch/PowerSearchBottomSheet.d.ts.map +1 -0
  90. package/dist/PowerSearch/PowerSearchBottomSheet.js +1913 -0
  91. package/dist/PowerSearch/PowerSearchEditPopover.d.ts +32 -1
  92. package/dist/PowerSearch/PowerSearchEditPopover.d.ts.map +1 -1
  93. package/dist/PowerSearch/PowerSearchEditPopover.js +17 -6
  94. package/dist/PowerSearch/PowerSearchValueEditor.d.ts +38 -3
  95. package/dist/PowerSearch/PowerSearchValueEditor.d.ts.map +1 -1
  96. package/dist/PowerSearch/PowerSearchValueEditor.js +170 -61
  97. package/dist/PowerSearch/formatFilterValue.d.ts +7 -0
  98. package/dist/PowerSearch/formatFilterValue.d.ts.map +1 -1
  99. package/dist/PowerSearch/formatFilterValue.js +11 -1
  100. package/dist/PowerSearch/formatTokenValue.d.ts +28 -0
  101. package/dist/PowerSearch/formatTokenValue.d.ts.map +1 -0
  102. package/dist/PowerSearch/formatTokenValue.js +160 -0
  103. package/dist/PowerSearch/powerSearchSheetUtils.d.ts +65 -0
  104. package/dist/PowerSearch/powerSearchSheetUtils.d.ts.map +1 -0
  105. package/dist/PowerSearch/powerSearchSheetUtils.js +168 -0
  106. package/dist/RadioList/RadioList.d.ts.map +1 -1
  107. package/dist/RadioList/RadioList.js +8 -2
  108. package/dist/RadioList/RadioListItem.d.ts +1 -1
  109. package/dist/RadioList/RadioListItem.d.ts.map +1 -1
  110. package/dist/RadioList/RadioListItem.js +5 -3
  111. package/dist/Resizable/ResizeHandle.d.ts.map +1 -1
  112. package/dist/Resizable/ResizeHandle.js +118 -260
  113. package/dist/Resizable/resizeGesture.d.ts +91 -0
  114. package/dist/Resizable/resizeGesture.d.ts.map +1 -0
  115. package/dist/Resizable/resizeGesture.js +251 -0
  116. package/dist/SegmentedControl/SegmentedControl.d.ts +3 -1
  117. package/dist/SegmentedControl/SegmentedControl.d.ts.map +1 -1
  118. package/dist/SegmentedControl/SegmentedControl.js +115 -8
  119. package/dist/SegmentedControl/SegmentedControlContext.d.ts +6 -0
  120. package/dist/SegmentedControl/SegmentedControlContext.d.ts.map +1 -1
  121. package/dist/SegmentedControl/SegmentedControlItem.d.ts +1 -0
  122. package/dist/SegmentedControl/SegmentedControlItem.d.ts.map +1 -1
  123. package/dist/SegmentedControl/SegmentedControlItem.js +8 -1
  124. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts +357 -0
  125. package/dist/SegmentedControl/useSegmentedSelectionMotion.d.ts.map +1 -0
  126. package/dist/SegmentedControl/useSegmentedSelectionMotion.js +1349 -0
  127. package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts +70 -0
  128. package/dist/SegmentedControl/useSegmentedTouchTracking.d.ts.map +1 -0
  129. package/dist/SegmentedControl/useSegmentedTouchTracking.js +264 -0
  130. package/dist/Selector/Selector.d.ts.map +1 -1
  131. package/dist/Selector/Selector.js +96 -67
  132. package/dist/Selector/optionShape.d.ts +24 -0
  133. package/dist/Selector/optionShape.d.ts.map +1 -0
  134. package/dist/Selector/optionShape.js +48 -0
  135. package/dist/Selector/selectableOptionEntries.d.ts +20 -0
  136. package/dist/Selector/selectableOptionEntries.d.ts.map +1 -0
  137. package/dist/Selector/selectableOptionEntries.js +38 -0
  138. package/dist/Selector/utils.d.ts +2 -17
  139. package/dist/Selector/utils.d.ts.map +1 -1
  140. package/dist/Selector/utils.js +3 -51
  141. package/dist/SideNav/SideNavItem.d.ts.map +1 -1
  142. package/dist/SideNav/SideNavItem.js +3 -9
  143. package/dist/Slider/sliderTouchController.js +2 -2
  144. package/dist/Spinner/Spinner.d.ts +1 -1
  145. package/dist/Spinner/Spinner.d.ts.map +1 -1
  146. package/dist/Spinner/Spinner.js +60 -58
  147. package/dist/Switch/switchTouch.d.ts +0 -7
  148. package/dist/Switch/switchTouch.d.ts.map +1 -1
  149. package/dist/Switch/switchTouch.js +2 -10
  150. package/dist/Switch/useSwitchTouch.d.ts +2 -11
  151. package/dist/Switch/useSwitchTouch.d.ts.map +1 -1
  152. package/dist/Switch/useSwitchTouch.js +35 -93
  153. package/dist/Table/BaseTable.d.ts.map +1 -1
  154. package/dist/Table/BaseTable.js +29 -8
  155. package/dist/TextInput/TextInput.d.ts +2 -2
  156. package/dist/TextInput/TextInput.d.ts.map +1 -1
  157. package/dist/TextInput/TextInput.js +10 -7
  158. package/dist/Toast/Toast.d.ts +11 -1
  159. package/dist/Toast/Toast.d.ts.map +1 -1
  160. package/dist/Toast/Toast.js +30 -15
  161. package/dist/Toast/ToastViewport.d.ts.map +1 -1
  162. package/dist/Toast/ToastViewport.js +94 -34
  163. package/dist/Toast/types.d.ts +9 -3
  164. package/dist/Toast/types.d.ts.map +1 -1
  165. package/dist/Token/Token.d.ts +2 -2
  166. package/dist/Token/Token.d.ts.map +1 -1
  167. package/dist/Token/Token.js +13 -4
  168. package/dist/Token/TokenNamingContext.d.ts +14 -0
  169. package/dist/Token/TokenNamingContext.d.ts.map +1 -0
  170. package/dist/Token/TokenNamingContext.js +23 -0
  171. package/dist/Tokenizer/Tokenizer.d.ts +46 -59
  172. package/dist/Tokenizer/Tokenizer.d.ts.map +1 -1
  173. package/dist/Tokenizer/Tokenizer.js +40 -57
  174. package/dist/Tokenizer/TokenizerBottomSheet.d.ts +25 -28
  175. package/dist/Tokenizer/TokenizerBottomSheet.d.ts.map +1 -1
  176. package/dist/Tokenizer/TokenizerBottomSheet.js +157 -54
  177. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts +3 -0
  178. package/dist/Tokenizer/TokenizerItemRemovableContext.d.ts.map +1 -0
  179. package/dist/Tokenizer/TokenizerItemRemovableContext.js +19 -0
  180. package/dist/Tooltip/Tooltip.d.ts +25 -30
  181. package/dist/Tooltip/Tooltip.d.ts.map +1 -1
  182. package/dist/Tooltip/Tooltip.js +24 -5
  183. package/dist/Tooltip/index.d.ts +1 -1
  184. package/dist/Tooltip/index.d.ts.map +1 -1
  185. package/dist/Tooltip/useTooltip.d.ts +45 -28
  186. package/dist/Tooltip/useTooltip.d.ts.map +1 -1
  187. package/dist/Tooltip/useTooltip.js +52 -59
  188. package/dist/TopNav/TopNavItem.d.ts.map +1 -1
  189. package/dist/TopNav/TopNavItem.js +4 -7
  190. package/dist/TopNav/TopNavMegaMenuItem.d.ts.map +1 -1
  191. package/dist/TopNav/TopNavMegaMenuItem.js +4 -7
  192. package/dist/TopNav/TopNavMenu.d.ts.map +1 -1
  193. package/dist/TopNav/TopNavMenu.js +4 -7
  194. package/dist/Typeahead/TypeaheadItem.d.ts +2 -1
  195. package/dist/Typeahead/TypeaheadItem.d.ts.map +1 -1
  196. package/dist/animator/createAnimator.d.ts +98 -0
  197. package/dist/animator/createAnimator.d.ts.map +1 -0
  198. package/dist/animator/createAnimator.js +259 -0
  199. package/dist/astryx.css +96 -7
  200. package/dist/gestures/GestureHost.d.ts +27 -1
  201. package/dist/gestures/GestureHost.d.ts.map +1 -1
  202. package/dist/gestures/GestureHost.js +157 -21
  203. package/dist/gestures/PressTracker.d.ts.map +1 -1
  204. package/dist/gestures/PressTracker.js +4 -3
  205. package/dist/hooks/actionGroupTracker.d.ts +7 -4
  206. package/dist/hooks/actionGroupTracker.d.ts.map +1 -1
  207. package/dist/hooks/actionGroupTracker.js +19 -56
  208. package/dist/hooks/index.d.ts +1 -0
  209. package/dist/hooks/index.d.ts.map +1 -1
  210. package/dist/hooks/index.js +1 -0
  211. package/dist/hooks/pressGroupContext.d.ts +6 -0
  212. package/dist/hooks/pressGroupContext.d.ts.map +1 -0
  213. package/dist/hooks/pressGroupContext.js +20 -0
  214. package/dist/hooks/useActionGroupTracking.d.ts +53 -0
  215. package/dist/hooks/useActionGroupTracking.d.ts.map +1 -0
  216. package/dist/hooks/useActionGroupTracking.js +174 -0
  217. package/dist/hooks/useAnimator.d.ts +41 -0
  218. package/dist/hooks/useAnimator.d.ts.map +1 -0
  219. package/dist/hooks/useAnimator.js +70 -0
  220. package/dist/hooks/useAttachment.d.ts +1 -1
  221. package/dist/hooks/useAttachment.js +1 -1
  222. package/dist/hooks/useHighlightedOptionScroll.d.ts +11 -1
  223. package/dist/hooks/useHighlightedOptionScroll.d.ts.map +1 -1
  224. package/dist/hooks/useHighlightedOptionScroll.js +112 -11
  225. package/dist/hooks/useInputStatusIcon.d.ts +1 -1
  226. package/dist/hooks/useInputStatusIcon.d.ts.map +1 -1
  227. package/dist/hooks/useInputStatusIcon.js +2 -2
  228. package/dist/hooks/useLongPress.d.ts +6 -2
  229. package/dist/hooks/useLongPress.d.ts.map +1 -1
  230. package/dist/hooks/useLongPress.js +95 -15
  231. package/dist/hooks/useMediaQuery.d.ts +2 -1
  232. package/dist/hooks/useMediaQuery.d.ts.map +1 -1
  233. package/dist/hooks/useMediaQuery.js +7 -4
  234. package/dist/hooks/useMenuPress.d.ts.map +1 -1
  235. package/dist/hooks/useMenuPress.js +4 -2
  236. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts +8 -0
  237. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.d.ts.map +1 -0
  238. package/dist/i18n/generated-english/InfoTip/InfoTip.generated.js +12 -0
  239. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.d.ts.map +1 -1
  240. package/dist/i18n/generated-english/PowerSearch/PowerSearch.generated.js +1 -0
  241. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts +8 -0
  242. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.d.ts.map +1 -0
  243. package/dist/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.js +42 -0
  244. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.d.ts.map +1 -1
  245. package/dist/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.js +6 -0
  246. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts +8 -0
  247. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.d.ts.map +1 -0
  248. package/dist/i18n/generated-english/PowerSearch/formatTokenValue.generated.js +14 -0
  249. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.d.ts.map +1 -1
  250. package/dist/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.js +7 -1
  251. package/dist/i18n/generated-locales/en.generated.d.ts.map +1 -1
  252. package/dist/i18n/generated-locales/en.generated.js +33 -1
  253. package/dist/i18n/generated-locales/pseudo.generated.d.ts.map +1 -1
  254. package/dist/i18n/generated-locales/pseudo.generated.js +33 -1
  255. package/dist/index.d.ts +2 -0
  256. package/dist/index.d.ts.map +1 -1
  257. package/dist/index.js +5 -0
  258. package/dist/internal/SheetConfirmation.d.ts +11 -0
  259. package/dist/internal/SheetConfirmation.d.ts.map +1 -0
  260. package/dist/internal/SheetConfirmation.js +92 -0
  261. package/dist/internal/indicatorPress.stylex.d.ts +20 -0
  262. package/dist/internal/indicatorPress.stylex.d.ts.map +1 -0
  263. package/dist/internal/indicatorPress.stylex.js +26 -0
  264. package/dist/internal/sheetDraft.d.ts +35 -0
  265. package/dist/internal/sheetDraft.d.ts.map +1 -0
  266. package/dist/internal/sheetDraft.js +65 -0
  267. package/dist/internal/touch-sheet.d.ts +21 -0
  268. package/dist/internal/touch-sheet.d.ts.map +1 -0
  269. package/dist/internal/touch-sheet.js +19 -0
  270. package/dist/internal/useDismissSearchOnTouchDrag.d.ts +24 -0
  271. package/dist/internal/useDismissSearchOnTouchDrag.d.ts.map +1 -0
  272. package/dist/internal/useDismissSearchOnTouchDrag.js +56 -0
  273. package/dist/motion/animator.d.ts +14 -14
  274. package/dist/motion/animator.d.ts.map +1 -1
  275. package/dist/motion/animator.js +41 -18
  276. package/dist/motion/reducedMotion.d.ts +24 -0
  277. package/dist/motion/reducedMotion.d.ts.map +1 -1
  278. package/dist/motion/reducedMotion.js +78 -1
  279. package/dist/motion/roles.d.ts +7 -1
  280. package/dist/motion/roles.d.ts.map +1 -1
  281. package/dist/motion/roles.js +20 -1
  282. package/dist/motion/uikit/alert.d.ts +41 -0
  283. package/dist/motion/uikit/alert.d.ts.map +1 -0
  284. package/dist/motion/uikit/alert.js +43 -0
  285. package/dist/motion/uikit/index.d.ts +6 -5
  286. package/dist/motion/uikit/index.d.ts.map +1 -1
  287. package/dist/motion/uikit/index.js +6 -5
  288. package/dist/motion/uikit/segmented.d.ts +55 -0
  289. package/dist/motion/uikit/segmented.d.ts.map +1 -0
  290. package/dist/motion/uikit/segmented.js +77 -0
  291. package/dist/motion/uikit/springs.d.ts +0 -18
  292. package/dist/motion/uikit/springs.d.ts.map +1 -1
  293. package/dist/motion/uikit/springs.js +5 -21
  294. package/dist/privateAttributes/pressGroupItem.stylex.d.ts +15 -0
  295. package/dist/privateAttributes/pressGroupItem.stylex.d.ts.map +1 -0
  296. package/dist/privateAttributes/pressGroupItem.stylex.js +23 -0
  297. package/dist/privateAttributes/segmentTitle.stylex.d.ts +5 -0
  298. package/dist/privateAttributes/segmentTitle.stylex.d.ts.map +1 -0
  299. package/dist/privateAttributes/segmentTitle.stylex.js +22 -0
  300. package/dist/utils/browserClickWait.d.ts +35 -0
  301. package/dist/utils/browserClickWait.d.ts.map +1 -0
  302. package/dist/utils/browserClickWait.js +123 -0
  303. package/dist/utils/clickGuard.d.ts +14 -1
  304. package/dist/utils/clickGuard.d.ts.map +1 -1
  305. package/dist/utils/clickGuard.js +46 -1
  306. package/dist/utils/dateParser.d.ts.map +1 -1
  307. package/dist/utils/dateParser.js +12 -0
  308. package/dist/utils/focusReturn.d.ts +15 -0
  309. package/dist/utils/focusReturn.d.ts.map +1 -1
  310. package/dist/utils/focusReturn.js +28 -0
  311. package/dist/utils/interactionOverlay.stylex.d.ts +3 -2
  312. package/dist/utils/interactionOverlay.stylex.d.ts.map +1 -1
  313. package/dist/utils/interactionOverlay.stylex.js +3 -2
  314. package/dist/utils/pressFeedback.d.ts.map +1 -1
  315. package/dist/utils/pressFeedback.js +19 -6
  316. package/dist/utils/triggerFocus.d.ts +36 -0
  317. package/dist/utils/triggerFocus.d.ts.map +1 -0
  318. package/dist/utils/triggerFocus.js +65 -0
  319. package/locales/en.json +128 -0
  320. package/locales/pseudo.json +96 -0
  321. package/package.json +18 -8
  322. package/src/AlertDialog/AlertDialog.doc.mjs +2 -2
  323. package/src/AlertDialog/AlertDialog.tsx +37 -5
  324. package/src/AppShell/useCloseMobileDrawerOnActivate.ts +52 -0
  325. package/src/BottomSheet/BottomSheet.tsx +95 -10
  326. package/src/BottomSheet/BottomSheetPanel.tsx +40 -5
  327. package/src/BottomSheet/bottomSheetStack.ts +8 -4
  328. package/src/BottomSheet/useSheetGestures.ts +3 -1
  329. package/src/Breadcrumbs/BreadcrumbItem.tsx +13 -3
  330. package/src/Button/Button.tsx +63 -5
  331. package/src/CheckboxInput/CheckboxInput.doc.mjs +18 -0
  332. package/src/CheckboxInput/CheckboxInput.tsx +36 -9
  333. package/src/CheckboxList/CheckboxList.doc.mjs +4 -2
  334. package/src/CheckboxList/CheckboxList.tsx +6 -0
  335. package/src/CheckboxList/CheckboxListContext.tsx +6 -0
  336. package/src/CheckboxList/CheckboxListItem.tsx +11 -2
  337. package/src/ComplexSelector/ComplexSelector.tsx +20 -5
  338. package/src/ContextMenu/ContextMenu.tsx +2 -2
  339. package/src/DateInput/DateInput.tsx +8 -2
  340. package/src/DateInput/MonthScroller.tsx +18 -22
  341. package/src/DateInput/Wheel.tsx +30 -32
  342. package/src/DateTimeInput/DateTimeInput.tsx +8 -2
  343. package/src/Dialog/Dialog.tsx +54 -45
  344. package/src/Drawer/Drawer.tsx +28 -11
  345. package/src/Drawer/useDrawerDialogPresence.ts +5 -1
  346. package/src/Field/Field.tsx +2 -2
  347. package/src/Field/PanelSearchInput.tsx +11 -0
  348. package/src/HoverCard/HoverCard.doc.mjs +4 -2
  349. package/src/HoverCard/HoverCard.tsx +35 -4
  350. package/src/HoverCard/useHoverCard.doc.mjs +178 -36
  351. package/src/HoverCard/useHoverCard.tsx +46 -52
  352. package/src/Icon/Icon.doc.mjs +49 -8
  353. package/src/Icon/Icon.tsx +15 -7
  354. package/src/InfoTip/InfoTip.doc.mjs +192 -0
  355. package/src/InfoTip/InfoTip.tsx +186 -0
  356. package/src/InfoTip/index.ts +14 -0
  357. package/src/Layer/layerAnimations.stylex.ts +24 -16
  358. package/src/Layer/useBackdropPress.ts +81 -0
  359. package/src/Layer/usePressPin.ts +86 -26
  360. package/src/Lightbox/Lightbox.tsx +18 -0
  361. package/src/MobileNav/MobileNav.tsx +1 -1
  362. package/src/MultiSelector/hooks.ts +2 -4
  363. package/src/Outline/Outline.doc.mjs +177 -51
  364. package/src/Outline/Outline.tsx +5 -57
  365. package/src/Outline/useOutlineFromMarkdown.doc.mjs +157 -0
  366. package/src/Outline/useOutlineFromMarkdown.ts +1 -0
  367. package/src/Outline/useScrollSpy.ts +3 -3
  368. package/src/PowerSearch/PowerSearch.doc.mjs +55 -6
  369. package/src/PowerSearch/PowerSearch.tsx +798 -324
  370. package/src/PowerSearch/PowerSearchBottomSheet.tsx +2515 -0
  371. package/src/PowerSearch/PowerSearchEditPopover.tsx +28 -6
  372. package/src/PowerSearch/PowerSearchValueEditor.tsx +203 -38
  373. package/src/PowerSearch/formatFilterValue.ts +18 -1
  374. package/src/PowerSearch/formatTokenValue.ts +215 -0
  375. package/src/PowerSearch/powerSearchSheetUtils.ts +224 -0
  376. package/src/RadioList/RadioList.doc.mjs +4 -2
  377. package/src/RadioList/RadioList.tsx +7 -2
  378. package/src/RadioList/RadioListItem.tsx +5 -3
  379. package/src/Resizable/ResizeHandle.tsx +133 -328
  380. package/src/Resizable/resizeGesture.ts +368 -0
  381. package/src/SegmentedControl/SegmentedControl.doc.mjs +3 -3
  382. package/src/SegmentedControl/SegmentedControl.tsx +179 -5
  383. package/src/SegmentedControl/SegmentedControlContext.ts +6 -0
  384. package/src/SegmentedControl/SegmentedControlItem.tsx +12 -1
  385. package/src/SegmentedControl/useSegmentedSelectionMotion.ts +1640 -0
  386. package/src/SegmentedControl/useSegmentedTouchTracking.ts +308 -0
  387. package/src/Selector/Selector.tsx +136 -89
  388. package/src/Selector/optionShape.ts +64 -0
  389. package/src/Selector/selectableOptionEntries.ts +39 -0
  390. package/src/Selector/utils.ts +9 -68
  391. package/src/SideNav/SideNavItem.tsx +3 -7
  392. package/src/Slider/sliderTouchController.ts +2 -2
  393. package/src/Spinner/Spinner.tsx +77 -63
  394. package/src/Switch/switchTouch.ts +2 -10
  395. package/src/Switch/useSwitchTouch.ts +38 -104
  396. package/src/Table/BaseTable.tsx +46 -12
  397. package/src/TextInput/TextInput.tsx +11 -10
  398. package/src/Toast/Toast.doc.mjs +4 -3
  399. package/src/Toast/Toast.tsx +57 -32
  400. package/src/Toast/ToastViewport.tsx +121 -35
  401. package/src/Toast/types.ts +9 -3
  402. package/src/Token/Token.tsx +15 -4
  403. package/src/Token/TokenNamingContext.ts +28 -0
  404. package/src/Tokenizer/Tokenizer.doc.mjs +16 -0
  405. package/src/Tokenizer/Tokenizer.tsx +89 -85
  406. package/src/Tokenizer/TokenizerBottomSheet.tsx +219 -93
  407. package/src/Tokenizer/TokenizerItemRemovableContext.ts +24 -0
  408. package/src/Tooltip/Tooltip.doc.mjs +123 -54
  409. package/src/Tooltip/Tooltip.tsx +36 -39
  410. package/src/Tooltip/index.ts +0 -1
  411. package/src/Tooltip/useTooltip.doc.mjs +171 -33
  412. package/src/Tooltip/useTooltip.tsx +98 -86
  413. package/src/TopNav/TopNavItem.tsx +6 -5
  414. package/src/TopNav/TopNavMegaMenuItem.tsx +6 -5
  415. package/src/TopNav/TopNavMenu.tsx +6 -5
  416. package/src/Typeahead/TypeaheadItem.doc.mjs +6 -3
  417. package/src/Typeahead/TypeaheadItem.tsx +2 -1
  418. package/src/animator/createAnimator.doc.mjs +153 -0
  419. package/src/animator/createAnimator.ts +324 -0
  420. package/src/gestures/GestureHost.ts +198 -20
  421. package/src/gestures/PressTracker.ts +4 -3
  422. package/src/gestures/README.md +7 -1
  423. package/src/hooks/actionGroupTracker.ts +32 -61
  424. package/src/hooks/index.ts +2 -0
  425. package/src/hooks/pressGroupContext.ts +21 -0
  426. package/src/hooks/useActionGroupTracking.ts +224 -0
  427. package/src/hooks/useAnimator.doc.mjs +109 -0
  428. package/src/hooks/useAnimator.ts +67 -0
  429. package/src/hooks/useAttachment.ts +1 -1
  430. package/src/hooks/useHighlightedOptionScroll.ts +123 -15
  431. package/src/hooks/useInputStatusIcon.tsx +3 -2
  432. package/src/hooks/useLongPress.doc.mjs +2 -2
  433. package/src/hooks/useLongPress.ts +136 -15
  434. package/src/hooks/useMediaQuery.doc.mjs +2 -2
  435. package/src/hooks/useMediaQuery.ts +13 -4
  436. package/src/hooks/useMenuPress.ts +8 -2
  437. package/src/i18n/generated-english/InfoTip/InfoTip.generated.ts +13 -0
  438. package/src/i18n/generated-english/PowerSearch/PowerSearch.generated.ts +1 -0
  439. package/src/i18n/generated-english/PowerSearch/PowerSearchBottomSheet.generated.ts +43 -0
  440. package/src/i18n/generated-english/PowerSearch/PowerSearchValueEditor.generated.ts +6 -0
  441. package/src/i18n/generated-english/PowerSearch/formatTokenValue.generated.ts +15 -0
  442. package/src/i18n/generated-english/Tokenizer/TokenizerBottomSheet.generated.ts +7 -1
  443. package/src/i18n/generated-locales/en.generated.ts +43 -0
  444. package/src/i18n/generated-locales/pseudo.generated.ts +44 -0
  445. package/src/index.ts +14 -0
  446. package/src/internal/SheetConfirmation.tsx +101 -0
  447. package/src/internal/indicatorPress.stylex.ts +24 -0
  448. package/src/internal/sheetDraft.ts +80 -0
  449. package/src/internal/touch-sheet.ts +21 -0
  450. package/src/internal/useDismissSearchOnTouchDrag.ts +69 -0
  451. package/src/motion/MEASUREMENTS.md +7 -0
  452. package/src/motion/README.md +9 -5
  453. package/src/motion/animator.ts +61 -29
  454. package/src/motion/reducedMotion.ts +79 -1
  455. package/src/motion/roles.ts +20 -1
  456. package/src/motion/uikit/alert.ts +43 -0
  457. package/src/motion/uikit/index.ts +5 -5
  458. package/src/motion/uikit/segmented.ts +74 -0
  459. package/src/motion/uikit/springs.ts +5 -21
  460. package/src/privateAttributes/pressGroupItem.stylex.ts +23 -0
  461. package/src/privateAttributes/segmentTitle.stylex.ts +22 -0
  462. package/src/utils/browserClickWait.ts +133 -0
  463. package/src/utils/clickGuard.ts +64 -1
  464. package/src/utils/dateParser.ts +16 -0
  465. package/src/utils/focusReturn.ts +30 -0
  466. package/src/utils/interactionOverlay.stylex.ts +3 -2
  467. package/src/utils/pressFeedback.ts +19 -6
  468. package/src/utils/triggerFocus.ts +80 -0
  469. package/dist/internal/tokenizer-presentation.d.ts +0 -21
  470. package/dist/internal/tokenizer-presentation.d.ts.map +0 -1
  471. package/dist/internal/tokenizer-presentation.js +0 -21
  472. package/src/internal/tokenizer-presentation.ts +0 -25
@@ -0,0 +1,1640 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ 'use client';
4
+
5
+ /**
6
+ * @file useSegmentedSelectionMotion.ts
7
+ * @input The radiogroup, the selected value, the presentation and tracking
8
+ * flags; the animator, the UIKit table and the touch tracking
9
+ * @output Exports `useSegmentedSelectionMotion`, `SegmentedSelectionMotion`,
10
+ * the layer custom properties and the segment and title selectors
11
+ * @position SegmentedControl's touch layer
12
+ *
13
+ * SYNC: When modified, update:
14
+ * - /packages/core/src/SegmentedControl/SegmentedControl.tsx (the pseudo-elements)
15
+ * - /packages/core/src/SegmentedControl/SegmentedControlItem.tsx (the title marker)
16
+ * - /packages/core/src/SegmentedControl/useSegmentedTouchTracking.ts
17
+ */
18
+
19
+ import {
20
+ useCallback,
21
+ useEffect,
22
+ useRef,
23
+ useState,
24
+ type RefCallback,
25
+ type RefObject,
26
+ } from 'react';
27
+ import {
28
+ createMotionAnimator,
29
+ type AnimateOptions,
30
+ type Animator,
31
+ type MotionCurve,
32
+ type MotionHandle,
33
+ type MotionValues,
34
+ type SpringTransition,
35
+ } from '../motion/animator';
36
+ import {
37
+ registerSegmentedTable,
38
+ segmentedControl,
39
+ } from '../motion/uikit/segmented';
40
+ import {registerSpringsTable, springs} from '../motion/uikit/springs';
41
+ import {useIsomorphicLayoutEffect} from '../hooks/useIsomorphicLayoutEffect';
42
+ import {observeResize} from '../utils/sharedResizeObserver';
43
+ import {titleOfSegment} from '../privateAttributes/segmentTitle.stylex';
44
+ import {cubicBezier} from '../motion/bezier';
45
+ import {parseCssEasing, parseMotionRoleValue} from '../motion/roles';
46
+ import {useSegmentedTouchTracking} from './useSegmentedTouchTracking';
47
+
48
+ /** The segments, in DOM order. */
49
+ export const SEGMENT_SELECTOR = '[role="radio"][data-value]';
50
+ /**
51
+ * The selected face moves to another segment only when the lens's center is
52
+ * this far past the boundary. The distance is a quarter of that segment's
53
+ * width, at least 8 px.
54
+ */
55
+ const FACE_HYSTERESIS_RATIO = 0.25;
56
+ const FACE_HYSTERESIS_MIN_PX = 8;
57
+
58
+ /** The selected surface's paint, copied into a layer's custom properties. */
59
+ export const SURFACE_PROPERTIES: ReadonlyArray<string> = [
60
+ 'background-color',
61
+ 'background-image',
62
+ 'background-position',
63
+ 'background-size',
64
+ 'background-repeat',
65
+ 'background-origin',
66
+ 'background-clip',
67
+ 'box-shadow',
68
+ 'border-top-left-radius',
69
+ 'border-top-right-radius',
70
+ 'border-bottom-right-radius',
71
+ 'border-bottom-left-radius',
72
+ 'border-top-width',
73
+ 'border-right-width',
74
+ 'border-bottom-width',
75
+ 'border-left-width',
76
+ 'border-top-style',
77
+ 'border-right-style',
78
+ 'border-bottom-style',
79
+ 'border-left-style',
80
+ 'border-top-color',
81
+ 'border-right-color',
82
+ 'border-bottom-color',
83
+ 'border-left-color',
84
+ 'backdrop-filter',
85
+ '-webkit-backdrop-filter',
86
+ ];
87
+
88
+ /**
89
+ * The radiogroup's two pseudo-element layers: the lens (::before) and the
90
+ * ::after, which under Reduced Motion shows the surface that fades out where
91
+ * it is.
92
+ */
93
+ export type Layer = 'lens' | 'after';
94
+
95
+ /** Box and visibility of a layer, besides its paint. */
96
+ export const LAYER_GEOMETRY: ReadonlyArray<string> = [
97
+ 'left',
98
+ 'top',
99
+ 'width',
100
+ 'height',
101
+ 'visibility',
102
+ ];
103
+
104
+ /** The custom property on the radiogroup that sets `property` of `layer`. */
105
+ export function layerVariable(layer: Layer, property: string): string {
106
+ return `--_segmented-${layer}-${property.replace(/^-+/, '')}`;
107
+ }
108
+
109
+ /**
110
+ * What the destination segment shows while the lens carries its surface.
111
+ * Written inline with priority so a theme's rule for the selected segment
112
+ * cannot paint a second surface; borders keep their width so nothing moves.
113
+ */
114
+ const HIDDEN_SURFACE: ReadonlyArray<readonly [string, string]> = [
115
+ ['background-color', 'transparent'],
116
+ ['background-image', 'none'],
117
+ ['box-shadow', 'none'],
118
+ ['border-top-color', 'transparent'],
119
+ ['border-right-color', 'transparent'],
120
+ ['border-bottom-color', 'transparent'],
121
+ ['border-left-color', 'transparent'],
122
+ ['backdrop-filter', 'none'],
123
+ ['-webkit-backdrop-filter', 'none'],
124
+ ];
125
+
126
+ /** A title's face: what a dragging finger moves between titles. */
127
+ interface TitleFace {
128
+ readonly color: string;
129
+ readonly fontWeight: string;
130
+ }
131
+
132
+ /** A box in the track's padding-box coordinates, in CSS px. */
133
+ interface Box {
134
+ readonly x: number;
135
+ readonly y: number;
136
+ readonly width: number;
137
+ readonly height: number;
138
+ }
139
+
140
+ interface Carried {
141
+ /** The segment the lens is bound for: it lands there at rest. */
142
+ readonly segment: HTMLElement;
143
+ /**
144
+ * The lens's layout box: the segment's, so it lands at translate 0 from
145
+ * the segment and scale 1, the segment's own size.
146
+ */
147
+ readonly box: Box;
148
+ /**
149
+ * The segment whose surface the lens shows, hidden there meanwhile: the
150
+ * selected one. A touch moves the lens before the new value comes back,
151
+ * so until then this is still the old selection.
152
+ */
153
+ hidden: HTMLElement;
154
+ /** The track's layout size when the box was measured. */
155
+ readonly trackSize: {readonly width: number; readonly height: number};
156
+ }
157
+
158
+ /** A finger's touch, from landing to letting go. */
159
+ interface Touch {
160
+ /** The track's geometry when the finger landed (read once). */
161
+ readonly toTrackX: (clientX: number) => number;
162
+ readonly boxes: ReadonlyMap<HTMLElement, Box>;
163
+ /** The segment the finger landed on, if any (the tracker's begin target). */
164
+ readonly origin: HTMLElement | null;
165
+ /** Whether it landed on the selected segment (a drag carries the lens). */
166
+ readonly carriesLens: boolean;
167
+ /** A finger pressed: under Reduced Motion its release cross-fades. */
168
+ readonly isFinger: boolean;
169
+ /** The selected title's face and an unselected one, read at landing. */
170
+ readonly faces: {
171
+ readonly selected: TitleFace;
172
+ readonly unselected: TitleFace;
173
+ } | null;
174
+ }
175
+
176
+ /**
177
+ * The motion a release starts after its commit's click runs. It aims at the
178
+ * destination's box when the new value shows, because the titles' new weights
179
+ * move the segments. Until the value comes back, it aims at the box measured
180
+ * at landing.
181
+ */
182
+ interface Pending {
183
+ readonly destination: HTMLElement;
184
+ /** The destination's box when the finger landed. */
185
+ readonly box: Box;
186
+ /** The held lens settles there; otherwise it slides from `from`. */
187
+ readonly isHeld: boolean;
188
+ /** The selected segment's box: where a tap's slide starts. */
189
+ readonly from: Box | null;
190
+ /** A finger released it: under Reduced Motion it cross-fades. */
191
+ readonly touchRelease: boolean;
192
+ }
193
+
194
+ function faceOf(element: HTMLElement): TitleFace {
195
+ const style = getComputedStyle(element);
196
+ return {color: style.color, fontWeight: style.fontWeight};
197
+ }
198
+
199
+ /**
200
+ * A hidden segment's own inline declarations (its `style`) for the properties
201
+ * `hideSurface` overrides: value and priority, restored by `showSurface`.
202
+ */
203
+ const ownSurface = new WeakMap<
204
+ HTMLElement,
205
+ ReadonlyArray<readonly [property: string, value: string, priority: string]>
206
+ >();
207
+
208
+ function hideSurface(segment: HTMLElement): void {
209
+ if (!ownSurface.has(segment)) {
210
+ ownSurface.set(
211
+ segment,
212
+ HIDDEN_SURFACE.map(([property]) => [
213
+ property,
214
+ segment.style.getPropertyValue(property),
215
+ segment.style.getPropertyPriority(property),
216
+ ]),
217
+ );
218
+ }
219
+ for (const [property, value] of HIDDEN_SURFACE) {
220
+ segment.style.setProperty(property, value, 'important');
221
+ }
222
+ }
223
+
224
+ /** Gives a hidden segment its surface back, its own inline values included. */
225
+ function showSurface(segment: HTMLElement): void {
226
+ const own = ownSurface.get(segment);
227
+ if (own == null) {
228
+ return;
229
+ }
230
+ ownSurface.delete(segment);
231
+ for (const [property, value, priority] of own) {
232
+ if (value === '') {
233
+ segment.style.removeProperty(property);
234
+ } else {
235
+ segment.style.setProperty(property, value, priority);
236
+ }
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Paints `layer` with `segment`'s computed surface. All values are read
242
+ * before any is written. A write to the track's style makes the next
243
+ * computed-style read recompute. Interleaved reads and writes restyle the
244
+ * control once per property.
245
+ */
246
+ function paintLayer(
247
+ track: HTMLElement,
248
+ layer: Layer,
249
+ segment: HTMLElement,
250
+ ): void {
251
+ const style = getComputedStyle(segment);
252
+ const values = SURFACE_PROPERTIES.map(property =>
253
+ style.getPropertyValue(property),
254
+ );
255
+ SURFACE_PROPERTIES.forEach((property, index) => {
256
+ track.style.setProperty(layerVariable(layer, property), values[index]);
257
+ });
258
+ }
259
+
260
+ /** Copies the lens's paint onto the ::after. */
261
+ function copyLensPaint(track: HTMLElement): void {
262
+ for (const property of SURFACE_PROPERTIES) {
263
+ track.style.setProperty(
264
+ layerVariable('after', property),
265
+ track.style.getPropertyValue(layerVariable('lens', property)),
266
+ );
267
+ }
268
+ }
269
+
270
+ /** Lays `layer` out on `box` and shows it. */
271
+ function placeLayer(track: HTMLElement, layer: Layer, box: Box): void {
272
+ track.style.setProperty(layerVariable(layer, 'left'), `${box.x}px`);
273
+ track.style.setProperty(layerVariable(layer, 'top'), `${box.y}px`);
274
+ track.style.setProperty(layerVariable(layer, 'width'), `${box.width}px`);
275
+ track.style.setProperty(layerVariable(layer, 'height'), `${box.height}px`);
276
+ track.style.setProperty(layerVariable(layer, 'visibility'), 'visible');
277
+ }
278
+
279
+ /** Hides `layer` and drops its custom properties. */
280
+ function clearLayer(track: HTMLElement, layer: Layer): void {
281
+ for (const property of [...LAYER_GEOMETRY, ...SURFACE_PROPERTIES]) {
282
+ track.style.removeProperty(layerVariable(layer, property));
283
+ }
284
+ }
285
+
286
+ /**
287
+ * The scale an ancestor transform applies to the track's client rects. The
288
+ * lens lays out untransformed. `offsetWidth` rounds to whole pixels, so a
289
+ * fractional track width reads as a scale slightly off 1. Only a real
290
+ * transform moves it further than that rounding.
291
+ */
292
+ function scaleOf(track: HTMLElement, rect: DOMRect): number {
293
+ if (track.offsetWidth <= 0 || rect.width <= 0) {
294
+ return 1;
295
+ }
296
+ return Math.abs(rect.width - track.offsetWidth) <= 1
297
+ ? 1
298
+ : rect.width / track.offsetWidth;
299
+ }
300
+
301
+ /** Boxes relative to the track's padding box. */
302
+ function boxesIn(track: HTMLElement): (element: HTMLElement) => Box {
303
+ const trackRect = track.getBoundingClientRect();
304
+ const scale = scaleOf(track, trackRect);
305
+ return element => {
306
+ const rect = element.getBoundingClientRect();
307
+ return {
308
+ x: (rect.left - trackRect.left) / scale - track.clientLeft,
309
+ y: (rect.top - trackRect.top) / scale - track.clientTop,
310
+ width: rect.width / scale,
311
+ height: rect.height / scale,
312
+ };
313
+ };
314
+ }
315
+
316
+ /** Maps a client x to the track's padding-box x, read once per touch. */
317
+ function trackXIn(track: HTMLElement): (clientX: number) => number {
318
+ const rect = track.getBoundingClientRect();
319
+ const scale = scaleOf(track, rect);
320
+ return clientX => (clientX - rect.left) / scale - track.clientLeft;
321
+ }
322
+
323
+ function segmentsOf(track: HTMLElement): HTMLElement[] {
324
+ return Array.from(track.querySelectorAll<HTMLElement>(SEGMENT_SELECTOR));
325
+ }
326
+
327
+ function isLaidOut(box: Box): boolean {
328
+ return box.width > 0 && box.height > 0;
329
+ }
330
+
331
+ function center(box: Box): number {
332
+ return box.x + box.width / 2;
333
+ }
334
+
335
+ /** Whether two boxes are the same to a quarter of a pixel. */
336
+ function isSameBox(a: Box, b: Box): boolean {
337
+ return (
338
+ Math.abs(a.x - b.x) < 0.25 &&
339
+ Math.abs(a.y - b.y) < 0.25 &&
340
+ Math.abs(a.width - b.width) < 0.25 &&
341
+ Math.abs(a.height - b.height) < 0.25
342
+ );
343
+ }
344
+
345
+ function isEnabledSegment(segment: HTMLElement): boolean {
346
+ return (
347
+ !segment.hasAttribute('disabled') &&
348
+ segment.getAttribute('aria-disabled') !== 'true'
349
+ );
350
+ }
351
+
352
+ const titleOf = titleOfSegment;
353
+
354
+ /** The theme's timing: a duration in seconds and its easing. */
355
+ interface ThemeTiming {
356
+ readonly seconds: number;
357
+ readonly ease: (progress: number) => number;
358
+ }
359
+
360
+ /**
361
+ * The timing of `segment`'s own transition, which reads the theme's duration
362
+ * and easing tokens. The values stay readable while a carry turns the
363
+ * transition's property off. Null when the theme gives no duration.
364
+ */
365
+ function themeTimingOf(segment: HTMLElement): ThemeTiming | null {
366
+ const view = segment.ownerDocument.defaultView;
367
+ if (view == null) {
368
+ return null;
369
+ }
370
+ const style = view.getComputedStyle(segment);
371
+ const ms = parseMotionRoleValue(
372
+ 'duration',
373
+ style.transitionDuration.split(',')[0] ?? '',
374
+ );
375
+ const points = parseCssEasing(
376
+ style.transitionTimingFunction.split(/,(?![^(]*\))/)[0] ?? '',
377
+ );
378
+ if (ms == null || ms <= 0 || points == null) {
379
+ return null;
380
+ }
381
+ return {seconds: ms / 1000, ease: cubicBezier(points)};
382
+ }
383
+
384
+ /** A theme that gives the segments no transition: the lens lands at once. */
385
+ const INSTANT: ThemeTiming = {seconds: 0.001, ease: progress => progress};
386
+
387
+ /** A curve from `from` to `to` on `timing`. */
388
+ function timedCurve(
389
+ from: number,
390
+ to: number,
391
+ timing: ThemeTiming,
392
+ ): MotionCurve {
393
+ const {seconds, ease} = timing;
394
+ const at = (t: number) =>
395
+ from + (to - from) * ease(Math.min(1, Math.max(0, t / seconds)));
396
+ const step = Math.min(seconds / 100, 0.001);
397
+ return {
398
+ duration: seconds,
399
+ at,
400
+ velocity: t => {
401
+ const before = Math.max(0, t - step);
402
+ const after = Math.min(seconds, t + step);
403
+ return (at(after) - at(before)) / (after - before);
404
+ },
405
+ };
406
+ }
407
+
408
+ /** What the control reads of its selection motion. */
409
+ export interface SegmentedSelection {
410
+ /**
411
+ * A press is down on the control (any pointer's), or its tap's click has
412
+ * not run yet. The press commits through that click, so a focus move it
413
+ * causes selects nothing.
414
+ */
415
+ readonly isPressing: () => boolean;
416
+ /** A key went down: a focus move it causes is the keyboard's, and selects. */
417
+ readonly keyPressed: () => void;
418
+ }
419
+
420
+ export interface SegmentedSelectionMotionOptions {
421
+ /** The animator's clock (ms, document timeline); `performance.now()` by default. */
422
+ readonly now?: () => number;
423
+ /**
424
+ * The animator for the radiogroup's ::after (the surface that fades out
425
+ * under Reduced Motion); one of its own by default.
426
+ */
427
+ readonly afterAnimator?: Animator;
428
+ }
429
+
430
+ function defaultNow(): number {
431
+ return typeof performance === 'undefined' ? Date.now() : performance.now();
432
+ }
433
+
434
+ /**
435
+ * The selection motion of one SegmentedControl, as UIKit's segmented control
436
+ * moves it. It lives outside React's render cycle. Nothing here sets state,
437
+ * so a slide never re-renders anything.
438
+ *
439
+ * At rest, the selected segment paints its own surface, so themes style it
440
+ * as before. For a slide, the lens takes over. The lens is the radiogroup's
441
+ * ::before, painted from custom properties copied from the segment's
442
+ * computed surface. The segment hides its own surface inline, so a theme
443
+ * rule cannot keep it painted. The lens moves with `translate` and `scale`
444
+ * on the compositor animator. Nothing lays out or paints per frame, and the
445
+ * slide keeps moving while JavaScript is busy. The lens lays out on the
446
+ * destination's box. It lands at translate 0 and scale 1 and hands the
447
+ * surface back pixel for pixel. Its position and its size run on separate
448
+ * springs, as UIKit's do. A change mid-flight starts from the presented
449
+ * geometry and its velocities. The titles change with the selection, at
450
+ * once.
451
+ *
452
+ * Under a finger:
453
+ * - A theme role owns a pressed thumb's lift, so a press on the selected
454
+ * segment lifts nothing here.
455
+ * - A drag from the selected segment carries the lens at its resting size.
456
+ * Its center chases the finger, at most `dragOvershoot` past the end
457
+ * segments.
458
+ * - The selected title's color follows the lens's center. It moves to a
459
+ * neighbor only once the center is a quarter of that segment past the
460
+ * boundary. Every title keeps its weight until the lift.
461
+ * - The lift commits the segment nearest the finger, through that segment's
462
+ * click. A lift outside the control commits nothing, and the lens settles
463
+ * back.
464
+ * - A press on another segment leaves the lens in place until the lift.
465
+ * Meanwhile that segment shows the shared pressed overlay.
466
+ * - A click that the item cancels sends the lens back at once. Otherwise the
467
+ * lens waits for the new value, which a parent can adopt later. It goes
468
+ * back only if the slide settles without the new value.
469
+ * - A cancelled touch puts the lens back.
470
+ *
471
+ * Under a mouse or a pen, the same press commits the same segment, but the
472
+ * theme owns the timing. A click, a key or a new value slides the lens on the
473
+ * theme's duration and easing, as the segments' own transition does. A drag
474
+ * from the selected segment carries the lens on the pointer itself.
475
+ *
476
+ * Under Reduced Motion, a held lens still follows the pointer: that is direct
477
+ * movement, not a transition. Nothing else chases, travels or scales. The lens
478
+ * fades in at the new segment while ::after, painted with the old surface,
479
+ * fades out in place. With a fine pointer under Reduce Motion, a change shows
480
+ * through the theme's transition.
481
+ */
482
+ export class SegmentedSelectionMotion {
483
+ private track: HTMLElement | null = null;
484
+ private carried: Carried | null = null;
485
+ /**
486
+ * Segments whose surface the lens carried until a change turned it
487
+ * elsewhere. They keep their surface hidden until the lens lands. A tap's
488
+ * press paint can still fade on such a segment. That paint would cover the
489
+ * lens, which sits under the segments' backgrounds, while the lens crosses
490
+ * it.
491
+ */
492
+ private readonly abandoned = new Set<HTMLElement>();
493
+ /** The segment showing the selected face under a dragging finger. */
494
+ private facedSegment: HTMLElement | null = null;
495
+ /** The frame request that keeps the face on the lens while it is carried. */
496
+ private lensFrame: number | null = null;
497
+ /** Titles wearing the other face's color during a drag. */
498
+ private readonly faceOverrides = new Set<HTMLElement>();
499
+ /** The motions the hand-back waits for. */
500
+ private readonly running = new Set<MotionHandle>();
501
+ /** A finger holds the lens: a drag from the selected segment carries it. */
502
+ private isHeld = false;
503
+ /**
504
+ * The touch presentation applies (a coarse primary pointer), so every
505
+ * change slides on UIKit's springs. When it is off, a change slides on the
506
+ * theme's duration and easing, as the segments' transition does. A drag from
507
+ * the selected segment carries the lens on the pointer itself.
508
+ */
509
+ private isPresenting = true;
510
+ /**
511
+ * Segments whose own transitions are off while a fine pointer's carry is
512
+ * on screen, with their own inline values to give back.
513
+ */
514
+ private readonly stilled = new Map<HTMLElement, readonly [string, string]>();
515
+ /** The frame request that gives the stilled segments their transitions back. */
516
+ private thawFrame: number | null = null;
517
+ private touch: Touch | null = null;
518
+ /**
519
+ * The value a touch already moved the lens to: the selection change that
520
+ * follows (the controlled value coming back) re-aims the lens.
521
+ */
522
+ private committed: string | null = null;
523
+ /**
524
+ * The commit's click ran without a veto: the lens waits for the value
525
+ * until the slide settles.
526
+ */
527
+ private isAwaitingValue = false;
528
+ /** The motion a release starts once its commit's click has run. */
529
+ private pending: Pending | null = null;
530
+ /** The last release was a finger's (decline and re-aim follow it). */
531
+ private releasedByFinger = true;
532
+ /** The ::after shows the leaving surface (Reduced Motion). */
533
+ private isLeavingShown = false;
534
+ /** Bumped by every leaving fade; a finished fade clears only its own. */
535
+ private leaving = 0;
536
+
537
+ private readonly now: () => number;
538
+ /** The ::after: the surface fading out under Reduced Motion. */
539
+ private readonly after: Animator;
540
+
541
+ constructor(
542
+ private readonly animator: Animator = createMotionAnimator(null, {
543
+ pseudoElement: '::before',
544
+ }),
545
+ options: SegmentedSelectionMotionOptions = {},
546
+ ) {
547
+ // The chase plays the shared `springs.fingerTracking` role, and the slide
548
+ // plays the segmented role. Both register before the first animation,
549
+ // because nothing registers on import. A second call does nothing.
550
+ registerSpringsTable();
551
+ registerSegmentedTable();
552
+ this.now = options.now ?? defaultNow;
553
+ this.after =
554
+ options.afterAnimator ??
555
+ createMotionAnimator(null, {
556
+ pseudoElement: '::after',
557
+ initial: {opacity: 0},
558
+ });
559
+ }
560
+
561
+ /** Attaches the radiogroup whose pseudo-elements are the lens (null detaches). */
562
+ attachTrack(element: HTMLElement | null): void {
563
+ if (element !== this.track) {
564
+ this.settle();
565
+ }
566
+ this.track = element;
567
+ this.animator.attach(element);
568
+ this.after.attach(element);
569
+ }
570
+
571
+ /** True while the lens carries the selected surface. */
572
+ get isSliding(): boolean {
573
+ return this.carried != null;
574
+ }
575
+
576
+ /** A press (any pointer's) is down on the control, from landing to letting go. */
577
+ get isPressed(): boolean {
578
+ return this.touch != null;
579
+ }
580
+
581
+ /**
582
+ * Whether the touch presentation applies. Off, under a fine primary
583
+ * pointer, a change slides on the theme's timing, and a drag from the
584
+ * selected segment still carries the lens. A change of presentation hands
585
+ * anything in flight back.
586
+ */
587
+ present(isPresenting: boolean): void {
588
+ if (this.isPresenting === isPresenting) {
589
+ return;
590
+ }
591
+ this.isPresenting = isPresenting;
592
+ this.settle();
593
+ }
594
+
595
+ /** The segment that commits `value`, if the track has one. */
596
+ segmentFor(track: HTMLElement, value: string): HTMLElement | null {
597
+ return (
598
+ segmentsOf(track).find(segment => segment.dataset.value === value) ?? null
599
+ );
600
+ }
601
+
602
+ /** The selection moved from `from` to `to`; the DOM already shows `to`. */
603
+ select(track: HTMLElement, from: string, to: string): void {
604
+ if (this.track !== track) {
605
+ return;
606
+ }
607
+ const origin = this.segmentFor(track, from);
608
+ const destination = this.segmentFor(track, to);
609
+ const isTouchCommit = this.committed === to;
610
+ // The commit's own styles take over from a drag's borrowed faces.
611
+ this.clearFaces();
612
+ this.committed = null;
613
+ this.isAwaitingValue = false;
614
+ if (isTouchCommit) {
615
+ if (this.pending?.destination === destination && destination != null) {
616
+ // Adopted during the click: the lens moves now, on the box the
617
+ // destination has with the new value shown.
618
+ this.startPending(destination, boxesIn(track)(destination));
619
+ }
620
+ // The new selection now paints the surface the lens carries, and its
621
+ // titles' weights may have moved the segments since the lens set off.
622
+ const carried = this.carried;
623
+ if (carried != null && carried.segment === destination) {
624
+ if (carried.hidden !== destination) {
625
+ showSurface(carried.hidden);
626
+ paintLayer(track, 'lens', destination);
627
+ hideSurface(destination);
628
+ carried.hidden = destination;
629
+ }
630
+ this.reaim(track, destination);
631
+ }
632
+ return;
633
+ }
634
+ if (!this.slidesChanges()) {
635
+ // A fine pointer under Reduce Motion: the theme's transition shows the
636
+ // change.
637
+ this.settle();
638
+ return;
639
+ }
640
+ if (destination == null) {
641
+ // Nothing is selected any more: no surface to carry.
642
+ this.settle();
643
+ return;
644
+ }
645
+ if (this.carried == null && origin == null) {
646
+ // Nothing was selected: there is no surface to slide from.
647
+ return;
648
+ }
649
+ const boxOf = boxesIn(track);
650
+ const to_ = boxOf(destination);
651
+ if (!isLaidOut(to_)) {
652
+ this.settle();
653
+ return;
654
+ }
655
+ const from_ = this.carried == null ? boxOf(origin as HTMLElement) : null;
656
+ if (this.animator.reducedMotion() !== 'off') {
657
+ this.crossFadeTo(destination, to_, from_, destination, false);
658
+ return;
659
+ }
660
+ const at = this.carry(destination, to_, from_, destination);
661
+ // A programmatic or keyboard change: the whole box slides.
662
+ this.settleLens(destination, {at});
663
+ }
664
+
665
+ /**
666
+ * Whether a change of value slides the lens. A fine pointer under Reduce
667
+ * Motion leaves the change to the theme's transition.
668
+ */
669
+ private slidesChanges(): boolean {
670
+ return this.isPresenting || this.animator.reducedMotion() === 'off';
671
+ }
672
+
673
+ /**
674
+ * A press landed on `origin`, the segment the action-group tracker resolved
675
+ * for it (null: none). It reads the track's geometry and title faces once.
676
+ * Under Reduced Motion a finger's release cross-fades.
677
+ */
678
+ begin(track: HTMLElement, origin: HTMLElement | null, isFinger = true): void {
679
+ const segments = segmentsOf(track);
680
+ const boxOf = boxesIn(track);
681
+ const boxes = new Map(segments.map(segment => [segment, boxOf(segment)]));
682
+ const toTrackX = trackXIn(track);
683
+ const selected =
684
+ segments.find(
685
+ segment => segment.getAttribute('aria-checked') === 'true',
686
+ ) ?? null;
687
+ const carriesLens = origin != null && origin === selected;
688
+ const selectedTitle = selected != null ? titleOf(selected) : null;
689
+ // An enabled segment's unselected face: a disabled title has its own.
690
+ const other = segments.find(
691
+ segment => segment !== selected && isEnabledSegment(segment),
692
+ );
693
+ const otherTitle = other != null ? titleOf(other) : null;
694
+ const faces =
695
+ selectedTitle != null && otherTitle != null
696
+ ? {selected: faceOf(selectedTitle), unselected: faceOf(otherTitle)}
697
+ : null;
698
+ this.touch = {
699
+ toTrackX,
700
+ boxes,
701
+ origin,
702
+ carriesLens,
703
+ isFinger,
704
+ faces,
705
+ };
706
+ }
707
+
708
+ /**
709
+ * The finger moved to `clientX`. A drag from the selected segment carries
710
+ * the lens at its resting size. A touch elsewhere leaves the lens alone,
711
+ * and that segment shows the shared pressed overlay.
712
+ */
713
+ drag(clientX: number): void {
714
+ const touch = this.touch;
715
+ if (touch == null || !touch.carriesLens) {
716
+ return;
717
+ }
718
+ this.hold();
719
+ if (!this.isHeld) {
720
+ return;
721
+ }
722
+ this.chase(clientX);
723
+ this.followLens();
724
+ }
725
+
726
+ /**
727
+ * The first move of a drag from the selected segment binds the lens there,
728
+ * at its resting size. Under Reduced Motion too: the lens follows the hand,
729
+ * and only the release cross-fades.
730
+ */
731
+ private hold(): void {
732
+ const touch = this.touch;
733
+ if (touch == null || touch.origin == null || this.isHeld) {
734
+ return;
735
+ }
736
+ const box = touch.boxes.get(touch.origin) as Box;
737
+ if (!isLaidOut(box)) {
738
+ return;
739
+ }
740
+ this.carry(touch.origin, box, null, touch.origin);
741
+ this.isHeld = true;
742
+ }
743
+
744
+ /**
745
+ * The selected face follows the held lens: it goes to the segment under the
746
+ * lens's presented center. Hysteresis around each boundary keeps a finger
747
+ * that rests on a boundary from flipping it. This runs every frame while
748
+ * the lens is held, so the face keeps up as the lens settles under a still
749
+ * finger.
750
+ */
751
+ private followLens(): void {
752
+ const touch = this.touch;
753
+ const carried = this.carried;
754
+ if (
755
+ touch == null ||
756
+ !touch.carriesLens ||
757
+ !this.isHeld ||
758
+ carried == null ||
759
+ touch.origin == null
760
+ ) {
761
+ this.lensFrame = null;
762
+ return;
763
+ }
764
+ const lensCenter = this.presented(carried.box, this.now()).center;
765
+ const spans = [...touch.boxes.entries()]
766
+ .filter(([segment]) => isEnabledSegment(segment))
767
+ .sort((a, b) => a[1].x - b[1].x);
768
+ const current = this.facedSegment ?? touch.origin;
769
+ const at = spans.findIndex(([segment]) => segment === current);
770
+ if (at >= 0) {
771
+ const edge = (i: number) =>
772
+ (spans[i][1].x + spans[i][1].width + spans[i + 1][1].x) / 2;
773
+ let next = at;
774
+ while (next + 1 < spans.length && lensCenter > edge(next)) {
775
+ next += 1;
776
+ }
777
+ while (next > 0 && lensCenter < edge(next - 1)) {
778
+ next -= 1;
779
+ }
780
+ if (next !== at) {
781
+ const boundary = next > at ? edge(at) : edge(at - 1);
782
+ const hysteresis = Math.max(
783
+ FACE_HYSTERESIS_MIN_PX,
784
+ FACE_HYSTERESIS_RATIO * spans[next][1].width,
785
+ );
786
+ if (Math.abs(lensCenter - boundary) >= hysteresis) {
787
+ const segment = spans[next][0];
788
+ this.showFaceAt(segment === touch.origin ? null : segment);
789
+ }
790
+ }
791
+ }
792
+ const view = this.track?.ownerDocument.defaultView;
793
+ if (this.lensFrame == null && view != null) {
794
+ this.lensFrame = view.requestAnimationFrame(() => {
795
+ this.lensFrame = null;
796
+ this.followLens();
797
+ });
798
+ }
799
+ }
800
+
801
+ /** The held lens's center chases the finger at `clientX`. */
802
+ private chase(clientX: number): void {
803
+ const touch = this.touch;
804
+ const carried = this.carried;
805
+ if (touch == null || carried == null) {
806
+ return;
807
+ }
808
+ const centers = [...touch.boxes.values()].map(center);
809
+ const reach = segmentedControl.dragOvershoot;
810
+ const target = Math.min(
811
+ Math.max(touch.toTrackX(clientX), Math.min(...centers) - reach),
812
+ Math.max(...centers) + reach,
813
+ );
814
+ if (!this.isPresenting || this.animator.reducedMotion() !== 'off') {
815
+ // A mouse or a pen holds the lens directly, as a finger does under
816
+ // Reduce Motion. Each move writes the pointer's position.
817
+ this.animator.set({x: target - center(carried.box)});
818
+ return;
819
+ }
820
+ // Start each chase at the call. A chase from rest waits for the browser
821
+ // to start it. Android Chrome sends the next move first, so the lens
822
+ // would stick.
823
+ const at = this.now();
824
+ this.springLens(
825
+ {x: target - center(carried.box)},
826
+ this.springFrom({x: target - center(carried.box)}, 'tracking'),
827
+ {at},
828
+ );
829
+ }
830
+
831
+ /**
832
+ * The finger let go on `target`, the segment the tracker resolved for it
833
+ * (null: none, or a disabled one). Moves the lens there and returns its
834
+ * value when it is not the selected one (the tracker's lift commits it
835
+ * through its click), else null.
836
+ */
837
+ release(target: HTMLElement | null): string | null {
838
+ const touch = this.touch;
839
+ this.touch = null;
840
+ if (touch == null) {
841
+ return null;
842
+ }
843
+ const selected =
844
+ [...touch.boxes.keys()].find(
845
+ segment => segment.getAttribute('aria-checked') === 'true',
846
+ ) ?? null;
847
+ // A disabled segment never commits: the lens settles back on the
848
+ // selection.
849
+ const destination =
850
+ (target != null && isEnabledSegment(target) ? target : null) ?? selected;
851
+ if (destination == null) {
852
+ this.clearFaces();
853
+ if (this.isHeld) {
854
+ this.isHeld = false;
855
+ this.settle();
856
+ }
857
+ return null;
858
+ }
859
+ const box = touch.boxes.get(destination) as Box;
860
+ const value =
861
+ destination !== selected ? (destination.dataset.value ?? null) : null;
862
+ // A drag's borrowed face stays only if the lift commits (the commit
863
+ // takes it over).
864
+ if (value == null) {
865
+ this.clearFaces();
866
+ }
867
+ const isHeld = touch.carriesLens && this.isHeld;
868
+ this.committed = value;
869
+ this.isAwaitingValue = false;
870
+ this.releasedByFinger = touch.isFinger;
871
+ if (value == null) {
872
+ if (isHeld) {
873
+ // The held lens settles back on the selection.
874
+ this.moveLensTo(
875
+ destination,
876
+ box,
877
+ selected ?? destination,
878
+ touch.isFinger,
879
+ );
880
+ }
881
+ return null;
882
+ }
883
+ // The commit follows at once, through the segment's click; the lens
884
+ // moves once it has run, on the box the destination has then. A held
885
+ // lens settles there; a tap (or a drag that started elsewhere) slides
886
+ // there from the selected segment.
887
+ // A fine pointer under Reduce Motion moves only a held lens.
888
+ if (isHeld || (selected != null && this.slidesChanges())) {
889
+ this.pending = {
890
+ destination,
891
+ box,
892
+ isHeld,
893
+ from:
894
+ !isHeld && selected != null
895
+ ? (touch.boxes.get(selected) as Box)
896
+ : null,
897
+ touchRelease: touch.isFinger,
898
+ };
899
+ }
900
+ return value;
901
+ }
902
+
903
+ /**
904
+ * The commit's click for `value` has run. A click that the item's
905
+ * `onClick` cancels sends the lens back at once. Otherwise the lens waits
906
+ * for the value, which a parent can adopt later.
907
+ */
908
+ clicked(track: HTMLElement, value: string, isCancelled: boolean): void {
909
+ if (this.committed !== value) {
910
+ // Adopted during the click, or superseded.
911
+ return;
912
+ }
913
+ if (isCancelled) {
914
+ this.pending = null;
915
+ this.decline(track, value);
916
+ return;
917
+ }
918
+ this.isAwaitingValue = true;
919
+ // React can render the click's change only after the event that
920
+ // dispatched it ends: a click inside a touch handler is batched with it.
921
+ // Wait one turn. If the value is adopted by then, the lens already moves
922
+ // on the new box. Otherwise it aims at the box measured at landing and
923
+ // re-aims when the value comes back.
924
+ queueMicrotask(() => {
925
+ if (this.committed === value && this.pending != null) {
926
+ this.startPending(this.selectedIn(track));
927
+ }
928
+ });
929
+ }
930
+
931
+ /**
932
+ * The value the finger let go on was not adopted. The item's `onClick`
933
+ * cancelled it, or a controlled parent kept its value. The lens goes back
934
+ * to the selected segment. The next change to that value slides as any
935
+ * change does.
936
+ */
937
+ decline(track: HTMLElement, value: string): void {
938
+ if (this.committed !== value) {
939
+ return;
940
+ }
941
+ this.committed = null;
942
+ this.isAwaitingValue = false;
943
+ this.pending = null;
944
+ this.clearFaces();
945
+ const selected =
946
+ segmentsOf(track).find(
947
+ segment => segment.getAttribute('aria-checked') === 'true',
948
+ ) ?? null;
949
+ if (selected == null || this.carried == null) {
950
+ this.settle();
951
+ return;
952
+ }
953
+ const box = boxesIn(track)(selected);
954
+ if (!isLaidOut(box)) {
955
+ this.settle();
956
+ return;
957
+ }
958
+ this.moveLensTo(selected, box, selected, this.releasedByFinger);
959
+ }
960
+
961
+ /** The touch was taken away (the page scrolled): the lens goes back. */
962
+ cancel(): void {
963
+ const touch = this.touch;
964
+ this.touch = null;
965
+ this.clearFaces();
966
+ if (
967
+ touch == null ||
968
+ !touch.carriesLens ||
969
+ touch.origin == null ||
970
+ !this.isHeld
971
+ ) {
972
+ // Nothing was held: nothing to put back.
973
+ return;
974
+ }
975
+ const box = touch.boxes.get(touch.origin) as Box;
976
+ this.moveLensTo(touch.origin, box, touch.origin, touch.isFinger);
977
+ }
978
+
979
+ /** Hands the surface back to its segment now and hides the lens. */
980
+ settle(): void {
981
+ this.endLeaving();
982
+ this.isHeld = false;
983
+ this.pending = null;
984
+ const carried = this.carried;
985
+ if (carried != null) {
986
+ this.carried = null;
987
+ this.running.clear();
988
+ this.animator.stop();
989
+ this.after.stop();
990
+ showSurface(carried.hidden);
991
+ for (const segment of this.abandoned) {
992
+ showSurface(segment);
993
+ }
994
+ this.abandoned.clear();
995
+ if (this.track != null) {
996
+ clearLayer(this.track, 'lens');
997
+ clearLayer(this.track, 'after');
998
+ }
999
+ this.isLeavingShown = false;
1000
+ }
1001
+ // Once the surfaces are back on screen, so nothing fades in.
1002
+ this.thawTransitions();
1003
+ }
1004
+
1005
+ /**
1006
+ * The control resized. The lens aims at boxes measured when its motion
1007
+ * began, so it hands the surface back rather than land somewhere stale.
1008
+ * Two cases keep the motion. In the first, the track keeps its measured
1009
+ * size: a new title weight resized a content-sized control before the box
1010
+ * was measured. In the second, a finger holds the lens.
1011
+ */
1012
+ resized(track: HTMLElement): void {
1013
+ const carried = this.carried;
1014
+ // A finger that holds the lens reflows the titles itself, because the
1015
+ // selected face moves to the title under it. The lens follows the finger,
1016
+ // and the commit measures the destination again.
1017
+ if (carried == null || this.touch != null) {
1018
+ return;
1019
+ }
1020
+ if (
1021
+ Math.abs(track.offsetWidth - carried.trackSize.width) < 0.5 &&
1022
+ Math.abs(track.offsetHeight - carried.trackSize.height) < 0.5
1023
+ ) {
1024
+ return;
1025
+ }
1026
+ this.settle();
1027
+ }
1028
+
1029
+ /**
1030
+ * Ends everything in flight (unmount). The controller stays usable, so an
1031
+ * effect cleanup followed by a setup (StrictMode's extra mount) loses
1032
+ * nothing.
1033
+ */
1034
+ stop(): void {
1035
+ this.touch = null;
1036
+ if (this.lensFrame != null) {
1037
+ this.track?.ownerDocument.defaultView?.cancelAnimationFrame(
1038
+ this.lensFrame,
1039
+ );
1040
+ this.lensFrame = null;
1041
+ }
1042
+ this.committed = null;
1043
+ this.isAwaitingValue = false;
1044
+ this.clearFaces();
1045
+ this.settle();
1046
+ this.cancelThaw();
1047
+ this.restoreTransitions();
1048
+ }
1049
+
1050
+ /**
1051
+ * A fine pointer's carry. The segments' own transitions go off while the
1052
+ * lens is on screen, so the borrowed surface does not fade out and back in.
1053
+ * Under a finger, the touch presentation already turns them off.
1054
+ */
1055
+ private stillTransitions(): void {
1056
+ const track = this.track;
1057
+ if (track == null) {
1058
+ return;
1059
+ }
1060
+ this.cancelThaw();
1061
+ for (const segment of segmentsOf(track)) {
1062
+ if (!this.stilled.has(segment)) {
1063
+ this.stilled.set(segment, [
1064
+ segment.style.getPropertyValue('transition-property'),
1065
+ segment.style.getPropertyPriority('transition-property'),
1066
+ ]);
1067
+ }
1068
+ segment.style.setProperty('transition-property', 'none', 'important');
1069
+ }
1070
+ }
1071
+
1072
+ /**
1073
+ * Gives the stilled segments their transitions back two frames after the
1074
+ * hand-back, once the surfaces it gave back have been on screen.
1075
+ */
1076
+ private thawTransitions(): void {
1077
+ if (this.stilled.size === 0 || this.thawFrame != null) {
1078
+ return;
1079
+ }
1080
+ const view = this.track?.ownerDocument.defaultView;
1081
+ if (view == null) {
1082
+ this.restoreTransitions();
1083
+ return;
1084
+ }
1085
+ this.thawFrame = view.requestAnimationFrame(() => {
1086
+ this.thawFrame = view.requestAnimationFrame(() => {
1087
+ this.thawFrame = null;
1088
+ this.restoreTransitions();
1089
+ });
1090
+ });
1091
+ }
1092
+
1093
+ private cancelThaw(): void {
1094
+ if (this.thawFrame != null) {
1095
+ this.track?.ownerDocument.defaultView?.cancelAnimationFrame(
1096
+ this.thawFrame,
1097
+ );
1098
+ this.thawFrame = null;
1099
+ }
1100
+ }
1101
+
1102
+ private restoreTransitions(): void {
1103
+ for (const [segment, [value, priority]] of this.stilled) {
1104
+ if (value === '') {
1105
+ segment.style.removeProperty('transition-property');
1106
+ } else {
1107
+ segment.style.setProperty('transition-property', value, priority);
1108
+ }
1109
+ }
1110
+ this.stilled.clear();
1111
+ }
1112
+
1113
+ /** The selected segment, as the DOM shows it now. */
1114
+ private selectedIn(track: HTMLElement): HTMLElement | null {
1115
+ return (
1116
+ segmentsOf(track).find(
1117
+ segment => segment.getAttribute('aria-checked') === 'true',
1118
+ ) ?? null
1119
+ );
1120
+ }
1121
+
1122
+ /**
1123
+ * Starts the motion the release left pending: on `box` (the destination's
1124
+ * box now) or the box measured at landing, showing `paint`'s surface (the
1125
+ * selected segment's).
1126
+ */
1127
+ private startPending(paint: HTMLElement | null, box?: Box): void {
1128
+ const pending = this.pending;
1129
+ this.pending = null;
1130
+ if (pending == null) {
1131
+ return;
1132
+ }
1133
+ const to = box ?? pending.box;
1134
+ const surface = this.carried?.hidden ?? paint ?? pending.destination;
1135
+ if (pending.isHeld) {
1136
+ this.moveLensTo(pending.destination, to, surface, pending.touchRelease);
1137
+ } else if (pending.from != null) {
1138
+ this.slideTo(
1139
+ pending.destination,
1140
+ to,
1141
+ pending.from,
1142
+ surface,
1143
+ pending.touchRelease,
1144
+ );
1145
+ }
1146
+ }
1147
+
1148
+ /**
1149
+ * Aims the lens at `destination`'s box as it is now: a value adopted
1150
+ * after its click (mid-flight) moves the segments with its titles' new
1151
+ * weights.
1152
+ */
1153
+ private reaim(track: HTMLElement, destination: HTMLElement): void {
1154
+ const carried = this.carried;
1155
+ if (carried == null) {
1156
+ return;
1157
+ }
1158
+ const box = boxesIn(track)(destination);
1159
+ if (!isLaidOut(box) || isSameBox(box, carried.box)) {
1160
+ return;
1161
+ }
1162
+ this.moveLensTo(destination, box, carried.hidden, this.releasedByFinger);
1163
+ }
1164
+
1165
+ /**
1166
+ * Binds the lens for `segment`, with `box` as its layout box and `paint`'s
1167
+ * surface, the selected segment's. It keeps what is on screen. With nothing
1168
+ * carried, it starts from `from`, a segment's resting box. Otherwise it
1169
+ * starts from the presented geometry, re-based onto the new box with its
1170
+ * velocities.
1171
+ */
1172
+ private carry(
1173
+ segment: HTMLElement,
1174
+ box: Box,
1175
+ from: Box | null,
1176
+ paint: HTMLElement,
1177
+ ): number | undefined {
1178
+ const track = this.track;
1179
+ if (track == null) {
1180
+ return undefined;
1181
+ }
1182
+ if (!this.isPresenting) {
1183
+ // Before the surface is borrowed: it must not fade out.
1184
+ this.stillTransitions();
1185
+ }
1186
+ const previous = this.carried;
1187
+ let start: MotionValues;
1188
+ // Motion already on screen continues from this instant; a start from
1189
+ // rest waits for the browser to commit it (the animator's rule).
1190
+ let at: number | undefined;
1191
+ this.velocity = {};
1192
+ if (previous != null) {
1193
+ at = this.now();
1194
+ const presented = this.presented(previous.box, at);
1195
+ start = {
1196
+ x: presented.center - center(box),
1197
+ scaleX: presented.width / box.width,
1198
+ scaleY: presented.height / box.height,
1199
+ };
1200
+ this.velocity = {
1201
+ x: this.animator.getVelocity('x', at),
1202
+ scaleX:
1203
+ (this.animator.getVelocity('scaleX', at) * previous.box.width) /
1204
+ box.width,
1205
+ scaleY:
1206
+ (this.animator.getVelocity('scaleY', at) * previous.box.height) /
1207
+ box.height,
1208
+ };
1209
+ } else {
1210
+ const origin = from ?? box;
1211
+ start = {
1212
+ x: center(origin) - center(box),
1213
+ scaleX: origin.width / box.width,
1214
+ scaleY: origin.height / box.height,
1215
+ };
1216
+ }
1217
+ if (previous?.hidden !== paint) {
1218
+ if (previous != null) {
1219
+ this.abandoned.add(previous.hidden);
1220
+ }
1221
+ // A segment abandoned mid-flight still has its surface hidden: read
1222
+ // its paint with it shown.
1223
+ if (this.abandoned.has(paint)) {
1224
+ showSurface(paint);
1225
+ }
1226
+ paintLayer(track, 'lens', paint);
1227
+ hideSurface(paint);
1228
+ this.abandoned.delete(paint);
1229
+ }
1230
+ placeLayer(track, 'lens', box);
1231
+ this.carried = {
1232
+ segment,
1233
+ box,
1234
+ hidden: paint,
1235
+ trackSize: {width: track.offsetWidth, height: track.offsetHeight},
1236
+ };
1237
+ // Opaque, whatever a Reduce Motion fade left.
1238
+ this.animator.set({...start, opacity: 1});
1239
+ return at;
1240
+ }
1241
+
1242
+ /** Velocities captured by the last `carry`, in the new box's units. */
1243
+ private velocity: Partial<Record<'x' | 'scaleX' | 'scaleY', number>> = {};
1244
+
1245
+ /** A spring transition that keeps the captured velocities of `values`. */
1246
+ private springFrom(
1247
+ values: MotionValues,
1248
+ kind: 'selection' | 'tracking',
1249
+ ): SpringTransition {
1250
+ const spring =
1251
+ kind === 'selection'
1252
+ ? segmentedControl.selectionSpring
1253
+ : springs.fingerTracking;
1254
+ const velocity: Record<string, number> = {};
1255
+ for (const key of Object.keys(values) as (keyof typeof this.velocity)[]) {
1256
+ const v = this.velocity[key];
1257
+ if (v != null && v !== 0) {
1258
+ velocity[key] = v;
1259
+ }
1260
+ }
1261
+ // A captured velocity is spent by the first call that drives its channel.
1262
+ this.velocity = Object.fromEntries(
1263
+ Object.entries(this.velocity).filter(([key]) => !(key in values)),
1264
+ );
1265
+ return Object.keys(velocity).length > 0 ? {spring, velocity} : {spring};
1266
+ }
1267
+
1268
+ /**
1269
+ * The lens settles at its resting place on `segment`. Under a finger it
1270
+ * springs on the slide spring. Under a fine pointer it takes the theme's
1271
+ * duration and easing, as the segments' own transition does.
1272
+ */
1273
+ private settleLens(segment: HTMLElement, options: AnimateOptions): void {
1274
+ const rest = {x: 0, scaleX: 1, scaleY: 1} as const;
1275
+ if (this.isPresenting) {
1276
+ this.springLens(rest, this.springFrom(rest, 'selection'), options);
1277
+ return;
1278
+ }
1279
+ this.velocity = {};
1280
+ const timing = themeTimingOf(segment) ?? INSTANT;
1281
+ const at = options.at;
1282
+ this.trackHandle(
1283
+ this.animator.play(
1284
+ {
1285
+ x: timedCurve(this.animator.get('x', at), 0, timing),
1286
+ scaleX: timedCurve(this.animator.get('scaleX', at), 1, timing),
1287
+ scaleY: timedCurve(this.animator.get('scaleY', at), 1, timing),
1288
+ },
1289
+ options,
1290
+ ),
1291
+ );
1292
+ }
1293
+
1294
+ /** Springs the lens toward `values`. */
1295
+ private springLens(
1296
+ values: MotionValues,
1297
+ transition: SpringTransition,
1298
+ options?: AnimateOptions,
1299
+ ): void {
1300
+ this.trackHandle(this.animator.animate(values, transition, options));
1301
+ }
1302
+
1303
+ /** The lens's presented center and size, from the animator. */
1304
+ private presented(
1305
+ box: Box,
1306
+ at: number,
1307
+ ): {center: number; width: number; height: number} {
1308
+ return {
1309
+ center: center(box) + this.animator.get('x', at),
1310
+ width: box.width * this.animator.get('scaleX', at),
1311
+ height: box.height * this.animator.get('scaleY', at),
1312
+ };
1313
+ }
1314
+
1315
+ /**
1316
+ * The lens settles on `segment`, at its resting size. Under Reduced Motion
1317
+ * a finger's release (`touchRelease`) cross-fades on any primary pointer. A
1318
+ * mouse's or a pen's lands at once under a fine one.
1319
+ */
1320
+ private moveLensTo(
1321
+ segment: HTMLElement,
1322
+ box: Box,
1323
+ paint: HTMLElement,
1324
+ touchRelease: boolean,
1325
+ ): void {
1326
+ this.isHeld = false;
1327
+ if (!isLaidOut(box)) {
1328
+ this.settle();
1329
+ return;
1330
+ }
1331
+ if (this.animator.reducedMotion({touchRelease}) !== 'off') {
1332
+ this.crossFadeTo(
1333
+ segment,
1334
+ box,
1335
+ null,
1336
+ this.carried?.hidden ?? paint,
1337
+ touchRelease,
1338
+ );
1339
+ return;
1340
+ }
1341
+ const at = this.carry(segment, box, null, this.carried?.hidden ?? paint);
1342
+ this.settleLens(segment, {at});
1343
+ }
1344
+
1345
+ /**
1346
+ * A tap's slide: from `from` to `segment` on the slide spring. Under Reduced
1347
+ * Motion there is no travel, and a finger's tap (`touchRelease`) fades.
1348
+ */
1349
+ private slideTo(
1350
+ segment: HTMLElement,
1351
+ box: Box,
1352
+ from: Box,
1353
+ paint: HTMLElement,
1354
+ touchRelease: boolean,
1355
+ ): void {
1356
+ if (!isLaidOut(box)) {
1357
+ this.settle();
1358
+ return;
1359
+ }
1360
+ if (this.animator.reducedMotion({touchRelease}) !== 'off') {
1361
+ this.crossFadeTo(
1362
+ segment,
1363
+ box,
1364
+ this.carried == null ? from : null,
1365
+ this.carried?.hidden ?? paint,
1366
+ touchRelease,
1367
+ );
1368
+ return;
1369
+ }
1370
+ const at = this.carry(
1371
+ segment,
1372
+ box,
1373
+ this.carried == null ? from : null,
1374
+ this.carried?.hidden ?? paint,
1375
+ );
1376
+ this.settleLens(segment, {at});
1377
+ }
1378
+
1379
+ /**
1380
+ * Under Reduced Motion, the lens lands on `segment` and fades in there, as
1381
+ * an animator entrance. The surface on screen fades out in place on the
1382
+ * same clock. That surface is the lens when it was carried, else `from`'s
1383
+ * resting surface. The animator decides whether anything fades. On a fine
1384
+ * pointer, both change at once unless a touch released the change
1385
+ * (`touchRelease`).
1386
+ */
1387
+ private crossFadeTo(
1388
+ segment: HTMLElement,
1389
+ box: Box,
1390
+ from: Box | null,
1391
+ paint: HTMLElement,
1392
+ touchRelease: boolean,
1393
+ ): void {
1394
+ const track = this.track;
1395
+ if (track == null) {
1396
+ return;
1397
+ }
1398
+ // What is on screen before the lens is rebound.
1399
+ let leaving: Box | null = null;
1400
+ let opacity = 1;
1401
+ const previous = this.carried;
1402
+ if (previous != null) {
1403
+ const now = this.now();
1404
+ const presented = this.presented(previous.box, now);
1405
+ leaving = {
1406
+ x: presented.center - presented.width / 2,
1407
+ y: previous.box.y,
1408
+ width: presented.width,
1409
+ height: previous.box.height,
1410
+ };
1411
+ opacity = this.animator.get('opacity', now);
1412
+ } else if (from != null) {
1413
+ leaving = from;
1414
+ }
1415
+ this.isHeld = false;
1416
+ this.endLeaving();
1417
+ this.after.stop();
1418
+ clearLayer(track, 'after');
1419
+ this.isLeavingShown = false;
1420
+ const fades =
1421
+ leaving != null &&
1422
+ opacity > 0 &&
1423
+ this.animator.reducedMotion({touchRelease}) === 'crossfade';
1424
+ if (fades) {
1425
+ this.leave(track, previous != null, paint, leaving as Box, opacity, box);
1426
+ }
1427
+ const at = this.carry(segment, box, from, paint);
1428
+ this.velocity = {};
1429
+ this.trackHandle(
1430
+ this.animator.animate(
1431
+ {x: 0, scaleX: 1, scaleY: 1},
1432
+ {spring: segmentedControl.selectionSpring},
1433
+ {at, crossfadeAs: 'entrance', touchRelease},
1434
+ ),
1435
+ );
1436
+ if (fades) {
1437
+ this.fadeLeaving(box, leaving as Box, at, touchRelease);
1438
+ }
1439
+ }
1440
+
1441
+ /**
1442
+ * Paints the ::after with the surface on screen (the lens's paint, or
1443
+ * `paint`'s resting surface) where it is (`from`, at `opacity`).
1444
+ */
1445
+ private leave(
1446
+ track: HTMLElement,
1447
+ fromLens: boolean,
1448
+ paint: HTMLElement,
1449
+ from: Box,
1450
+ opacity: number,
1451
+ box: Box,
1452
+ ): void {
1453
+ if (fromLens) {
1454
+ copyLensPaint(track);
1455
+ } else {
1456
+ paintLayer(track, 'after', paint);
1457
+ }
1458
+ placeLayer(track, 'after', {...from, y: box.y, height: box.height});
1459
+ this.isLeavingShown = true;
1460
+ this.after.set({x: 0, scaleX: 1, scaleY: 1, opacity});
1461
+ }
1462
+
1463
+ /**
1464
+ * The ::after fades out in place while the lens fades in on `box`. As a
1465
+ * leaving travel, it fades on the animator's Reduce Motion clock. It lands
1466
+ * only once it is invisible.
1467
+ */
1468
+ private fadeLeaving(
1469
+ box: Box,
1470
+ from: Box,
1471
+ at: number | undefined,
1472
+ touchRelease: boolean,
1473
+ ): void {
1474
+ const id = ++this.leaving;
1475
+ const handle = this.after.animate(
1476
+ {x: center(box) - center(from), opacity: 0},
1477
+ {spring: segmentedControl.selectionSpring},
1478
+ {at, touchRelease},
1479
+ );
1480
+ void handle.finished.then(() => {
1481
+ if (this.leaving === id) {
1482
+ this.endLeaving();
1483
+ }
1484
+ });
1485
+ }
1486
+
1487
+ private endLeaving(): void {
1488
+ if (!this.isLeavingShown) {
1489
+ return;
1490
+ }
1491
+ this.isLeavingShown = false;
1492
+ this.leaving++;
1493
+ this.after.stop();
1494
+ if (this.track != null) {
1495
+ clearLayer(this.track, 'after');
1496
+ }
1497
+ }
1498
+
1499
+ /**
1500
+ * Waits for `handle`; the surface goes back when nothing is left. A
1501
+ * touch's value still waiting to come back when the motion settles goes
1502
+ * back instead: the settled slide bounds the wait.
1503
+ */
1504
+ private trackHandle(handle: MotionHandle): void {
1505
+ this.running.add(handle);
1506
+ void handle.finished.then(() => {
1507
+ this.running.delete(handle);
1508
+ if (this.running.size > 0 || this.isHeld || this.touch != null) {
1509
+ return;
1510
+ }
1511
+ const committed = this.committed;
1512
+ if (this.isAwaitingValue && committed != null && this.track != null) {
1513
+ this.decline(this.track, committed);
1514
+ return;
1515
+ }
1516
+ if (this.committed == null) {
1517
+ this.settle();
1518
+ }
1519
+ });
1520
+ }
1521
+
1522
+ /**
1523
+ * Gives `segment`'s title the selected face and the selected segment's
1524
+ * title an unselected one (none for null or the selected segment), at once.
1525
+ */
1526
+ private showFaceAt(segment: HTMLElement | null): void {
1527
+ const touch = this.touch;
1528
+ const selected = touch?.origin ?? null;
1529
+ if (segment === this.facedSegment) {
1530
+ return;
1531
+ }
1532
+ this.clearFaces();
1533
+ if (
1534
+ touch?.faces == null ||
1535
+ selected == null ||
1536
+ segment == null ||
1537
+ segment === selected
1538
+ ) {
1539
+ return;
1540
+ }
1541
+ const title = titleOf(segment);
1542
+ const selectedTitle = titleOf(selected);
1543
+ if (title == null || selectedTitle == null) {
1544
+ return;
1545
+ }
1546
+ this.wearFace(title, touch.faces.selected);
1547
+ this.wearFace(selectedTitle, touch.faces.unselected);
1548
+ this.facedSegment = segment;
1549
+ }
1550
+
1551
+ /**
1552
+ * Mid-drag, the face is the color alone. A weight swap changes the titles'
1553
+ * widths, which moves the segments under the finger and shakes the
1554
+ * boundary. The commit's own styles give the new weight at the lift.
1555
+ */
1556
+ private wearFace(title: HTMLElement, face: TitleFace): void {
1557
+ title.style.color = face.color;
1558
+ this.faceOverrides.add(title);
1559
+ }
1560
+
1561
+ /** Every title back to its own face. */
1562
+ private clearFaces(): void {
1563
+ for (const title of this.faceOverrides) {
1564
+ title.style.removeProperty('color');
1565
+ }
1566
+ this.faceOverrides.clear();
1567
+ this.facedSegment = null;
1568
+ }
1569
+ }
1570
+
1571
+ /**
1572
+ * Wires `SegmentedSelectionMotion` to a SegmentedControl. While enabled,
1573
+ * every change of `value` slides the selection in the commit that shows the
1574
+ * new value, so no frame shows it unanimated. A press drives the lens while
1575
+ * `isTracking`.
1576
+ */
1577
+ export function useSegmentedSelectionMotion(
1578
+ trackRef: RefObject<HTMLElement | null>,
1579
+ value: string,
1580
+ isPresenting: boolean,
1581
+ isTracking: boolean,
1582
+ ): {
1583
+ readonly selection: SegmentedSelection;
1584
+ /** The radiogroup's callback ref: it attaches the animators to the node. */
1585
+ readonly attachTrack: RefCallback<HTMLElement>;
1586
+ } {
1587
+ const [motion] = useState(() => new SegmentedSelectionMotion());
1588
+ const previousValueRef = useRef(value);
1589
+ // The lens is the radiogroup's own pseudo-elements. The animators attach to
1590
+ // it while the touch presentation applies or while a press can drag it,
1591
+ // which any pointer can.
1592
+ const isEnabled = isPresenting || isTracking;
1593
+
1594
+ // Syncs the lens animators with the pointer presentation: a flip hands any
1595
+ // motion in flight back.
1596
+ useIsomorphicLayoutEffect(() => {
1597
+ motion.present(isPresenting);
1598
+ }, [isPresenting, motion]);
1599
+
1600
+ useIsomorphicLayoutEffect(() => {
1601
+ const from = previousValueRef.current;
1602
+ previousValueRef.current = value;
1603
+ const track = trackRef.current;
1604
+ if (!isEnabled || track == null || from === value) {
1605
+ return;
1606
+ }
1607
+ motion.select(track, from, value);
1608
+ }, [isEnabled, motion, trackRef, value]);
1609
+
1610
+ // Stops the animators that drive the radiogroup's pseudo-elements.
1611
+ useEffect(() => () => motion.stop(), [motion]);
1612
+
1613
+ // Every pointer's press is tracked. The lift commits the segment nearest
1614
+ // the pointer, through that segment's click.
1615
+ const clickWait = useSegmentedTouchTracking(trackRef, motion, isTracking);
1616
+
1617
+ // The animators attach to the radiogroup while enabled. The lens aims at
1618
+ // boxes measured when its motion began, so a resize hands the surface back.
1619
+ const attachTrack = useCallback(
1620
+ (track: HTMLElement | null) => {
1621
+ if (track == null || !isEnabled) {
1622
+ return undefined;
1623
+ }
1624
+ motion.attachTrack(track);
1625
+ const unobserve = observeResize(track, () => motion.resized(track));
1626
+ return () => {
1627
+ unobserve();
1628
+ motion.settle();
1629
+ motion.attachTrack(null);
1630
+ };
1631
+ },
1632
+ [isEnabled, motion],
1633
+ );
1634
+
1635
+ const [selection] = useState<SegmentedSelection>(() => ({
1636
+ isPressing: () => motion.isPressed || clickWait.isAwaiting(),
1637
+ keyPressed: clickWait.end,
1638
+ }));
1639
+ return {selection, attachTrack};
1640
+ }