@primitiv-ui/react 0.1.28 → 0.1.30

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 (456) hide show
  1. package/README.md +19 -2
  2. package/package.json +6 -1
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +32 -9
  4. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +5 -0
  5. package/src/AccessibleIcon/types.ts +15 -2
  6. package/src/Accordion/Accordion.tsx +32 -11
  7. package/src/Accordion/README.md +2 -2
  8. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +46 -0
  9. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +15 -0
  10. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +42 -0
  11. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +33 -0
  12. package/src/Accordion/hooks/useAccordionRoot.ts +44 -7
  13. package/src/Accordion/hooks/useAccordionTrigger.ts +9 -1
  14. package/src/Accordion/types.ts +72 -21
  15. package/src/Alert/Alert.tsx +36 -7
  16. package/src/Alert/__tests__/Alert.test.tsx +5 -0
  17. package/src/Alert/types.ts +18 -2
  18. package/src/Avatar/Avatar.tsx +99 -15
  19. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +10 -0
  20. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +30 -0
  21. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +35 -0
  22. package/src/Avatar/hooks/useAvatarImage.ts +16 -0
  23. package/src/Avatar/types.ts +15 -3
  24. package/src/Breadcrumb/Breadcrumb.tsx +139 -21
  25. package/src/Breadcrumb/README.md +47 -0
  26. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +11 -0
  27. package/src/Breadcrumb/__tests__/Breadcrumb.ellipsis.test.tsx +110 -0
  28. package/src/Breadcrumb/types.ts +73 -7
  29. package/src/Button/Button.tsx +2 -0
  30. package/src/Button/__tests__/Button.basic-rendering.test.tsx +6 -0
  31. package/src/Button/__tests__/Button.contract.test.tsx +1 -1
  32. package/src/Button/types.ts +21 -0
  33. package/src/Carousel/Carousel.tsx +268 -20
  34. package/src/Carousel/README.md +764 -87
  35. package/src/Carousel/__tests__/Carousel.autoplay-status.test.tsx +239 -0
  36. package/src/Carousel/__tests__/Carousel.drag-status.test.tsx +292 -0
  37. package/src/Carousel/__tests__/Carousel.dynamic-slides.test.tsx +38 -0
  38. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +101 -0
  39. package/src/Carousel/__tests__/Carousel.infinite.test.tsx +1453 -0
  40. package/src/Carousel/__tests__/Carousel.instant-scroll.test.tsx +124 -0
  41. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +90 -2
  42. package/src/Carousel/__tests__/Carousel.loop.test.tsx +163 -0
  43. package/src/Carousel/__tests__/Carousel.loopEngine.test.ts +77 -0
  44. package/src/Carousel/__tests__/Carousel.mouse-drag.test.tsx +504 -0
  45. package/src/Carousel/__tests__/Carousel.multi-slide-bounds.test.tsx +91 -0
  46. package/src/Carousel/__tests__/Carousel.orientation.test.tsx +207 -0
  47. package/src/Carousel/__tests__/Carousel.overscroll.test.tsx +532 -0
  48. package/src/Carousel/__tests__/Carousel.page-snap-points.test.tsx +50 -0
  49. package/src/Carousel/__tests__/Carousel.progress-text.test.tsx +76 -0
  50. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +3 -6
  51. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +2 -5
  52. package/src/Carousel/__tests__/Carousel.scroll-margin.test.tsx +167 -0
  53. package/src/Carousel/__tests__/Carousel.scroll-progress.test.tsx +518 -0
  54. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +32 -11
  55. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +237 -24
  56. package/src/Carousel/__tests__/Carousel.scroll-to-index.test.tsx +115 -0
  57. package/src/Carousel/__tests__/Carousel.slide-snap.test.tsx +172 -0
  58. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +179 -1
  59. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +91 -7
  60. package/src/Carousel/__tests__/Carousel.snap-type.test.tsx +35 -0
  61. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +30 -5
  62. package/src/Carousel/__tests__/Carousel.translations.test.tsx +23 -0
  63. package/src/Carousel/__tests__/Carousel.wheel.test.tsx +160 -0
  64. package/src/Carousel/hooks/index.ts +1 -0
  65. package/src/Carousel/hooks/useCarouselLoop.ts +869 -0
  66. package/src/Carousel/hooks/useCarouselRoot.ts +338 -42
  67. package/src/Carousel/hooks/useCarouselSlide.ts +44 -9
  68. package/src/Carousel/hooks/useCarouselViewport.ts +649 -46
  69. package/src/Carousel/loopEngine.ts +54 -0
  70. package/src/Carousel/types.ts +458 -36
  71. package/src/Checkbox/Checkbox.tsx +11 -3
  72. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +14 -0
  73. package/src/Checkbox/hooks/useCheckboxInput.ts +4 -0
  74. package/src/Checkbox/types.ts +19 -0
  75. package/src/CheckboxCard/CheckboxCard.tsx +14 -5
  76. package/src/CheckboxCard/README.md +1 -1
  77. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +7 -0
  78. package/src/CheckboxCard/types.ts +65 -17
  79. package/src/Collapsible/Collapsible.tsx +49 -3
  80. package/src/Collapsible/README.md +41 -1
  81. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +89 -0
  82. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +27 -0
  83. package/src/Collapsible/__tests__/Collapsible.collapsedHeight.test.tsx +140 -0
  84. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +3 -1
  85. package/src/Collapsible/types.ts +89 -22
  86. package/src/Combobox/Combobox.tsx +508 -0
  87. package/src/Combobox/ComboboxContext.ts +23 -0
  88. package/src/Combobox/README.md +170 -0
  89. package/src/Combobox/__tests__/Combobox.asChild.test.tsx +62 -0
  90. package/src/Combobox/__tests__/Combobox.basic-rendering.test.tsx +16 -0
  91. package/src/Combobox/__tests__/Combobox.content.test.tsx +36 -0
  92. package/src/Combobox/__tests__/Combobox.controlled-state.test.tsx +88 -0
  93. package/src/Combobox/__tests__/Combobox.empty.test.tsx +42 -0
  94. package/src/Combobox/__tests__/Combobox.ids.test.tsx +66 -0
  95. package/src/Combobox/__tests__/Combobox.item.test.tsx +76 -0
  96. package/src/Combobox/__tests__/Combobox.keyboard-interaction.test.tsx +196 -0
  97. package/src/Combobox/__tests__/Combobox.light-dismiss.test.tsx +101 -0
  98. package/src/Combobox/__tests__/Combobox.query.test.tsx +105 -0
  99. package/src/Combobox/hooks/index.ts +1 -0
  100. package/src/Combobox/hooks/useComboboxRoot.ts +2 -0
  101. package/src/Combobox/index.ts +3 -0
  102. package/src/Combobox/types.ts +133 -0
  103. package/src/ContextMenu/ContextMenu.tsx +270 -4
  104. package/src/ContextMenu/README.md +1 -1
  105. package/src/ContextMenu/types.ts +148 -6
  106. package/src/DirectionProvider/DirectionProvider.tsx +41 -9
  107. package/src/DirectionProvider/README.md +4 -4
  108. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +5 -0
  109. package/src/DirectionProvider/types.ts +28 -3
  110. package/src/Divider/Divider.tsx +24 -10
  111. package/src/Divider/__tests__/Divider.contract.test.tsx +1 -1
  112. package/src/Divider/__tests__/Divider.test.tsx +5 -0
  113. package/src/Divider/types.ts +10 -3
  114. package/src/Drawer/Drawer.tsx +315 -0
  115. package/src/Drawer/README.md +124 -0
  116. package/src/Drawer/__tests__/Drawer.behavior.test.tsx +100 -0
  117. package/src/Drawer/__tests__/Drawer.focus-trap.test.tsx +55 -0
  118. package/src/Drawer/__tests__/Drawer.rendering.test.tsx +111 -0
  119. package/src/Drawer/__tests__/Drawer.side.test.tsx +69 -0
  120. package/src/Drawer/__tests__/dialog-polyfill.ts +39 -0
  121. package/src/Drawer/index.ts +2 -0
  122. package/src/Drawer/types.ts +66 -0
  123. package/src/Dropdown/Dropdown.tsx +119 -2
  124. package/src/Dropdown/README.md +7 -5
  125. package/src/Dropdown/types.ts +241 -11
  126. package/src/EmptyState/EmptyState.tsx +74 -4
  127. package/src/EmptyState/README.md +2 -2
  128. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +7 -2
  129. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +3 -1
  130. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +10 -0
  131. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +3 -1
  132. package/src/EmptyState/types.ts +49 -6
  133. package/src/Field/Field.tsx +23 -0
  134. package/src/Field/README.md +1 -1
  135. package/src/Field/__tests__/Field.basic-rendering.test.tsx +11 -0
  136. package/src/Field/__tests__/Field.error-handling.test.tsx +23 -0
  137. package/src/Field/__tests__/Field.state-cascade.test.tsx +21 -0
  138. package/src/Field/hooks/__tests__/useFieldProps.test.tsx +104 -9
  139. package/src/Field/hooks/useFieldProps.ts +1 -1
  140. package/src/Field/types.ts +60 -12
  141. package/src/Fieldset/Fieldset.tsx +55 -19
  142. package/src/Fieldset/README.md +2 -2
  143. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +9 -0
  144. package/src/Fieldset/types.ts +20 -2
  145. package/src/Input/Input.tsx +15 -10
  146. package/src/Input/README.md +2 -2
  147. package/src/Input/__tests__/Input.basic-rendering.test.tsx +5 -0
  148. package/src/Input/types.ts +19 -2
  149. package/src/InputGroup/InputGroup.tsx +11 -3
  150. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +14 -0
  151. package/src/InputGroup/types.ts +26 -5
  152. package/src/Listbox/Listbox.tsx +475 -0
  153. package/src/Listbox/ListboxContext.ts +48 -0
  154. package/src/Listbox/README.md +248 -0
  155. package/src/Listbox/__tests__/Listbox.active-descendant.test.tsx +128 -0
  156. package/src/Listbox/__tests__/Listbox.asChild.test.tsx +91 -0
  157. package/src/Listbox/__tests__/Listbox.basic-rendering.test.tsx +16 -0
  158. package/src/Listbox/__tests__/Listbox.controlled-state.test.tsx +114 -0
  159. package/src/Listbox/__tests__/Listbox.disabled.test.tsx +90 -0
  160. package/src/Listbox/__tests__/Listbox.fixtures.ts +27 -0
  161. package/src/Listbox/__tests__/Listbox.group-label.test.tsx +97 -0
  162. package/src/Listbox/__tests__/Listbox.group.test.tsx +45 -0
  163. package/src/Listbox/__tests__/Listbox.keyboard-edge-cases.test.tsx +144 -0
  164. package/src/Listbox/__tests__/Listbox.keyboard-interaction.test.tsx +112 -0
  165. package/src/Listbox/__tests__/Listbox.modifier-keys.test.tsx +86 -0
  166. package/src/Listbox/__tests__/Listbox.multi-select-keyboard.test.tsx +368 -0
  167. package/src/Listbox/__tests__/Listbox.multiple-mode.test.tsx +91 -0
  168. package/src/Listbox/__tests__/Listbox.mutation-hardening.test.tsx +480 -0
  169. package/src/Listbox/__tests__/Listbox.orientation.test.tsx +59 -0
  170. package/src/Listbox/__tests__/Listbox.reading-direction.test.tsx +65 -0
  171. package/src/Listbox/__tests__/Listbox.reorder.test.tsx +78 -0
  172. package/src/Listbox/__tests__/Listbox.selection-follows-focus.test.tsx +64 -0
  173. package/src/Listbox/__tests__/Listbox.strict-context.test.tsx +21 -0
  174. package/src/Listbox/__tests__/Listbox.typeahead.test.tsx +123 -0
  175. package/src/Listbox/__tests__/Listbox.uncontrolled-state.test.tsx +48 -0
  176. package/src/Listbox/constants.ts +2 -0
  177. package/src/Listbox/hooks/index.ts +1 -0
  178. package/src/Listbox/hooks/useListboxRoot.ts +407 -0
  179. package/src/Listbox/index.ts +3 -0
  180. package/src/Listbox/types.ts +238 -0
  181. package/src/MillerColumns/MillerColumns.tsx +186 -45
  182. package/src/MillerColumns/README.md +90 -20
  183. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +45 -0
  184. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +4 -2
  185. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +18 -3
  186. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +48 -0
  187. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +35 -2
  188. package/src/MillerColumns/__tests__/MillerColumns.keyboard-selection-state.test.tsx +96 -0
  189. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +29 -4
  190. package/src/MillerColumns/__tests__/MillerColumns.preview-visibility.test.tsx +79 -0
  191. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +189 -0
  192. package/src/MillerColumns/__tests__/MillerColumns.styled-parts.test.tsx +143 -0
  193. package/src/MillerColumns/__tests__/MillerColumns.typeahead.test.tsx +104 -0
  194. package/src/MillerColumns/constants.ts +2 -0
  195. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +5 -2
  196. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +29 -7
  197. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +80 -10
  198. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  199. package/src/MillerColumns/index.ts +4 -0
  200. package/src/MillerColumns/types.ts +136 -30
  201. package/src/MillerColumns/utils.ts +69 -3
  202. package/src/Modal/Modal.tsx +35 -8
  203. package/src/Modal/README.md +24 -15
  204. package/src/Modal/__tests__/Modal.focus-trap.test.tsx +162 -0
  205. package/src/Modal/hooks/useModalContent.ts +32 -0
  206. package/src/Modal/types.ts +110 -29
  207. package/src/NavigationMenu/NavigationMenu.tsx +890 -0
  208. package/src/NavigationMenu/NavigationMenuContext.ts +67 -0
  209. package/src/NavigationMenu/README.md +355 -0
  210. package/src/NavigationMenu/__tests__/NavigationMenu.anchor-positioning.test.tsx +132 -0
  211. package/src/NavigationMenu/__tests__/NavigationMenu.asChild.test.tsx +73 -0
  212. package/src/NavigationMenu/__tests__/NavigationMenu.basic-rendering.test.tsx +92 -0
  213. package/src/NavigationMenu/__tests__/NavigationMenu.contract.test.tsx +45 -0
  214. package/src/NavigationMenu/__tests__/NavigationMenu.controlled-state.test.tsx +134 -0
  215. package/src/NavigationMenu/__tests__/NavigationMenu.error-handling.test.tsx +62 -0
  216. package/src/NavigationMenu/__tests__/NavigationMenu.fixtures.tsx +79 -0
  217. package/src/NavigationMenu/__tests__/NavigationMenu.forceMount.test.tsx +52 -0
  218. package/src/NavigationMenu/__tests__/NavigationMenu.hover-interaction.test.tsx +254 -0
  219. package/src/NavigationMenu/__tests__/NavigationMenu.indicator.test.tsx +252 -0
  220. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-interaction.test.tsx +294 -0
  221. package/src/NavigationMenu/__tests__/NavigationMenu.keyboard-navigation.test.tsx +126 -0
  222. package/src/NavigationMenu/__tests__/NavigationMenu.link.test.tsx +139 -0
  223. package/src/NavigationMenu/__tests__/NavigationMenu.motion.test.tsx +568 -0
  224. package/src/NavigationMenu/__tests__/NavigationMenu.mouse-interaction.test.tsx +76 -0
  225. package/src/NavigationMenu/__tests__/NavigationMenu.uncontrolled-state.test.tsx +30 -0
  226. package/src/NavigationMenu/__tests__/NavigationMenu.viewport.test.tsx +113 -0
  227. package/src/NavigationMenu/constants.ts +5 -0
  228. package/src/NavigationMenu/hooks/index.ts +7 -0
  229. package/src/NavigationMenu/hooks/useNavigationMenuEntry.ts +33 -0
  230. package/src/NavigationMenu/hooks/useNavigationMenuIndicator.ts +70 -0
  231. package/src/NavigationMenu/hooks/useNavigationMenuLink.ts +72 -0
  232. package/src/NavigationMenu/hooks/useNavigationMenuRoot.ts +301 -0
  233. package/src/NavigationMenu/hooks/useNavigationMenuTopLevelEntry.ts +65 -0
  234. package/src/NavigationMenu/hooks/useNavigationMenuTrigger.ts +158 -0
  235. package/src/NavigationMenu/hooks/useNavigationMenuViewport.ts +101 -0
  236. package/src/NavigationMenu/index.ts +3 -0
  237. package/src/NavigationMenu/types.ts +259 -0
  238. package/src/NavigationMenu/utils.ts +89 -0
  239. package/src/Popover/Popover.tsx +427 -0
  240. package/src/Popover/PopoverContext.ts +13 -0
  241. package/src/Popover/README.md +106 -0
  242. package/src/Popover/__tests__/Popover.anchor.test.tsx +38 -0
  243. package/src/Popover/__tests__/Popover.asChild.test.tsx +160 -0
  244. package/src/Popover/__tests__/Popover.basic-rendering.test.tsx +106 -0
  245. package/src/Popover/__tests__/Popover.click-outside.test.tsx +149 -0
  246. package/src/Popover/__tests__/Popover.close.test.tsx +70 -0
  247. package/src/Popover/__tests__/Popover.content.test.tsx +58 -0
  248. package/src/Popover/__tests__/Popover.controlled-state.test.tsx +45 -0
  249. package/src/Popover/__tests__/Popover.error-handling.test.tsx +51 -0
  250. package/src/Popover/__tests__/Popover.focus.test.tsx +43 -0
  251. package/src/Popover/__tests__/Popover.keyboard-interaction.test.tsx +71 -0
  252. package/src/Popover/__tests__/Popover.mouse-interaction.test.tsx +69 -0
  253. package/src/Popover/__tests__/Popover.uncontrolled-state.test.tsx +44 -0
  254. package/src/Popover/hooks/index.ts +3 -0
  255. package/src/Popover/hooks/usePopoverContent.ts +98 -0
  256. package/src/Popover/hooks/usePopoverRoot.ts +84 -0
  257. package/src/Popover/hooks/usePopoverTrigger.ts +33 -0
  258. package/src/Popover/index.ts +2 -0
  259. package/src/Popover/types.ts +197 -0
  260. package/src/Portal/Portal.tsx +50 -10
  261. package/src/Portal/README.md +2 -2
  262. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +5 -0
  263. package/src/Portal/types.ts +5 -0
  264. package/src/Progress/Progress.tsx +33 -1
  265. package/src/Progress/README.md +1 -1
  266. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +10 -0
  267. package/src/Progress/__tests__/Progress.value.test.tsx +21 -0
  268. package/src/Progress/types.ts +20 -4
  269. package/src/Radio/Radio.tsx +10 -2
  270. package/src/Radio/__tests__/Radio.basic-rendering.test.tsx +14 -0
  271. package/src/Radio/hooks/useRadioRoot.ts +3 -0
  272. package/src/Radio/types.ts +20 -0
  273. package/src/RadioCard/RadioCard.tsx +11 -0
  274. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +8 -0
  275. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +27 -0
  276. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +53 -0
  277. package/src/RadioCard/__tests__/RadioCard.ref-forwarding.test.tsx +54 -0
  278. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +21 -0
  279. package/src/RadioCard/hooks/useRadioCardRoot.ts +11 -0
  280. package/src/RadioCard/types.ts +95 -22
  281. package/src/RadioGroup/README.md +2 -2
  282. package/src/RadioGroup/RadioGroup.tsx +12 -1
  283. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +8 -0
  284. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +27 -0
  285. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +29 -0
  286. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +31 -0
  287. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +11 -0
  288. package/src/RadioGroup/types.ts +73 -9
  289. package/src/SegmentedControl/README.md +154 -0
  290. package/src/SegmentedControl/SegmentedControl.tsx +309 -0
  291. package/src/SegmentedControl/SegmentedControlContext.ts +28 -0
  292. package/src/SegmentedControl/__tests__/SegmentedControl.asChild.test.tsx +50 -0
  293. package/src/SegmentedControl/__tests__/SegmentedControl.basic-rendering.test.tsx +100 -0
  294. package/src/SegmentedControl/__tests__/SegmentedControl.controlled-state.test.tsx +107 -0
  295. package/src/SegmentedControl/__tests__/SegmentedControl.disabled-keydown-guards.test.tsx +62 -0
  296. package/src/SegmentedControl/__tests__/SegmentedControl.disabled.test.tsx +151 -0
  297. package/src/SegmentedControl/__tests__/SegmentedControl.error-handling.test.tsx +17 -0
  298. package/src/SegmentedControl/__tests__/SegmentedControl.keyboard-interaction.test.tsx +117 -0
  299. package/src/SegmentedControl/__tests__/SegmentedControl.orientation.test.tsx +54 -0
  300. package/src/SegmentedControl/__tests__/SegmentedControl.reading-direction.test.tsx +56 -0
  301. package/src/SegmentedControl/__tests__/SegmentedControl.ref-forwarding.test.tsx +45 -0
  302. package/src/SegmentedControl/__tests__/SegmentedControl.roving-tabindex.test.tsx +104 -0
  303. package/src/SegmentedControl/__tests__/SegmentedControl.uncontrolled-state.test.tsx +94 -0
  304. package/src/SegmentedControl/hooks/index.ts +2 -0
  305. package/src/SegmentedControl/hooks/useSegmentedControlContext.ts +1 -0
  306. package/src/SegmentedControl/hooks/useSegmentedControlRoot.ts +87 -0
  307. package/src/SegmentedControl/index.ts +2 -0
  308. package/src/SegmentedControl/types.ts +138 -0
  309. package/src/Select/README.md +122 -157
  310. package/src/Select/Select.tsx +596 -69
  311. package/src/Select/SelectContext.ts +38 -0
  312. package/src/Select/SelectItemIndicatorContext.ts +29 -0
  313. package/src/Select/__tests__/Select.asChild.test.tsx +2 -2
  314. package/src/Select/__tests__/Select.basic-rendering.test.tsx +22 -3
  315. package/src/Select/__tests__/Select.controlled-state.test.tsx +6 -6
  316. package/src/Select/__tests__/Select.data-attributes.test.tsx +4 -4
  317. package/src/Select/__tests__/Select.field-integration.test.tsx +11 -11
  318. package/src/Select/__tests__/Select.form.test.tsx +38 -0
  319. package/src/Select/__tests__/Select.group.test.tsx +6 -6
  320. package/src/Select/__tests__/Select.keyboard-edge.test.tsx +95 -0
  321. package/src/Select/__tests__/Select.keyboard.test.tsx +124 -0
  322. package/src/Select/__tests__/Select.mutation-hardening.test.tsx +317 -0
  323. package/src/Select/__tests__/Select.native-item.test.tsx +30 -0
  324. package/src/Select/__tests__/Select.open-close.test.tsx +57 -0
  325. package/src/Select/__tests__/Select.placeholder.test.tsx +7 -7
  326. package/src/Select/__tests__/Select.rich-asChild.test.tsx +60 -0
  327. package/src/Select/__tests__/Select.rich-mode.test.tsx +16 -0
  328. package/src/Select/__tests__/Select.selection.test.tsx +104 -0
  329. package/src/Select/__tests__/Select.separator.test.tsx +71 -0
  330. package/src/Select/__tests__/Select.strict-context.test.tsx +36 -0
  331. package/src/Select/__tests__/Select.typeahead.test.tsx +156 -0
  332. package/src/Select/__tests__/Select.value-mirror.test.tsx +240 -0
  333. package/src/Select/constants.ts +8 -0
  334. package/src/Select/hooks/index.ts +2 -0
  335. package/src/Select/hooks/useSelectContent.ts +149 -0
  336. package/src/Select/hooks/useSelectRoot.ts +149 -0
  337. package/src/Select/types.ts +199 -31
  338. package/src/SkipNav/README.md +3 -3
  339. package/src/SkipNav/SkipNav.tsx +72 -32
  340. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +6 -0
  341. package/src/SkipNav/types.ts +17 -9
  342. package/src/Slider/README.md +37 -7
  343. package/src/Slider/Slider.tsx +42 -4
  344. package/src/Slider/SliderContext.ts +28 -1
  345. package/src/Slider/types.ts +157 -9
  346. package/src/Slot/Slot.tsx +94 -19
  347. package/src/Slot/__tests__/Slot.test.tsx +112 -2
  348. package/src/Slot/composeEventHandlers.ts +26 -2
  349. package/src/Slot/types.ts +16 -0
  350. package/src/SplitButton/README.md +225 -0
  351. package/src/SplitButton/SplitButton.tsx +457 -0
  352. package/src/SplitButton/SplitButtonContext.ts +26 -0
  353. package/src/SplitButton/__tests__/SplitButton.accessibility.test.tsx +55 -0
  354. package/src/SplitButton/__tests__/SplitButton.asChild.test.tsx +51 -0
  355. package/src/SplitButton/__tests__/SplitButton.basic-rendering.test.tsx +57 -0
  356. package/src/SplitButton/__tests__/SplitButton.data-attributes.test.tsx +28 -0
  357. package/src/SplitButton/__tests__/SplitButton.disabled.test.tsx +79 -0
  358. package/src/SplitButton/__tests__/SplitButton.error-handling.test.tsx +29 -0
  359. package/src/SplitButton/__tests__/SplitButton.keyboard-interaction.test.tsx +56 -0
  360. package/src/SplitButton/__tests__/SplitButton.state-modes.test.tsx +68 -0
  361. package/src/SplitButton/index.ts +2 -0
  362. package/src/SplitButton/types.ts +186 -0
  363. package/src/Status/Status.tsx +36 -15
  364. package/src/Status/__tests__/Status.test.tsx +5 -0
  365. package/src/Status/types.ts +12 -1
  366. package/src/Switch/README.md +1 -1
  367. package/src/Switch/Switch.tsx +17 -4
  368. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +7 -0
  369. package/src/Switch/hooks/useSwitchRoot.ts +3 -0
  370. package/src/Switch/types.ts +49 -22
  371. package/src/Table/README.md +110 -8
  372. package/src/Table/Table.tsx +246 -16
  373. package/src/Table/TableExpandableContext.ts +7 -0
  374. package/src/Table/__tests__/Table.DetailRow.test.tsx +83 -0
  375. package/src/Table/__tests__/Table.ExpandTrigger.test.tsx +102 -0
  376. package/src/Table/__tests__/Table.Expandable.test.tsx +121 -0
  377. package/src/Table/types.ts +191 -17
  378. package/src/Tabs/README.md +4 -4
  379. package/src/Tabs/Tabs.tsx +13 -4
  380. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +58 -0
  381. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +15 -0
  382. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +24 -0
  383. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +36 -0
  384. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +17 -0
  385. package/src/Tabs/hooks/useTabsContent.ts +5 -0
  386. package/src/Tabs/hooks/useTabsRoot.ts +29 -5
  387. package/src/Tabs/hooks/useTabsTrigger.ts +7 -2
  388. package/src/Tabs/types.ts +29 -4
  389. package/src/Textarea/Textarea.tsx +9 -1
  390. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +5 -0
  391. package/src/Textarea/types.ts +20 -3
  392. package/src/Toggle/Toggle.tsx +51 -13
  393. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +5 -0
  394. package/src/Toggle/types.ts +54 -4
  395. package/src/ToggleGroup/README.md +4 -4
  396. package/src/ToggleGroup/ToggleGroup.tsx +14 -1
  397. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +20 -0
  398. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +9 -0
  399. package/src/ToggleGroup/__tests__/ToggleGroup.dynamic-items.test.tsx +70 -0
  400. package/src/ToggleGroup/__tests__/ToggleGroup.error-handling.test.tsx +23 -0
  401. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +23 -1
  402. package/src/ToggleGroup/__tests__/ToggleGroup.ref-forwarding.test.tsx +58 -0
  403. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +21 -1
  404. package/src/ToggleGroup/types.ts +55 -2
  405. package/src/Tooltip/README.md +7 -7
  406. package/src/Tooltip/Tooltip.tsx +100 -22
  407. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +50 -0
  408. package/src/Tooltip/__tests__/Tooltip.error-handling.test.tsx +37 -0
  409. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  410. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +134 -0
  411. package/src/Tooltip/__tests__/Tooltip.skip-delay.test.tsx +216 -0
  412. package/src/Tooltip/__tests__/Tooltip.timer-coordination.test.tsx +223 -0
  413. package/src/Tooltip/hooks/__tests__/useTooltipProvider.test.tsx +40 -0
  414. package/src/Tooltip/hooks/useTooltipContent.ts +3 -0
  415. package/src/Tooltip/hooks/useTooltipProvider.ts +46 -8
  416. package/src/Tooltip/hooks/useTooltipRoot.ts +30 -6
  417. package/src/Tooltip/types.ts +89 -8
  418. package/src/Tree/README.md +69 -5
  419. package/src/Tree/Tree.tsx +188 -34
  420. package/src/Tree/__tests__/Tree.styled-wrapper.test.tsx +159 -0
  421. package/src/Tree/hooks/useTreePath.ts +33 -1
  422. package/src/Tree/index.ts +6 -1
  423. package/src/Tree/types.ts +54 -12
  424. package/src/Tree/utils.ts +46 -4
  425. package/src/VisuallyHidden/VisuallyHidden.tsx +33 -8
  426. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +15 -1
  427. package/src/VisuallyHidden/types.ts +11 -1
  428. package/src/hooks/index.ts +1 -0
  429. package/src/hooks/useTypeahead.ts +95 -0
  430. package/src/index.ts +10 -0
  431. package/src/test/intersectionObserverPolyfill.ts +7 -0
  432. package/src/test/resizeObserverPolyfill.ts +80 -0
  433. package/src/useLocalStorage/__tests__/useLocalStorage.reading.test.tsx +31 -0
  434. package/src/useLocalStorage/__tests__/useLocalStorage.resilience.test.tsx +97 -0
  435. package/src/useLocalStorage/__tests__/useLocalStorage.ssr.test.tsx +20 -0
  436. package/src/useLocalStorage/__tests__/useLocalStorage.writing.test.tsx +72 -0
  437. package/src/useLocalStorage/index.ts +1 -0
  438. package/src/useLocalStorage/useLocalStorage.ts +140 -0
  439. package/src/useMediaQuery/README.md +66 -0
  440. package/src/useMediaQuery/__tests__/useMediaQuery.matching.test.tsx +42 -0
  441. package/src/useMediaQuery/__tests__/useMediaQuery.ssr.test.tsx +19 -0
  442. package/src/useMediaQuery/__tests__/useMediaQuery.subscription.test.tsx +89 -0
  443. package/src/useMediaQuery/index.ts +1 -0
  444. package/src/useMediaQuery/useMediaQuery.ts +49 -0
  445. package/src/usePagination/README.md +170 -0
  446. package/src/usePagination/__tests__/usePagination.navigation.test.tsx +109 -0
  447. package/src/usePagination/__tests__/usePagination.page-count.test.tsx +56 -0
  448. package/src/usePagination/__tests__/usePagination.page-state.test.tsx +87 -0
  449. package/src/usePagination/__tests__/usePagination.range.test.tsx +82 -0
  450. package/src/usePagination/__tests__/usePagination.slice.test.tsx +59 -0
  451. package/src/usePagination/__tests__/usePagination.windowing.test.tsx +68 -0
  452. package/src/usePagination/index.ts +1 -0
  453. package/src/usePagination/usePagination.ts +292 -0
  454. package/src/utils/focusable.ts +15 -0
  455. package/src/utils/getKeyToActionMap.ts +1 -1
  456. package/src/utils/index.ts +1 -0
@@ -1,7 +1,34 @@
1
- import { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from "react";
1
+ import {
2
+ DragEvent,
3
+ KeyboardEvent,
4
+ MouseEvent,
5
+ PointerEvent,
6
+ useCallback,
7
+ useEffect,
8
+ useMemo,
9
+ useRef,
10
+ } from "react";
2
11
 
3
12
  import { useCarouselContext } from "./useCarouselContext";
4
13
 
14
+ // A pointer movement below this many pixels (along the scroll axis) is
15
+ // still a click, not a drag — so a link/button inside a slide keeps
16
+ // working under the pointer that just tapped it.
17
+ const DRAG_THRESHOLD_PX = 3;
18
+
19
+ // Amplifies the pointer delta applied to scrollLeft/scrollTop during a
20
+ // drag. A slide can be many hundreds of pixels wide, and a literal 1:1
21
+ // ratio means dragging the slide's full width to move through it once —
22
+ // far more physical movement than the gesture should cost. 2x means a
23
+ // drag needs to cover only half the on-screen distance to produce the
24
+ // same scroll, without introducing momentum (the multiplier only scales
25
+ // the *tracked* delta; there's still no motion after release).
26
+ const DRAG_SENSITIVITY = 2;
27
+
28
+ function clamp(value: number, min: number, max: number): number {
29
+ return Math.min(Math.max(value, min), max);
30
+ }
31
+
5
32
  /**
6
33
  * Owns the Viewport-side scroll-state sync — bidirectional.
7
34
  *
@@ -41,7 +68,11 @@ export function useCarouselViewport() {
41
68
  const {
42
69
  slidesRef,
43
70
  slideKeys,
71
+ slidesPerPage,
44
72
  effectiveSlidesPerMove,
73
+ maxOffset,
74
+ currentPageOffset,
75
+ pageForSlideIndex,
45
76
  totalPages,
46
77
  currentPage,
47
78
  goTo,
@@ -51,11 +82,29 @@ export function useCarouselViewport() {
51
82
  canGoPrevious,
52
83
  transition,
53
84
  snapAlign,
85
+ orientation,
86
+ loop,
87
+ allowMouseDrag,
88
+ onDragStatusChange,
89
+ onOverscrollStatusChange,
90
+ inViewThreshold,
54
91
  refreshTick,
55
92
  visibleSlideIndicesRef,
56
93
  setSlideInView,
57
94
  isProgrammaticScrollRef,
95
+ instantScrollRef,
96
+ setDragging,
97
+ setOverscrolling,
98
+ setScrollProgress,
99
+ setSlideProgress,
58
100
  } = useCarouselContext();
101
+ // The observer's own `threshold` option accepts a number or number[]
102
+ // as-is; for the "in view" cutoff a single number is used directly, and
103
+ // an array uses its highest value (the strictest crossing) — matching
104
+ // the single-boolean semantics `isInView`/the IO page-drive fallback need.
105
+ const inViewCutoff = Array.isArray(inViewThreshold)
106
+ ? Math.max(...inViewThreshold)
107
+ : inViewThreshold;
59
108
  const internalRef = useRef<HTMLDivElement>(null);
60
109
  // Set to true by the scrollsnapchange handler and the IntersectionObserver
61
110
  // callback before they call goTo(), so the scroll effect knows the page
@@ -70,6 +119,204 @@ export function useCarouselViewport() {
70
119
  internalRef.current = node;
71
120
  }, []);
72
121
 
122
+ // Mouse click-and-drag scrolling. Opt-in via `allowMouseDrag` (default
123
+ // false) — an unconditionally-on drag could conflict with a consumer's own
124
+ // drag-sensitive slide content (a nested carousel, a draggable card, a
125
+ // canvas). When enabled, tracks the pointer at DRAG_SENSITIVITY× (no
126
+ // momentum — the multiplier only scales the tracked delta, motion still
127
+ // stops dead on release) by writing scrollLeft/scrollTop directly during
128
+ // the drag; release lets the
129
+ // existing scroll-snap-type settle to the nearest slide, which the
130
+ // scrollsnapchange sync above already picks up for free — no extra "scroll
131
+ // → state" wiring needed here. `dragStateRef.current.dragging` only flips
132
+ // true once the pointer clears DRAG_THRESHOLD_PX, so a plain click (no
133
+ // capture, no preventDefault) still reaches a link/button under the
134
+ // pointer untouched. The delta is always computed against the *start*
135
+ // scroll position (not incrementally from the last move), and
136
+ // scrollLeft/scrollTop is direction-mode-agnostic — dragging right always
137
+ // reveals content that was to the left, in both LTR and the
138
+ // now-standardized negative-scrollLeft RTL convention — so no RTL
139
+ // special-casing is needed.
140
+ const dragStateRef = useRef<{
141
+ pointerId: number;
142
+ startClient: number;
143
+ startScroll: number;
144
+ dragging: boolean;
145
+ // Set while the drag is pushing past a boundary the carousel has
146
+ // already reached — see the overscroll detection in onPointerMove.
147
+ overscrollEdge: "start" | "end" | null;
148
+ } | null>(null);
149
+ // Browsers still synthesize a click at the release point after a drag
150
+ // unless it's suppressed — set once a drag crosses the threshold, consumed
151
+ // (and cleared) by the next click, so a link/button under the release
152
+ // point doesn't fire.
153
+ const suppressNextClickRef = useRef(false);
154
+
155
+ const onPointerDown = useCallback(
156
+ (event: PointerEvent<HTMLDivElement>) => {
157
+ if (
158
+ !allowMouseDrag ||
159
+ event.pointerType !== "mouse" ||
160
+ transition !== "slide"
161
+ ) {
162
+ return;
163
+ }
164
+ // Guaranteed populated — this handler only ever fires as a React
165
+ // event on the very element viewportRef attached to.
166
+ const viewport = internalRef.current!;
167
+ const vertical = orientation === "vertical";
168
+ dragStateRef.current = {
169
+ pointerId: event.pointerId,
170
+ startClient: vertical ? event.clientY : event.clientX,
171
+ startScroll: vertical ? viewport.scrollTop : viewport.scrollLeft,
172
+ dragging: false,
173
+ overscrollEdge: null,
174
+ };
175
+ },
176
+ [allowMouseDrag, orientation, transition],
177
+ );
178
+
179
+ // A real <img>/<a> inside a slide is natively draggable, and starting a
180
+ // drag over one fires the browser's own HTML5 drag-and-drop (the
181
+ // semi-transparent ghost that follows the cursor) — which visually
182
+ // competes with the custom pointer-based drag-to-scroll below. Only
183
+ // suppressed when allowMouseDrag is opted into; a consumer who hasn't
184
+ // enabled our drag-to-scroll keeps native image/link dragging untouched.
185
+ const onDragStart = useCallback(
186
+ (event: DragEvent<HTMLDivElement>) => {
187
+ if (!allowMouseDrag) return;
188
+ event.preventDefault();
189
+ },
190
+ [allowMouseDrag],
191
+ );
192
+
193
+ const onPointerMove = useCallback(
194
+ (event: PointerEvent<HTMLDivElement>) => {
195
+ const drag = dragStateRef.current;
196
+ if (!drag || event.pointerId !== drag.pointerId) return;
197
+ // Guaranteed populated (see onPointerDown).
198
+ const viewport = internalRef.current!;
199
+ const vertical = orientation === "vertical";
200
+ const clientPos = vertical ? event.clientY : event.clientX;
201
+ const delta = clientPos - drag.startClient;
202
+
203
+ if (!drag.dragging) {
204
+ if (Math.abs(delta) < DRAG_THRESHOLD_PX) return;
205
+ drag.dragging = true;
206
+ suppressNextClickRef.current = true;
207
+ viewport.setPointerCapture?.(drag.pointerId);
208
+ viewport.setAttribute("data-dragging", "");
209
+ setDragging(true);
210
+ onDragStatusChange?.({
211
+ type: "dragging.start",
212
+ page: currentPage,
213
+ isDragging: true,
214
+ });
215
+ } else {
216
+ onDragStatusChange?.({
217
+ type: "dragging",
218
+ page: currentPage,
219
+ isDragging: true,
220
+ });
221
+ }
222
+
223
+ // Overscroll: dragging further past a boundary the carousel has
224
+ // already reached. Positive delta drags toward the start edge,
225
+ // negative delta toward the end edge — see the scrollLeft/scrollTop
226
+ // assignment below (the same convention the regular drag scroll
227
+ // uses). Only meaningful once the drag threshold is crossed
228
+ // (drag.dragging, asserted above).
229
+ const overscrollEdge =
230
+ delta > 0 && !canGoPrevious
231
+ ? "start"
232
+ : delta < 0 && !canGoNext
233
+ ? "end"
234
+ : null;
235
+ if (overscrollEdge) {
236
+ const amount = Math.abs(delta) * DRAG_SENSITIVITY;
237
+ const type = drag.overscrollEdge ? "overscroll" : "overscroll.start";
238
+ drag.overscrollEdge = overscrollEdge;
239
+ setOverscrolling(true);
240
+ viewport.setAttribute("data-overscroll", overscrollEdge);
241
+ onOverscrollStatusChange?.({
242
+ type,
243
+ edge: overscrollEdge,
244
+ source: "drag",
245
+ amount,
246
+ page: currentPage,
247
+ });
248
+ } else if (drag.overscrollEdge) {
249
+ const endedEdge = drag.overscrollEdge;
250
+ drag.overscrollEdge = null;
251
+ setOverscrolling(false);
252
+ viewport.removeAttribute("data-overscroll");
253
+ onOverscrollStatusChange?.({
254
+ type: "overscroll.end",
255
+ edge: endedEdge,
256
+ source: "drag",
257
+ amount: 0,
258
+ page: currentPage,
259
+ });
260
+ }
261
+
262
+ event.preventDefault();
263
+ const nextScroll = drag.startScroll - delta * DRAG_SENSITIVITY;
264
+ if (vertical) viewport.scrollTop = nextScroll;
265
+ else viewport.scrollLeft = nextScroll;
266
+ },
267
+ [
268
+ orientation,
269
+ currentPage,
270
+ onDragStatusChange,
271
+ setDragging,
272
+ canGoNext,
273
+ canGoPrevious,
274
+ onOverscrollStatusChange,
275
+ setOverscrolling,
276
+ ],
277
+ );
278
+
279
+ const endDrag = useCallback(() => {
280
+ const drag = dragStateRef.current;
281
+ if (!drag) return;
282
+ if (drag.dragging) {
283
+ internalRef.current?.releasePointerCapture?.(drag.pointerId);
284
+ internalRef.current?.removeAttribute("data-dragging");
285
+ setDragging(false);
286
+ onDragStatusChange?.({
287
+ type: "dragging.end",
288
+ page: currentPage,
289
+ isDragging: false,
290
+ });
291
+ if (drag.overscrollEdge) {
292
+ internalRef.current?.removeAttribute("data-overscroll");
293
+ setOverscrolling(false);
294
+ onOverscrollStatusChange?.({
295
+ type: "overscroll.end",
296
+ edge: drag.overscrollEdge,
297
+ source: "drag",
298
+ amount: 0,
299
+ page: currentPage,
300
+ });
301
+ }
302
+ }
303
+ dragStateRef.current = null;
304
+ }, [
305
+ currentPage,
306
+ onDragStatusChange,
307
+ setDragging,
308
+ onOverscrollStatusChange,
309
+ setOverscrolling,
310
+ ]);
311
+
312
+ const onClickCapture = useCallback((event: MouseEvent<HTMLDivElement>) => {
313
+ if (suppressNextClickRef.current) {
314
+ suppressNextClickRef.current = false;
315
+ event.preventDefault();
316
+ event.stopPropagation();
317
+ }
318
+ }, []);
319
+
73
320
  // Read prefers-reduced-motion once on mount; choose scrollTo
74
321
  // behavior accordingly so we don't fight the OS-level setting.
75
322
  const scrollBehavior = useMemo<ScrollBehavior>(
@@ -82,10 +329,11 @@ export function useCarouselViewport() {
82
329
 
83
330
  useEffect(() => {
84
331
  // transition="none" hands the visual to consumer CSS; we don't
85
- // touch viewport.scrollTo at all in that mode.
86
- if (transition !== "slide") return;
87
- const firstSlideIndex = currentPage * effectiveSlidesPerMove;
88
- const firstSlideKey = slideKeys[firstSlideIndex];
332
+ // touch viewport.scrollTo at all in that mode. loop="infinite" is driven by
333
+ // the transform engine (useCarouselLoop), not native scroll — this effect
334
+ // stands down for it.
335
+ if (transition !== "slide" || loop === "infinite") return;
336
+ const firstSlideKey = slideKeys[currentPageOffset];
89
337
  // No slides registered yet, or page out of range: nothing to scroll to.
90
338
  if (!firstSlideKey) return;
91
339
 
@@ -109,17 +357,82 @@ export function useCarouselViewport() {
109
357
  // mount, neither of which goes through next() / previous().
110
358
  isProgrammaticScrollRef.current = true;
111
359
 
112
- // Native-first: delegate the horizontal scroll to the browser via
113
- // scrollIntoView rather than computing scrollLeft ourselves. The
114
- // consumer's `scroll-snap-align` then makes the final correction so
115
- // we never fight the snap engine. `inline` maps to snapAlign;
116
- // `block: "nearest"` keeps the page from scrolling vertically.
117
- const targetEl = slidesRef.current!.get(firstSlideKey)!;
118
- targetEl.scrollIntoView({
119
- behavior: scrollBehavior,
120
- inline: snapAlign === "center" ? "center" : "start",
121
- block: "nearest",
122
- });
360
+ // Scroll the *viewport itself* — never `targetEl.scrollIntoView()`, which
361
+ // walks every scrollable ancestor (including the page/window) and so scrolls
362
+ // the whole document when the carousel is off-screen. We read the target
363
+ // slide's `getBoundingClientRect` relative to the viewport and add the delta
364
+ // to the current scroll offset (so the maths is correct mid-scroll), then let
365
+ // the consumer's `scroll-snap-align` make the final correction — we never
366
+ // fight the snap engine. The scroll axis follows `orientation`; the cross axis
367
+ // is untouched, so the viewport never drifts on the other axis. `start` aligns
368
+ // the slide's leading edge to the viewport's; `center` centres it; `end`
369
+ // aligns its trailing edge.
370
+ const targetEl = slidesRef.current!.get(firstSlideKey);
371
+ // A loop-mode switch reparents the slides into / out of the transform track,
372
+ // so a slide can unmount + re-register in the same commit that re-runs this
373
+ // effect: the key is still in slideKeys but its element isn't back in slidesRef
374
+ // yet. Bail; the re-register bumps slideKeys and re-runs this against the
375
+ // settled set. (Same race the --slide-progress effect below already guards.)
376
+ if (!targetEl) return;
377
+ const vertical = orientation === "vertical";
378
+ const viewportRect = viewport.getBoundingClientRect();
379
+ const targetRect = targetEl.getBoundingClientRect();
380
+ const currentScroll = vertical ? viewport.scrollTop : viewport.scrollLeft;
381
+ const delta = vertical
382
+ ? targetRect.top - viewportRect.top
383
+ : targetRect.left - viewportRect.left;
384
+ // The target slide's own snapAlign (Carousel.Slide's `snapAlign` prop,
385
+ // already merged with the root default) is read off the DOM rather than
386
+ // context, since only the slide component knows whether it set a
387
+ // per-slide override. `Carousel.Slide` always publishes data-snap-align
388
+ // on a valid snap-start slide — the only kind `firstSlideKey` ever is
389
+ // (see useCarouselSlide's isSnapStart gate) — so this is never
390
+ // undefined in practice; no fallback needed. `snapAlign` (the root
391
+ // value) stays a dependency below purely so this effect re-runs and
392
+ // re-reads the DOM when it changes without an accompanying page change.
393
+ const effectiveSnapAlign = targetEl.dataset.snapAlign;
394
+ // For center/end, the leftover space must be measured against the whole
395
+ // *page* (every slide sharing this page, edge to edge) rather than just
396
+ // the leading slide's own box — a multi-slide page's other members would
397
+ // otherwise be excluded from the alignment math entirely, undershooting
398
+ // the scroll and leaving the page's trailing slide(s) clipped off-screen.
399
+ // `start` doesn't depend on this (its alignOffset is always 0), but
400
+ // computing it unconditionally costs nothing and keeps the code path
401
+ // uniform. The last member's index is clamped to the actual slide count
402
+ // (slidesPerPage can exceed how many slides really exist).
403
+ const lastMemberIndex = Math.min(
404
+ currentPageOffset + slidesPerPage - 1,
405
+ slideKeys.length - 1,
406
+ );
407
+ // Present whenever targetEl is (a loop-mode reparent unmounts every slide
408
+ // together, so the guard above already caught a mid-reparent run).
409
+ const lastMemberEl = slidesRef.current!.get(slideKeys[lastMemberIndex])!;
410
+ const lastMemberRect = lastMemberEl.getBoundingClientRect();
411
+ // Computed as left/top + width/height, not the rect's own .right/.bottom
412
+ // — real DOMRects derive those the same way, but keeping the maths
413
+ // explicit here matches the plain-object rect fixtures used in tests.
414
+ const pageSpan = vertical
415
+ ? lastMemberRect.top + lastMemberRect.height - targetRect.top
416
+ : lastMemberRect.left + lastMemberRect.width - targetRect.left;
417
+ const leftoverSpace = vertical
418
+ ? viewport.clientHeight - pageSpan
419
+ : viewport.clientWidth - pageSpan;
420
+ const alignOffset =
421
+ effectiveSnapAlign === "center"
422
+ ? leftoverSpace / 2
423
+ : effectiveSnapAlign === "end"
424
+ ? leftoverSpace
425
+ : 0;
426
+ const position = currentScroll + delta - alignOffset;
427
+ // instantScrollRef is a one-shot override set by this specific next() /
428
+ // previous() / goTo() call — consumed (and cleared) immediately so it
429
+ // never leaks into a later page change that didn't request it (e.g. a
430
+ // user swipe or a subsequent plain next()).
431
+ const behavior = instantScrollRef.current ? "instant" : scrollBehavior;
432
+ instantScrollRef.current = false;
433
+ viewport.scrollTo(
434
+ vertical ? { top: position, behavior } : { left: position, behavior },
435
+ );
123
436
 
124
437
  // Clear the programmatic-scroll guard once the animation settles.
125
438
  // `scrollend` is the reliable signal in real browsers; the setTimeout
@@ -142,25 +455,257 @@ export function useCarouselViewport() {
142
455
  }, [
143
456
  transition,
144
457
  snapAlign,
145
- currentPage,
146
- effectiveSlidesPerMove,
458
+ orientation,
459
+ loop,
460
+ currentPageOffset,
461
+ slidesPerPage,
147
462
  slideKeys,
148
463
  slidesRef,
149
464
  refreshTick,
150
465
  scrollBehavior,
151
466
  ]);
152
467
 
468
+ // Keeps every page's leading slide `scroll-margin` extended out to its
469
+ // page's actual last member, so a user's own swipe/wheel/touch — settled
470
+ // entirely by the browser's native scroll-snap, never by the scrollTo
471
+ // effect above — also spans the whole page for center/end alignment
472
+ // instead of just the leading slide's own box (the same fix applied to
473
+ // the programmatic scroll, just expressed as a native snap-area
474
+ // extension rather than a scrollTo offset). A single slide-per-page
475
+ // carousel needs no extension at all, so this is a no-op there. Runs for
476
+ // every page up front (not just the current one), since a fast native
477
+ // fling under `mandatory` snapping can settle on a page the effect above
478
+ // never programmatically targeted.
479
+ useEffect(() => {
480
+ if (transition !== "slide" || slidesPerPage <= 1) return;
481
+ // Guaranteed populated — this effect only runs post-commit, and
482
+ // slidesRef.current is a plain useRef(new Map()) in useCarouselRoot,
483
+ // never null.
484
+ const viewport = internalRef.current!;
485
+ const vertical = orientation === "vertical";
486
+
487
+ const recomputeScrollMargins = () => {
488
+ const slides = slidesRef.current!;
489
+ for (let page = 0; page < totalPages; page++) {
490
+ const offset = Math.min(page * effectiveSlidesPerMove, maxOffset);
491
+ const lastIndex = Math.min(
492
+ offset + slidesPerPage - 1,
493
+ slideKeys.length - 1,
494
+ );
495
+ // Guaranteed populated — any key in slideKeys was just registered
496
+ // into slidesRef in lockstep by useCarouselRoot.registerSlide (see
497
+ // the scroll-position effect above).
498
+ const leadingEl = slides.get(slideKeys[offset])!;
499
+ const lastEl = slides.get(slideKeys[lastIndex])!;
500
+ const leadingRect = leadingEl.getBoundingClientRect();
501
+ const lastRect = lastEl.getBoundingClientRect();
502
+ // Computed as left/top + width/height (not .right/.bottom) to match
503
+ // the plain-object rect fixtures used in tests — real DOMRects
504
+ // derive those the same way.
505
+ const extra = vertical
506
+ ? lastRect.top + lastRect.height - (leadingRect.top + leadingRect.height)
507
+ : lastRect.left + lastRect.width - (leadingRect.left + leadingRect.width);
508
+ leadingEl.style.scrollMarginInlineEnd = vertical ? "" : `${extra}px`;
509
+ leadingEl.style.scrollMarginBlockEnd = vertical ? `${extra}px` : "";
510
+ }
511
+ };
512
+
513
+ recomputeScrollMargins();
514
+ // Equal-share slide sizes derive from the viewport's own width/height, so
515
+ // the px extension above must stay current across resizes, not just
516
+ // page/slide-count changes (which the effect already re-runs for).
517
+ const resizeObserver = new ResizeObserver(recomputeScrollMargins);
518
+ resizeObserver.observe(viewport);
519
+ return () => resizeObserver.disconnect();
520
+ }, [
521
+ transition,
522
+ orientation,
523
+ slidesPerPage,
524
+ effectiveSlidesPerMove,
525
+ maxOffset,
526
+ totalPages,
527
+ slideKeys,
528
+ slidesRef,
529
+ ]);
530
+
531
+ // Continuous scroll-progress signal — a live 0..1 global value plus a
532
+ // per-slide -1..1 center-distance, both mirrored onto CSS custom
533
+ // properties for real-time visual consumption without a React
534
+ // re-render. Unconditional (unlike the scroll-margin effect above,
535
+ // which is meaningless outside `transition: "slide"`) — progress
536
+ // should degrade gracefully to near-zero in `fade` mode rather than
537
+ // not exist at all.
538
+ useEffect(() => {
539
+ const viewport = internalRef.current!;
540
+ const vertical = orientation === "vertical";
541
+ // Coalesces any number of scroll/resize signals within one animation
542
+ // frame into a single recompute — a native `scroll` event can fire many
543
+ // times per frame during momentum scrolling, and each recompute reads
544
+ // getBoundingClientRect() per slide (a forced-layout read), so batching
545
+ // avoids a textbook scroll-jank anti-pattern. `pendingFrame` is a plain
546
+ // closure variable, not a ref — it never needs to outlive this specific
547
+ // effect instance.
548
+ let pendingFrame: number | null = null;
549
+
550
+ const recomputeProgress = () => {
551
+ const maxScroll = vertical
552
+ ? viewport.scrollHeight - viewport.clientHeight
553
+ : viewport.scrollWidth - viewport.clientWidth;
554
+ const scrollPos = vertical ? viewport.scrollTop : viewport.scrollLeft;
555
+ // Math.abs, not the raw signed value — modern browsers standardize
556
+ // RTL scrollLeft on the "negative" convention (0 at the start, down
557
+ // to -maxScroll at the end), so this makes "distance travelled from
558
+ // the start" monotonic in both directions with no dir/
559
+ // getComputedStyle read, mirroring why the drag handler above needs
560
+ // no RTL special-casing either.
561
+ const scrollProgress =
562
+ maxScroll > 0 ? clamp(Math.abs(scrollPos) / maxScroll, 0, 1) : 0;
563
+ setScrollProgress(scrollProgress);
564
+ viewport.style.setProperty("--carousel-progress", String(scrollProgress));
565
+
566
+ const viewportRect = viewport.getBoundingClientRect();
567
+ const viewportHalfSize = vertical
568
+ ? viewportRect.height / 2
569
+ : viewportRect.width / 2;
570
+ const viewportCenter = vertical
571
+ ? viewportRect.top + viewportHalfSize
572
+ : viewportRect.left + viewportHalfSize;
573
+ const slides = slidesRef.current!;
574
+ slideKeys.forEach((key, index) => {
575
+ const slideEl = slides.get(key);
576
+ // A slide can unmount in the same commit that re-runs this effect
577
+ // (orientation is one of its other dependencies) — the removed
578
+ // slide's ref callback deletes it from slidesRef synchronously
579
+ // during the mutation phase, while this effect's own closure still
580
+ // has the pre-drop slideKeys (its slideKeys update hasn't flushed
581
+ // a render yet). Skip the orphaned key; the next slideKeys change
582
+ // re-runs this effect against the settled set. Same race as the
583
+ // IntersectionObserver effect above.
584
+ if (!slideEl) return;
585
+ const slideRect = slideEl.getBoundingClientRect();
586
+ const slideCenter = vertical
587
+ ? slideRect.top + slideRect.height / 2
588
+ : slideRect.left + slideRect.width / 2;
589
+ const slideProgress =
590
+ viewportHalfSize > 0
591
+ ? clamp((slideCenter - viewportCenter) / viewportHalfSize, -1, 1)
592
+ : 0;
593
+ setSlideProgress(index, slideProgress);
594
+ slideEl.style.setProperty("--slide-progress", String(slideProgress));
595
+ });
596
+ };
597
+
598
+ const scheduleRecompute = () => {
599
+ if (pendingFrame !== null) return;
600
+ pendingFrame = requestAnimationFrame(() => {
601
+ pendingFrame = null;
602
+ recomputeProgress();
603
+ });
604
+ };
605
+
606
+ recomputeProgress();
607
+ viewport.addEventListener("scroll", scheduleRecompute, { passive: true });
608
+ const resizeObserver = new ResizeObserver(scheduleRecompute);
609
+ resizeObserver.observe(viewport);
610
+ return () => {
611
+ viewport.removeEventListener("scroll", scheduleRecompute);
612
+ resizeObserver.disconnect();
613
+ if (pendingFrame !== null) cancelAnimationFrame(pendingFrame);
614
+ };
615
+ }, [orientation, slideKeys, slidesRef, setScrollProgress, setSlideProgress]);
616
+
617
+ // Horizontal mouse-wheel translation. A physical scroll wheel (vertical
618
+ // notches, deltaY only) already natively scrolls a vertically-scrollable
619
+ // container, so orientation="vertical" needs nothing here. But browsers
620
+ // only auto-translate a plain vertical wheel to horizontal scroll when
621
+ // Shift is held, so a horizontal (the default) carousel gets no scroll at
622
+ // all from a physical wheel without this. Registered via addEventListener
623
+ // (not the React onWheel prop) with { passive: false } — React attaches
624
+ // wheel listeners as passive by default, which would silently no-op
625
+ // preventDefault() and let the page scroll vertically at the same time.
626
+ // Deliberately stands down whenever deltaX is non-negligible: a trackpad
627
+ // or Magic Mouse horizontal swipe already produces real deltaX and already
628
+ // scrolls a horizontal viewport natively via the same mechanism as touch —
629
+ // this must never fight that, only fill the gap a plain vertical wheel
630
+ // leaves. deltaY is normalized from line/page units to pixels so a
631
+ // physical wheel's larger, fewer DOM_DELTA_LINE ticks feel proportional to
632
+ // a trackpad's many small DOM_DELTA_PIXEL ones.
633
+ useEffect(() => {
634
+ // Infinite is transform-driven, not a scroll container — no wheel-to-scroll.
635
+ if (
636
+ transition !== "slide" ||
637
+ orientation === "vertical" ||
638
+ loop === "infinite"
639
+ )
640
+ return;
641
+ const viewport = internalRef.current!;
642
+
643
+ const handler = (event: WheelEvent) => {
644
+ if (Math.abs(event.deltaX) > 0.5) return;
645
+ let deltaY = event.deltaY;
646
+ if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
647
+ deltaY *= 16;
648
+ } else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
649
+ deltaY *= viewport.clientWidth;
650
+ }
651
+
652
+ // Overscroll: a wheel notch pushing forward/backward while already
653
+ // at that boundary. A single discrete tick — no meaningful distance
654
+ // to report, unlike the continuous drag case.
655
+ if (deltaY > 0 && !canGoNext) {
656
+ onOverscrollStatusChange?.({
657
+ type: "overscroll",
658
+ edge: "end",
659
+ source: "wheel",
660
+ amount: 0,
661
+ page: currentPage,
662
+ });
663
+ } else if (deltaY < 0 && !canGoPrevious) {
664
+ onOverscrollStatusChange?.({
665
+ type: "overscroll",
666
+ edge: "start",
667
+ source: "wheel",
668
+ amount: 0,
669
+ page: currentPage,
670
+ });
671
+ }
672
+
673
+ event.preventDefault();
674
+ viewport.scrollLeft += deltaY;
675
+ };
676
+
677
+ viewport.addEventListener("wheel", handler, { passive: false });
678
+ return () => viewport.removeEventListener("wheel", handler);
679
+ }, [
680
+ transition,
681
+ orientation,
682
+ loop,
683
+ canGoNext,
684
+ canGoPrevious,
685
+ currentPage,
686
+ onOverscrollStatusChange,
687
+ ]);
688
+
153
689
  // User-driven scroll → state. Listen for scrollsnapchange and update
154
690
  // currentPage from the snapped slide's index. The viewport ref is
155
691
  // guaranteed populated post-commit (callback ref runs first).
156
692
  useEffect(() => {
157
- if (transition !== "slide") return;
693
+ // Infinite is transform-driven (useCarouselLoop) with no native scroll, so
694
+ // it emits no scrollsnapchange — this sync is for the scroll-snap modes.
695
+ if (transition !== "slide" || loop === "infinite") return;
158
696
  const viewport = internalRef.current!;
159
697
 
160
698
  const handler = (event: Event) => {
161
- const target = (event as Event & { snapTargetInline?: Element })
162
- .snapTargetInline;
163
-
699
+ // The snapped slide is reported on the axis the viewport scrolls:
700
+ // snapTargetBlock for a vertical carousel, snapTargetInline otherwise.
701
+ const snapEvent = event as Event & {
702
+ snapTargetInline?: Element;
703
+ snapTargetBlock?: Element;
704
+ };
705
+ const target =
706
+ orientation === "vertical"
707
+ ? snapEvent.snapTargetBlock
708
+ : snapEvent.snapTargetInline;
164
709
  // findIndex returns -1 when the snap target isn't one of our
165
710
  // registered slides — e.g. a consumer-wrapped element inside the
166
711
  // viewport. Ignore those; only registered slides drive the page.
@@ -169,7 +714,7 @@ export function useCarouselViewport() {
169
714
  );
170
715
  if (slideIndex < 0) return;
171
716
 
172
- const targetPage = Math.floor(slideIndex / effectiveSlidesPerMove);
717
+ const targetPage = pageForSlideIndex(slideIndex);
173
718
  if (targetPage !== currentPage) {
174
719
  isUserScrollRef.current = true;
175
720
  goTo(targetPage);
@@ -180,17 +725,20 @@ export function useCarouselViewport() {
180
725
  return () => viewport.removeEventListener("scrollsnapchange", handler);
181
726
  }, [
182
727
  transition,
728
+ loop,
729
+ orientation,
183
730
  slideKeys,
184
731
  slidesRef,
185
- effectiveSlidesPerMove,
732
+ pageForSlideIndex,
186
733
  currentPage,
187
734
  goTo,
188
735
  ]);
189
736
 
190
737
  // IntersectionObserver fallback for browsers without scrollsnapchange,
191
738
  // and the source of truth for isInView() on the imperative API. The
192
- // observer fires whenever a slide crosses the 0.6 visibility
193
- // threshold; the lowest-index visible slide derives the active page.
739
+ // observer fires whenever a slide crosses the inViewThreshold visibility
740
+ // threshold (0.6 by default); the lowest-index visible slide derives the
741
+ // active page.
194
742
  useEffect(() => {
195
743
  if (transition !== "slide") return;
196
744
 
@@ -206,55 +754,67 @@ export function useCarouselViewport() {
206
754
 
207
755
  const observer = new IntersectionObserver(
208
756
  (entries) => {
209
- // Both lookups (slideKeys.findIndex → registered key, and the
210
- // slidesRef get → element) are guaranteed to resolve: the
211
- // observer only observes elements registered into slidesRef
212
- // alongside their slideKey, and is disconnected on cleanup
213
- // before slides can unmount.
757
+ // The live observer only observes elements still present in
758
+ // slidesRef (the setup loop skips orphaned keys), and a stale
759
+ // observer is disconnected on cleanup when the effect re-runs, so
760
+ // by the time a callback fires both lookups (slideKeys.findIndex →
761
+ // registered key, and the slidesRef get → element) resolve.
214
762
  for (const entry of entries) {
215
763
  const idx = slideKeys.findIndex(
216
764
  (key) => slidesRef.current!.get(key) === entry.target,
217
765
  );
218
766
  setSlideInView(
219
767
  idx,
220
- entry.isIntersecting && entry.intersectionRatio >= 0.6,
768
+ entry.isIntersecting && entry.intersectionRatio >= inViewCutoff,
221
769
  );
222
770
  }
223
771
 
224
772
  // isInView is updated above regardless; only the page-drive below
225
- // is the fallback that scrollsnapchange supersedes.
226
- if (supportsSnapEvents) return;
773
+ // is the fallback that scrollsnapchange supersedes. Infinite is
774
+ // transform-driven — its page never comes from viewport visibility — so
775
+ // the page-drive stands down there too (the isInView feed above stays).
776
+ if (supportsSnapEvents || loop === "infinite") return;
227
777
 
228
778
  const visible = visibleSlideIndicesRef.current;
229
779
  if (visible.size === 0) return;
230
780
  const firstVisible = Math.min(...visible);
231
- const targetPage = Math.floor(firstVisible / effectiveSlidesPerMove);
781
+ const targetPage = pageForSlideIndex(firstVisible);
232
782
  // Guard: if a programmatic scroll is in flight (e.g. user clicked
233
783
  // NextTrigger and the smooth-scroll animation hasn't settled), the
234
- // IO may still see the old slide as ≥0.6 visible. Calling goTo
235
- // here would undo the navigation, so skip until the flag clears.
784
+ // IO may still see the old slide as ≥ the cutoff visible. Calling
785
+ // goTo here would undo the navigation, so skip until the flag clears.
236
786
  if (targetPage !== currentPage && !isProgrammaticScrollRef.current) {
237
787
  isUserScrollRef.current = true;
238
788
  goTo(targetPage);
239
789
  }
240
790
  },
241
- { threshold: 0.6 },
791
+ { threshold: inViewThreshold },
242
792
  );
243
793
 
244
794
  for (const key of slideKeys) {
245
- observer.observe(slidesRef.current!.get(key)!);
795
+ // A slide can unmount in the same commit that re-runs this effect — a
796
+ // dynamic slide-count drop removes its element from slidesRef during the
797
+ // mutation phase while this effect still closes over the pre-drop
798
+ // slideKeys. Skip the orphaned key; the registerSlide that unmounted it
799
+ // schedules a fresh slideKeys, re-running this effect to observe the
800
+ // settled set.
801
+ const element = slidesRef.current!.get(key);
802
+ if (element) observer.observe(element);
246
803
  }
247
804
 
248
805
  return () => observer.disconnect();
249
806
  }, [
250
807
  transition,
808
+ loop,
251
809
  slideKeys,
252
810
  slidesRef,
253
- effectiveSlidesPerMove,
811
+ pageForSlideIndex,
254
812
  currentPage,
255
813
  goTo,
256
814
  setSlideInView,
257
815
  visibleSlideIndicesRef,
816
+ inViewThreshold,
817
+ inViewCutoff,
258
818
  ]);
259
819
 
260
820
  // Keyboard navigation per the WAI-ARIA Carousel pattern: arrow keys
@@ -269,12 +829,36 @@ export function useCarouselViewport() {
269
829
  // focus inside a slide (e.g. on a link or form control) keeps
270
830
  // its native arrow-key semantics.
271
831
  if (event.target !== event.currentTarget) return;
272
- if (event.key === "ArrowRight") {
832
+ // The paging arrows follow the scroll axis: Down/Up for a vertical
833
+ // carousel, Right/Left otherwise. Home/End are axis-agnostic.
834
+ const forwardKey = orientation === "vertical" ? "ArrowDown" : "ArrowRight";
835
+ const backwardKey = orientation === "vertical" ? "ArrowUp" : "ArrowLeft";
836
+ if (event.key === forwardKey) {
273
837
  event.preventDefault();
274
- if (canGoNext) next();
275
- } else if (event.key === "ArrowLeft") {
838
+ if (canGoNext) {
839
+ next();
840
+ } else {
841
+ onOverscrollStatusChange?.({
842
+ type: "overscroll",
843
+ edge: "end",
844
+ source: "keyboard",
845
+ amount: 0,
846
+ page: currentPage,
847
+ });
848
+ }
849
+ } else if (event.key === backwardKey) {
276
850
  event.preventDefault();
277
- if (canGoPrevious) previous();
851
+ if (canGoPrevious) {
852
+ previous();
853
+ } else {
854
+ onOverscrollStatusChange?.({
855
+ type: "overscroll",
856
+ edge: "start",
857
+ source: "keyboard",
858
+ amount: 0,
859
+ page: currentPage,
860
+ });
861
+ }
278
862
  } else if (event.key === "Home") {
279
863
  event.preventDefault();
280
864
  goTo(0);
@@ -283,8 +867,27 @@ export function useCarouselViewport() {
283
867
  goTo(totalPages - 1);
284
868
  }
285
869
  },
286
- [canGoNext, canGoPrevious, next, previous, goTo, totalPages],
870
+ [
871
+ orientation,
872
+ canGoNext,
873
+ canGoPrevious,
874
+ next,
875
+ previous,
876
+ goTo,
877
+ totalPages,
878
+ currentPage,
879
+ onOverscrollStatusChange,
880
+ ],
287
881
  );
288
882
 
289
- return { viewportRef, onKeyDown };
883
+ return {
884
+ viewportRef,
885
+ onKeyDown,
886
+ onDragStart,
887
+ onPointerDown,
888
+ onPointerMove,
889
+ onPointerUp: endDrag,
890
+ onPointerCancel: endDrag,
891
+ onClickCapture,
892
+ };
290
893
  }