@primitiv-ui/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (585) hide show
  1. package/README.md +79 -0
  2. package/package.json +59 -0
  3. package/src/AccessibleIcon/AccessibleIcon.tsx +40 -0
  4. package/src/AccessibleIcon/README.md +42 -0
  5. package/src/AccessibleIcon/__tests__/AccessibleIcon.test.tsx +47 -0
  6. package/src/AccessibleIcon/index.ts +2 -0
  7. package/src/AccessibleIcon/types.ts +8 -0
  8. package/src/Accordion/Accordion.tsx +412 -0
  9. package/src/Accordion/AccordionContext.ts +12 -0
  10. package/src/Accordion/README.md +202 -0
  11. package/src/Accordion/__tests__/Accordion.asChild.test.tsx +237 -0
  12. package/src/Accordion/__tests__/Accordion.basic-rendering.test.tsx +333 -0
  13. package/src/Accordion/__tests__/Accordion.controlled-state.test.tsx +175 -0
  14. package/src/Accordion/__tests__/Accordion.data-attributes.test.tsx +272 -0
  15. package/src/Accordion/__tests__/Accordion.disabled-items.test.tsx +311 -0
  16. package/src/Accordion/__tests__/Accordion.error-handling.test.tsx +119 -0
  17. package/src/Accordion/__tests__/Accordion.forceMount.test.tsx +119 -0
  18. package/src/Accordion/__tests__/Accordion.keyboard-interaction.test.tsx +736 -0
  19. package/src/Accordion/__tests__/Accordion.mouse-interaction.test.tsx +212 -0
  20. package/src/Accordion/__tests__/Accordion.multiple-mode.test.tsx +90 -0
  21. package/src/Accordion/__tests__/Accordion.reading-direction.test.tsx +139 -0
  22. package/src/Accordion/__tests__/Accordion.uncontrolled-state.test.tsx +154 -0
  23. package/src/Accordion/hooks/index.ts +6 -0
  24. package/src/Accordion/hooks/useAccordionContext.ts +1 -0
  25. package/src/Accordion/hooks/useAccordionHeaderContext.ts +10 -0
  26. package/src/Accordion/hooks/useAccordionItem.ts +22 -0
  27. package/src/Accordion/hooks/useAccordionItemContext.ts +1 -0
  28. package/src/Accordion/hooks/useAccordionRoot.ts +151 -0
  29. package/src/Accordion/hooks/useAccordionTrigger.ts +90 -0
  30. package/src/Accordion/index.ts +1 -0
  31. package/src/Accordion/types.ts +81 -0
  32. package/src/Alert/Alert.tsx +43 -0
  33. package/src/Alert/README.md +54 -0
  34. package/src/Alert/__tests__/Alert.test.tsx +28 -0
  35. package/src/Alert/index.ts +2 -0
  36. package/src/Alert/types.ts +5 -0
  37. package/src/Avatar/Avatar.tsx +149 -0
  38. package/src/Avatar/AvatarContext.ts +20 -0
  39. package/src/Avatar/README.md +116 -0
  40. package/src/Avatar/__tests__/Avatar.asChild.test.tsx +53 -0
  41. package/src/Avatar/__tests__/Avatar.basic-rendering.test.tsx +14 -0
  42. package/src/Avatar/__tests__/Avatar.error-handling.test.tsx +30 -0
  43. package/src/Avatar/__tests__/Avatar.fallback.test.tsx +75 -0
  44. package/src/Avatar/__tests__/Avatar.image-loading.test.tsx +81 -0
  45. package/src/Avatar/hooks/index.ts +2 -0
  46. package/src/Avatar/hooks/useAvatarContext.ts +1 -0
  47. package/src/Avatar/hooks/useAvatarImage.ts +40 -0
  48. package/src/Avatar/index.ts +3 -0
  49. package/src/Avatar/types.ts +44 -0
  50. package/src/Breadcrumb/Breadcrumb.tsx +234 -0
  51. package/src/Breadcrumb/README.md +111 -0
  52. package/src/Breadcrumb/__tests__/Breadcrumb.asChild.test.tsx +33 -0
  53. package/src/Breadcrumb/__tests__/Breadcrumb.basic-rendering.test.tsx +132 -0
  54. package/src/Breadcrumb/index.ts +2 -0
  55. package/src/Breadcrumb/types.ts +22 -0
  56. package/src/Button/Button.tsx +95 -0
  57. package/src/Button/README.md +112 -0
  58. package/src/Button/__tests__/Button.asChild.test.tsx +91 -0
  59. package/src/Button/__tests__/Button.basic-rendering.test.tsx +126 -0
  60. package/src/Button/__tests__/Button.contract.test.tsx +72 -0
  61. package/src/Button/__tests__/Button.disabled.test.tsx +52 -0
  62. package/src/Button/__tests__/Button.icon-usage.test.tsx +57 -0
  63. package/src/Button/__tests__/Button.keyboard-interaction.test.tsx +70 -0
  64. package/src/Button/index.ts +2 -0
  65. package/src/Button/types.ts +8 -0
  66. package/src/Carousel/Carousel.tsx +708 -0
  67. package/src/Carousel/CarouselContext.ts +11 -0
  68. package/src/Carousel/README.md +848 -0
  69. package/src/Carousel/__tests__/Carousel.asChild.test.tsx +178 -0
  70. package/src/Carousel/__tests__/Carousel.auto-play.test.tsx +617 -0
  71. package/src/Carousel/__tests__/Carousel.basic-rendering.test.tsx +569 -0
  72. package/src/Carousel/__tests__/Carousel.controlled-state.test.tsx +137 -0
  73. package/src/Carousel/__tests__/Carousel.error-handling.test.tsx +81 -0
  74. package/src/Carousel/__tests__/Carousel.ids.test.tsx +111 -0
  75. package/src/Carousel/__tests__/Carousel.imperative-api.test.tsx +213 -0
  76. package/src/Carousel/__tests__/Carousel.indicators.test.tsx +560 -0
  77. package/src/Carousel/__tests__/Carousel.intersection-observer.test.tsx +276 -0
  78. package/src/Carousel/__tests__/Carousel.keyboard-navigation.test.tsx +158 -0
  79. package/src/Carousel/__tests__/Carousel.play-pause.test.tsx +232 -0
  80. package/src/Carousel/__tests__/Carousel.prev-next.test.tsx +68 -0
  81. package/src/Carousel/__tests__/Carousel.reduced-motion.test.tsx +49 -0
  82. package/src/Carousel/__tests__/Carousel.refresh-progress.test.tsx +87 -0
  83. package/src/Carousel/__tests__/Carousel.scroll-snap-change.test.tsx +179 -0
  84. package/src/Carousel/__tests__/Carousel.scroll-sync.test.tsx +109 -0
  85. package/src/Carousel/__tests__/Carousel.slides-per-move.test.tsx +151 -0
  86. package/src/Carousel/__tests__/Carousel.slides-per-page.test.tsx +183 -0
  87. package/src/Carousel/__tests__/Carousel.touch-interaction.test.tsx +96 -0
  88. package/src/Carousel/__tests__/Carousel.transition-modes.test.tsx +70 -0
  89. package/src/Carousel/__tests__/Carousel.translations.test.tsx +157 -0
  90. package/src/Carousel/__tests__/Carousel.uncontrolled-state.test.tsx +146 -0
  91. package/src/Carousel/hooks/index.ts +4 -0
  92. package/src/Carousel/hooks/useCarouselContext.ts +13 -0
  93. package/src/Carousel/hooks/useCarouselRoot.ts +450 -0
  94. package/src/Carousel/hooks/useCarouselSlide.ts +45 -0
  95. package/src/Carousel/hooks/useCarouselViewport.ts +290 -0
  96. package/src/Carousel/index.ts +3 -0
  97. package/src/Carousel/types.ts +400 -0
  98. package/src/Checkbox/Checkbox.tsx +228 -0
  99. package/src/Checkbox/CheckboxContext.ts +12 -0
  100. package/src/Checkbox/README.md +156 -0
  101. package/src/Checkbox/__tests__/Checkbox.asChild.test.tsx +69 -0
  102. package/src/Checkbox/__tests__/Checkbox.basic-rendering.test.tsx +41 -0
  103. package/src/Checkbox/__tests__/Checkbox.controlled-state.test.tsx +82 -0
  104. package/src/Checkbox/__tests__/Checkbox.disabled.test.tsx +15 -0
  105. package/src/Checkbox/__tests__/Checkbox.indeterminate.test.tsx +82 -0
  106. package/src/Checkbox/__tests__/Checkbox.indicator.test.tsx +117 -0
  107. package/src/Checkbox/__tests__/Checkbox.uncontrolled-state.test.tsx +89 -0
  108. package/src/Checkbox/hooks/index.ts +2 -0
  109. package/src/Checkbox/hooks/useCheckboxContext.ts +1 -0
  110. package/src/Checkbox/hooks/useCheckboxRoot.ts +32 -0
  111. package/src/Checkbox/index.ts +1 -0
  112. package/src/Checkbox/types.ts +33 -0
  113. package/src/CheckboxCard/CheckboxCard.tsx +208 -0
  114. package/src/CheckboxCard/CheckboxCardContext.ts +12 -0
  115. package/src/CheckboxCard/README.md +114 -0
  116. package/src/CheckboxCard/__tests__/CheckboxCard.asChild.test.tsx +54 -0
  117. package/src/CheckboxCard/__tests__/CheckboxCard.basic-rendering.test.tsx +58 -0
  118. package/src/CheckboxCard/__tests__/CheckboxCard.controlled-state.test.tsx +77 -0
  119. package/src/CheckboxCard/__tests__/CheckboxCard.disabled.test.tsx +55 -0
  120. package/src/CheckboxCard/__tests__/CheckboxCard.error-handling.test.tsx +20 -0
  121. package/src/CheckboxCard/__tests__/CheckboxCard.indeterminate.test.tsx +60 -0
  122. package/src/CheckboxCard/__tests__/CheckboxCard.indicator.test.tsx +136 -0
  123. package/src/CheckboxCard/__tests__/CheckboxCard.uncontrolled-state.test.tsx +73 -0
  124. package/src/CheckboxCard/hooks/index.ts +2 -0
  125. package/src/CheckboxCard/hooks/useCheckboxCardContext.ts +1 -0
  126. package/src/CheckboxCard/hooks/useCheckboxCardRoot.ts +30 -0
  127. package/src/CheckboxCard/index.ts +3 -0
  128. package/src/CheckboxCard/types.ts +33 -0
  129. package/src/Collapsible/Collapsible.tsx +316 -0
  130. package/src/Collapsible/CollapsibleContext.ts +7 -0
  131. package/src/Collapsible/README.md +174 -0
  132. package/src/Collapsible/__tests__/Collapsible.asChild.test.tsx +240 -0
  133. package/src/Collapsible/__tests__/Collapsible.basic-rendering.test.tsx +118 -0
  134. package/src/Collapsible/__tests__/Collapsible.controlled-state.test.tsx +134 -0
  135. package/src/Collapsible/__tests__/Collapsible.disabled.test.tsx +132 -0
  136. package/src/Collapsible/__tests__/Collapsible.error-handling.test.tsx +40 -0
  137. package/src/Collapsible/__tests__/Collapsible.forceMount.test.tsx +111 -0
  138. package/src/Collapsible/__tests__/Collapsible.triggerIcon.test.tsx +93 -0
  139. package/src/Collapsible/__tests__/Collapsible.uncontrolled-state.test.tsx +125 -0
  140. package/src/Collapsible/hooks/index.ts +2 -0
  141. package/src/Collapsible/hooks/useCollapsibleRoot.ts +34 -0
  142. package/src/Collapsible/hooks/useCollapsibleTrigger.ts +49 -0
  143. package/src/Collapsible/index.ts +1 -0
  144. package/src/Collapsible/types.ts +48 -0
  145. package/src/ContextMenu/ContextMenu.tsx +1004 -0
  146. package/src/ContextMenu/ContextMenuContentContext.ts +15 -0
  147. package/src/ContextMenu/ContextMenuContext.ts +21 -0
  148. package/src/ContextMenu/ContextMenuGroupContext.ts +8 -0
  149. package/src/ContextMenu/ContextMenuItemIndicatorContext.ts +8 -0
  150. package/src/ContextMenu/ContextMenuRadioGroupContext.ts +9 -0
  151. package/src/ContextMenu/ContextMenuSubContext.ts +15 -0
  152. package/src/ContextMenu/README.md +275 -0
  153. package/src/ContextMenu/__tests__/ContextMenu.asChild.test.tsx +186 -0
  154. package/src/ContextMenu/__tests__/ContextMenu.basic-rendering.test.tsx +39 -0
  155. package/src/ContextMenu/__tests__/ContextMenu.checkbox-item.test.tsx +145 -0
  156. package/src/ContextMenu/__tests__/ContextMenu.error-handling.test.tsx +113 -0
  157. package/src/ContextMenu/__tests__/ContextMenu.group-label.test.tsx +48 -0
  158. package/src/ContextMenu/__tests__/ContextMenu.item-indicator.test.tsx +88 -0
  159. package/src/ContextMenu/__tests__/ContextMenu.item.test.tsx +106 -0
  160. package/src/ContextMenu/__tests__/ContextMenu.keyboard-interaction.test.tsx +172 -0
  161. package/src/ContextMenu/__tests__/ContextMenu.mouse-interaction.test.tsx +227 -0
  162. package/src/ContextMenu/__tests__/ContextMenu.radio-item.test.tsx +127 -0
  163. package/src/ContextMenu/__tests__/ContextMenu.reading-direction.test.tsx +152 -0
  164. package/src/ContextMenu/__tests__/ContextMenu.separator.test.tsx +47 -0
  165. package/src/ContextMenu/__tests__/ContextMenu.state-modes.test.tsx +119 -0
  166. package/src/ContextMenu/__tests__/ContextMenu.sub.test.tsx +262 -0
  167. package/src/ContextMenu/__tests__/ContextMenu.typeahead.test.tsx +89 -0
  168. package/src/ContextMenu/constants.ts +4 -0
  169. package/src/ContextMenu/index.ts +1 -0
  170. package/src/ContextMenu/types.ts +199 -0
  171. package/src/DirectionProvider/DirectionContext.ts +21 -0
  172. package/src/DirectionProvider/DirectionProvider.tsx +31 -0
  173. package/src/DirectionProvider/README.md +62 -0
  174. package/src/DirectionProvider/__tests__/DirectionProvider.test.tsx +29 -0
  175. package/src/DirectionProvider/index.ts +3 -0
  176. package/src/DirectionProvider/types.ts +10 -0
  177. package/src/Divider/Divider.tsx +57 -0
  178. package/src/Divider/README.md +57 -0
  179. package/src/Divider/__tests__/Divider.test.tsx +41 -0
  180. package/src/Divider/index.ts +1 -0
  181. package/src/Divider/types.ts +5 -0
  182. package/src/Dropdown/Dropdown.tsx +842 -0
  183. package/src/Dropdown/DropdownContentContext.ts +15 -0
  184. package/src/Dropdown/DropdownContext.ts +17 -0
  185. package/src/Dropdown/DropdownGroupContext.ts +8 -0
  186. package/src/Dropdown/DropdownItemIndicatorContext.ts +13 -0
  187. package/src/Dropdown/DropdownRadioGroupContext.ts +9 -0
  188. package/src/Dropdown/DropdownSubContext.ts +15 -0
  189. package/src/Dropdown/README.md +284 -0
  190. package/src/Dropdown/__tests__/Dropdown.asChild.test.tsx +286 -0
  191. package/src/Dropdown/__tests__/Dropdown.basic-rendering.test.tsx +43 -0
  192. package/src/Dropdown/__tests__/Dropdown.checkbox-item.test.tsx +121 -0
  193. package/src/Dropdown/__tests__/Dropdown.disabled.test.tsx +143 -0
  194. package/src/Dropdown/__tests__/Dropdown.error-handling.test.tsx +85 -0
  195. package/src/Dropdown/__tests__/Dropdown.group-label.test.tsx +68 -0
  196. package/src/Dropdown/__tests__/Dropdown.item-indicator.test.tsx +260 -0
  197. package/src/Dropdown/__tests__/Dropdown.item.test.tsx +72 -0
  198. package/src/Dropdown/__tests__/Dropdown.keyboard-edge-cases.test.tsx +77 -0
  199. package/src/Dropdown/__tests__/Dropdown.keyboard-interaction.test.tsx +310 -0
  200. package/src/Dropdown/__tests__/Dropdown.mouse-interaction.test.tsx +347 -0
  201. package/src/Dropdown/__tests__/Dropdown.radio-item.test.tsx +134 -0
  202. package/src/Dropdown/__tests__/Dropdown.reading-direction.test.tsx +153 -0
  203. package/src/Dropdown/__tests__/Dropdown.separator.test.tsx +46 -0
  204. package/src/Dropdown/__tests__/Dropdown.state-modes.test.tsx +100 -0
  205. package/src/Dropdown/__tests__/Dropdown.sub.test.tsx +185 -0
  206. package/src/Dropdown/__tests__/Dropdown.trigger.test.tsx +110 -0
  207. package/src/Dropdown/__tests__/Dropdown.typeahead.test.tsx +133 -0
  208. package/src/Dropdown/constants.ts +4 -0
  209. package/src/Dropdown/hooks/index.ts +9 -0
  210. package/src/Dropdown/hooks/useCloseSiblingSub.ts +13 -0
  211. package/src/Dropdown/hooks/useDropdownContent.ts +162 -0
  212. package/src/Dropdown/hooks/useDropdownContext.ts +1 -0
  213. package/src/Dropdown/hooks/useDropdownGroup.ts +18 -0
  214. package/src/Dropdown/hooks/useDropdownItem.ts +49 -0
  215. package/src/Dropdown/hooks/useDropdownLabel.ts +15 -0
  216. package/src/Dropdown/hooks/useDropdownRoot.ts +57 -0
  217. package/src/Dropdown/hooks/useDropdownSubContext.ts +1 -0
  218. package/src/Dropdown/hooks/useDropdownTrigger.ts +31 -0
  219. package/src/Dropdown/index.ts +1 -0
  220. package/src/Dropdown/types.ts +200 -0
  221. package/src/EmptyState/EmptyState.tsx +245 -0
  222. package/src/EmptyState/README.md +129 -0
  223. package/src/EmptyState/__tests__/EmptyState.Actions.test.tsx +32 -0
  224. package/src/EmptyState/__tests__/EmptyState.Description.test.tsx +30 -0
  225. package/src/EmptyState/__tests__/EmptyState.Media.test.tsx +34 -0
  226. package/src/EmptyState/__tests__/EmptyState.Root.test.tsx +28 -0
  227. package/src/EmptyState/__tests__/EmptyState.Title.test.tsx +26 -0
  228. package/src/EmptyState/index.ts +2 -0
  229. package/src/EmptyState/types.ts +21 -0
  230. package/src/Field/Field.tsx +239 -0
  231. package/src/Field/FieldContext.ts +22 -0
  232. package/src/Field/README.md +167 -0
  233. package/src/Field/__tests__/Field.asChild.test.tsx +83 -0
  234. package/src/Field/__tests__/Field.basic-rendering.test.tsx +104 -0
  235. package/src/Field/__tests__/Field.state-cascade.test.tsx +75 -0
  236. package/src/Field/hooks/index.ts +2 -0
  237. package/src/Field/hooks/useFieldContext.ts +1 -0
  238. package/src/Field/hooks/useFieldProps.ts +57 -0
  239. package/src/Field/index.ts +2 -0
  240. package/src/Field/types.ts +33 -0
  241. package/src/Fieldset/Fieldset.tsx +104 -0
  242. package/src/Fieldset/README.md +74 -0
  243. package/src/Fieldset/__tests__/Fieldset.basic-rendering.test.tsx +81 -0
  244. package/src/Fieldset/__tests__/Fieldset.disabled.test.tsx +41 -0
  245. package/src/Fieldset/index.ts +2 -0
  246. package/src/Fieldset/types.ts +5 -0
  247. package/src/Input/Input.tsx +120 -0
  248. package/src/Input/README.md +180 -0
  249. package/src/Input/__tests__/Input.asChild.test.tsx +85 -0
  250. package/src/Input/__tests__/Input.basic-rendering.test.tsx +118 -0
  251. package/src/Input/__tests__/Input.disabled.test.tsx +49 -0
  252. package/src/Input/__tests__/Input.field-integration.test.tsx +148 -0
  253. package/src/Input/index.ts +2 -0
  254. package/src/Input/types.ts +7 -0
  255. package/src/InputGroup/InputGroup.tsx +228 -0
  256. package/src/InputGroup/README.md +178 -0
  257. package/src/InputGroup/__tests__/InputGroup.asChild.test.tsx +109 -0
  258. package/src/InputGroup/__tests__/InputGroup.basic-rendering.test.tsx +106 -0
  259. package/src/InputGroup/index.ts +2 -0
  260. package/src/InputGroup/types.ts +13 -0
  261. package/src/MillerColumns/MillerColumns.tsx +329 -0
  262. package/src/MillerColumns/MillerColumnsContext.ts +25 -0
  263. package/src/MillerColumns/README.md +278 -0
  264. package/src/MillerColumns/__tests__/MillerColumns.aria.test.tsx +82 -0
  265. package/src/MillerColumns/__tests__/MillerColumns.asChild.test.tsx +106 -0
  266. package/src/MillerColumns/__tests__/MillerColumns.auto-scroll.test.tsx +68 -0
  267. package/src/MillerColumns/__tests__/MillerColumns.basic-rendering.test.tsx +52 -0
  268. package/src/MillerColumns/__tests__/MillerColumns.column-projection.test.tsx +161 -0
  269. package/src/MillerColumns/__tests__/MillerColumns.controlled-state.test.tsx +90 -0
  270. package/src/MillerColumns/__tests__/MillerColumns.data-attributes.test.tsx +77 -0
  271. package/src/MillerColumns/__tests__/MillerColumns.disabled-items.test.tsx +65 -0
  272. package/src/MillerColumns/__tests__/MillerColumns.error-handling.test.tsx +57 -0
  273. package/src/MillerColumns/__tests__/MillerColumns.fixtures.ts +15 -0
  274. package/src/MillerColumns/__tests__/MillerColumns.item-indicator.test.tsx +57 -0
  275. package/src/MillerColumns/__tests__/MillerColumns.keyboard-interaction.test.tsx +181 -0
  276. package/src/MillerColumns/__tests__/MillerColumns.preview-panel.test.tsx +47 -0
  277. package/src/MillerColumns/__tests__/MillerColumns.resize.test.tsx +137 -0
  278. package/src/MillerColumns/__tests__/MillerColumns.roving-tabindex.test.tsx +91 -0
  279. package/src/MillerColumns/__tests__/MillerColumns.selection.test.tsx +54 -0
  280. package/src/MillerColumns/__tests__/MillerColumns.uncontrolled-state.test.tsx +70 -0
  281. package/src/MillerColumns/hooks/index.ts +7 -0
  282. package/src/MillerColumns/hooks/useMillerColumnsColumn.ts +23 -0
  283. package/src/MillerColumns/hooks/useMillerColumnsColumnContext.ts +1 -0
  284. package/src/MillerColumns/hooks/useMillerColumnsContext.ts +1 -0
  285. package/src/MillerColumns/hooks/useMillerColumnsItem.ts +157 -0
  286. package/src/MillerColumns/hooks/useMillerColumnsItemContext.ts +1 -0
  287. package/src/MillerColumns/hooks/useMillerColumnsResizeHandle.ts +76 -0
  288. package/src/MillerColumns/hooks/useMillerColumnsRoot.ts +0 -0
  289. package/src/MillerColumns/index.ts +3 -0
  290. package/src/MillerColumns/types.ts +93 -0
  291. package/src/MillerColumns/useMillerColumnsSelection.ts +31 -0
  292. package/src/MillerColumns/utils.ts +75 -0
  293. package/src/Modal/Modal.tsx +474 -0
  294. package/src/Modal/ModalContext.ts +13 -0
  295. package/src/Modal/README.md +207 -0
  296. package/src/Modal/__tests__/Modal.accessibility.test.tsx +167 -0
  297. package/src/Modal/__tests__/Modal.asChild.test.tsx +162 -0
  298. package/src/Modal/__tests__/Modal.click-outside.test.tsx +115 -0
  299. package/src/Modal/__tests__/Modal.content.test.tsx +193 -0
  300. package/src/Modal/__tests__/Modal.controlled-state.test.tsx +120 -0
  301. package/src/Modal/__tests__/Modal.error-handling.test.tsx +30 -0
  302. package/src/Modal/__tests__/Modal.escape-hatches.test.tsx +99 -0
  303. package/src/Modal/__tests__/Modal.imperative-api.test.tsx +119 -0
  304. package/src/Modal/__tests__/Modal.nested.test.tsx +106 -0
  305. package/src/Modal/__tests__/Modal.overlay.test.tsx +99 -0
  306. package/src/Modal/__tests__/Modal.portal.test.tsx +90 -0
  307. package/src/Modal/__tests__/Modal.presence.test.tsx +111 -0
  308. package/src/Modal/__tests__/Modal.trigger.test.tsx +70 -0
  309. package/src/Modal/__tests__/Modal.uncontrolled-state.test.tsx +72 -0
  310. package/src/Modal/__tests__/dialog-polyfill.ts +40 -0
  311. package/src/Modal/hooks/index.ts +4 -0
  312. package/src/Modal/hooks/useModalContent.ts +62 -0
  313. package/src/Modal/hooks/useModalContext.ts +1 -0
  314. package/src/Modal/hooks/useModalRoot.ts +81 -0
  315. package/src/Modal/hooks/useModalTrigger.ts +25 -0
  316. package/src/Modal/index.ts +3 -0
  317. package/src/Modal/types.ts +76 -0
  318. package/src/Portal/Portal.tsx +28 -0
  319. package/src/Portal/README.md +70 -0
  320. package/src/Portal/__tests__/Portal.basic-rendering.test.tsx +17 -0
  321. package/src/Portal/index.ts +2 -0
  322. package/src/Portal/types.ts +6 -0
  323. package/src/Progress/Progress.tsx +178 -0
  324. package/src/Progress/ProgressContext.ts +15 -0
  325. package/src/Progress/README.md +112 -0
  326. package/src/Progress/__tests__/Progress.asChild.test.tsx +37 -0
  327. package/src/Progress/__tests__/Progress.basic-rendering.test.tsx +65 -0
  328. package/src/Progress/__tests__/Progress.error-handling.test.tsx +40 -0
  329. package/src/Progress/__tests__/Progress.fixtures.ts +7 -0
  330. package/src/Progress/__tests__/Progress.value.test.tsx +83 -0
  331. package/src/Progress/hooks/index.ts +2 -0
  332. package/src/Progress/hooks/useProgressContext.ts +1 -0
  333. package/src/Progress/hooks/useProgressRoot.ts +45 -0
  334. package/src/Progress/index.ts +3 -0
  335. package/src/Progress/types.ts +43 -0
  336. package/src/RadioCard/README.md +133 -0
  337. package/src/RadioCard/RadioCard.tsx +334 -0
  338. package/src/RadioCard/RadioCardContext.ts +23 -0
  339. package/src/RadioCard/RadioCardItemContext.ts +10 -0
  340. package/src/RadioCard/__tests__/RadioCard.asChild.test.tsx +76 -0
  341. package/src/RadioCard/__tests__/RadioCard.basic-rendering.test.tsx +87 -0
  342. package/src/RadioCard/__tests__/RadioCard.controlled-state.test.tsx +107 -0
  343. package/src/RadioCard/__tests__/RadioCard.disabled-items.test.tsx +61 -0
  344. package/src/RadioCard/__tests__/RadioCard.error-handling.test.tsx +35 -0
  345. package/src/RadioCard/__tests__/RadioCard.indicator.test.tsx +119 -0
  346. package/src/RadioCard/__tests__/RadioCard.keyboard-interaction.test.tsx +158 -0
  347. package/src/RadioCard/__tests__/RadioCard.orientation.test.tsx +90 -0
  348. package/src/RadioCard/__tests__/RadioCard.reading-direction.test.tsx +65 -0
  349. package/src/RadioCard/__tests__/RadioCard.uncontrolled-state.test.tsx +108 -0
  350. package/src/RadioCard/hooks/index.ts +3 -0
  351. package/src/RadioCard/hooks/useRadioCardContext.ts +1 -0
  352. package/src/RadioCard/hooks/useRadioCardItemContext.ts +1 -0
  353. package/src/RadioCard/hooks/useRadioCardRoot.ts +77 -0
  354. package/src/RadioCard/index.ts +4 -0
  355. package/src/RadioCard/types.ts +51 -0
  356. package/src/RadioGroup/README.md +185 -0
  357. package/src/RadioGroup/RadioGroup.tsx +353 -0
  358. package/src/RadioGroup/RadioGroupContext.ts +23 -0
  359. package/src/RadioGroup/RadioGroupItemContext.ts +10 -0
  360. package/src/RadioGroup/__tests__/RadioGroup.asChild.test.tsx +105 -0
  361. package/src/RadioGroup/__tests__/RadioGroup.basic-rendering.test.tsx +72 -0
  362. package/src/RadioGroup/__tests__/RadioGroup.controlled-state.test.tsx +109 -0
  363. package/src/RadioGroup/__tests__/RadioGroup.disabled-keydown-guards.test.tsx +68 -0
  364. package/src/RadioGroup/__tests__/RadioGroup.disabled.test.tsx +79 -0
  365. package/src/RadioGroup/__tests__/RadioGroup.error-handling.test.tsx +33 -0
  366. package/src/RadioGroup/__tests__/RadioGroup.indicator.test.tsx +85 -0
  367. package/src/RadioGroup/__tests__/RadioGroup.keyboard-interaction.test.tsx +135 -0
  368. package/src/RadioGroup/__tests__/RadioGroup.orientation.test.tsx +90 -0
  369. package/src/RadioGroup/__tests__/RadioGroup.reading-direction.test.tsx +65 -0
  370. package/src/RadioGroup/__tests__/RadioGroup.ref-forwarding.test.tsx +45 -0
  371. package/src/RadioGroup/__tests__/RadioGroup.roving-tabindex.test.tsx +105 -0
  372. package/src/RadioGroup/__tests__/RadioGroup.uncontrolled-state.test.tsx +96 -0
  373. package/src/RadioGroup/hooks/index.ts +3 -0
  374. package/src/RadioGroup/hooks/useRadioGroupContext.ts +1 -0
  375. package/src/RadioGroup/hooks/useRadioGroupItemContext.ts +1 -0
  376. package/src/RadioGroup/hooks/useRadioGroupRoot.ts +87 -0
  377. package/src/RadioGroup/index.ts +1 -0
  378. package/src/RadioGroup/types.ts +51 -0
  379. package/src/Select/README.md +203 -0
  380. package/src/Select/Select.tsx +204 -0
  381. package/src/Select/__tests__/Select.asChild.test.tsx +36 -0
  382. package/src/Select/__tests__/Select.basic-rendering.test.tsx +17 -0
  383. package/src/Select/__tests__/Select.controlled-state.test.tsx +69 -0
  384. package/src/Select/__tests__/Select.data-attributes.test.tsx +29 -0
  385. package/src/Select/__tests__/Select.field-integration.test.tsx +150 -0
  386. package/src/Select/__tests__/Select.group.test.tsx +42 -0
  387. package/src/Select/__tests__/Select.placeholder.test.tsx +32 -0
  388. package/src/Select/index.ts +2 -0
  389. package/src/Select/types.ts +89 -0
  390. package/src/SkipNav/README.md +87 -0
  391. package/src/SkipNav/SkipNav.tsx +116 -0
  392. package/src/SkipNav/__tests__/SkipNav.basic-rendering.test.tsx +23 -0
  393. package/src/SkipNav/__tests__/SkipNav.ids.test.tsx +19 -0
  394. package/src/SkipNav/index.ts +1 -0
  395. package/src/SkipNav/types.ts +26 -0
  396. package/src/Slider/README.md +215 -0
  397. package/src/Slider/Slider.tsx +308 -0
  398. package/src/Slider/SliderContext.ts +24 -0
  399. package/src/Slider/__tests__/Slider.asChild.test.tsx +119 -0
  400. package/src/Slider/__tests__/Slider.basic-rendering.test.tsx +157 -0
  401. package/src/Slider/__tests__/Slider.controlled-state.test.tsx +78 -0
  402. package/src/Slider/__tests__/Slider.disabled.test.tsx +82 -0
  403. package/src/Slider/__tests__/Slider.error-handling.test.tsx +45 -0
  404. package/src/Slider/__tests__/Slider.fixtures.ts +53 -0
  405. package/src/Slider/__tests__/Slider.form.test.tsx +67 -0
  406. package/src/Slider/__tests__/Slider.inverted.test.tsx +112 -0
  407. package/src/Slider/__tests__/Slider.keyboard-interaction.test.tsx +118 -0
  408. package/src/Slider/__tests__/Slider.multiple-thumbs.test.tsx +84 -0
  409. package/src/Slider/__tests__/Slider.orientation.test.tsx +101 -0
  410. package/src/Slider/__tests__/Slider.pointer-interaction.test.tsx +205 -0
  411. package/src/Slider/__tests__/Slider.reading-direction.test.tsx +99 -0
  412. package/src/Slider/__tests__/Slider.uncontrolled-state.test.tsx +69 -0
  413. package/src/Slider/__tests__/Slider.value-commit.test.tsx +103 -0
  414. package/src/Slider/hooks/index.ts +3 -0
  415. package/src/Slider/hooks/useSliderContext.ts +1 -0
  416. package/src/Slider/hooks/useSliderRoot.ts +197 -0
  417. package/src/Slider/hooks/useSliderThumb.ts +77 -0
  418. package/src/Slider/index.ts +3 -0
  419. package/src/Slider/types.ts +48 -0
  420. package/src/Slider/utils.ts +155 -0
  421. package/src/Slot/Slot.tsx +158 -0
  422. package/src/Slot/__tests__/Slot.test.tsx +163 -0
  423. package/src/Slot/__tests__/composeEventHandlers.test.ts +74 -0
  424. package/src/Slot/composeEventHandlers.ts +38 -0
  425. package/src/Slot/index.ts +3 -0
  426. package/src/Slot/types.ts +5 -0
  427. package/src/Status/README.md +50 -0
  428. package/src/Status/Status.tsx +44 -0
  429. package/src/Status/__tests__/Status.test.tsx +28 -0
  430. package/src/Status/index.ts +2 -0
  431. package/src/Status/types.ts +5 -0
  432. package/src/Switch/README.md +121 -0
  433. package/src/Switch/Switch.tsx +167 -0
  434. package/src/Switch/SwitchContext.ts +10 -0
  435. package/src/Switch/__tests__/Switch.asChild.test.tsx +56 -0
  436. package/src/Switch/__tests__/Switch.basic-rendering.test.tsx +76 -0
  437. package/src/Switch/__tests__/Switch.contract.test.tsx +109 -0
  438. package/src/Switch/__tests__/Switch.controlled-state.test.tsx +79 -0
  439. package/src/Switch/__tests__/Switch.disabled.test.tsx +60 -0
  440. package/src/Switch/__tests__/Switch.error-handling.test.tsx +20 -0
  441. package/src/Switch/__tests__/Switch.keyboard-interaction.test.tsx +56 -0
  442. package/src/Switch/__tests__/Switch.thumb.test.tsx +84 -0
  443. package/src/Switch/__tests__/Switch.uncontrolled-state.test.tsx +83 -0
  444. package/src/Switch/hooks/index.ts +2 -0
  445. package/src/Switch/hooks/useSwitchContext.ts +1 -0
  446. package/src/Switch/hooks/useSwitchRoot.ts +28 -0
  447. package/src/Switch/index.ts +3 -0
  448. package/src/Switch/types.ts +37 -0
  449. package/src/Table/README.md +205 -0
  450. package/src/Table/Table.tsx +380 -0
  451. package/src/Table/__tests__/Table.Body.test.tsx +61 -0
  452. package/src/Table/__tests__/Table.Caption.test.tsx +70 -0
  453. package/src/Table/__tests__/Table.Cell.test.tsx +73 -0
  454. package/src/Table/__tests__/Table.Footer.test.tsx +61 -0
  455. package/src/Table/__tests__/Table.Head.test.tsx +61 -0
  456. package/src/Table/__tests__/Table.Header.test.tsx +73 -0
  457. package/src/Table/__tests__/Table.Root.test.tsx +49 -0
  458. package/src/Table/__tests__/Table.Row.test.tsx +67 -0
  459. package/src/Table/__tests__/Table.ScrollArea.test.tsx +83 -0
  460. package/src/Table/index.ts +1 -0
  461. package/src/Table/types.ts +63 -0
  462. package/src/Tabs/README.md +110 -0
  463. package/src/Tabs/Tabs.tsx +434 -0
  464. package/src/Tabs/TabsContext.ts +13 -0
  465. package/src/Tabs/__tests__/Tabs.activation-mode.test.tsx +114 -0
  466. package/src/Tabs/__tests__/Tabs.asChild.test.tsx +91 -0
  467. package/src/Tabs/__tests__/Tabs.basic-rendering.test.tsx +483 -0
  468. package/src/Tabs/__tests__/Tabs.change-event-callbacks.test.tsx +133 -0
  469. package/src/Tabs/__tests__/Tabs.controlled-state.test.tsx +152 -0
  470. package/src/Tabs/__tests__/Tabs.disabled-tabs.test.tsx +203 -0
  471. package/src/Tabs/__tests__/Tabs.error-handling.test.tsx +82 -0
  472. package/src/Tabs/__tests__/Tabs.fixtures.ts +171 -0
  473. package/src/Tabs/__tests__/Tabs.imperative-api.test.tsx +118 -0
  474. package/src/Tabs/__tests__/Tabs.keyboard-interaction.test.tsx +192 -0
  475. package/src/Tabs/__tests__/Tabs.lazy-mount.test.tsx +61 -0
  476. package/src/Tabs/__tests__/Tabs.mouse-interaction.test.tsx +216 -0
  477. package/src/Tabs/__tests__/Tabs.reading-direction.test.tsx +58 -0
  478. package/src/Tabs/__tests__/Tabs.uncontrolled-state.test.tsx +197 -0
  479. package/src/Tabs/hooks/index.ts +4 -0
  480. package/src/Tabs/hooks/useTabsContent.ts +27 -0
  481. package/src/Tabs/hooks/useTabsContext.ts +1 -0
  482. package/src/Tabs/hooks/useTabsRoot.ts +148 -0
  483. package/src/Tabs/hooks/useTabsTrigger.ts +111 -0
  484. package/src/Tabs/index.ts +3 -0
  485. package/src/Tabs/types.ts +99 -0
  486. package/src/Tabs/utils.ts +8 -0
  487. package/src/Textarea/README.md +98 -0
  488. package/src/Textarea/Textarea.tsx +93 -0
  489. package/src/Textarea/__tests__/Textarea.asChild.test.tsx +85 -0
  490. package/src/Textarea/__tests__/Textarea.basic-rendering.test.tsx +107 -0
  491. package/src/Textarea/__tests__/Textarea.disabled.test.tsx +49 -0
  492. package/src/Textarea/__tests__/Textarea.field-integration.test.tsx +134 -0
  493. package/src/Textarea/index.ts +2 -0
  494. package/src/Textarea/types.ts +7 -0
  495. package/src/Toggle/README.md +97 -0
  496. package/src/Toggle/Toggle.tsx +81 -0
  497. package/src/Toggle/__tests__/Toggle.asChild.test.tsx +42 -0
  498. package/src/Toggle/__tests__/Toggle.basic-rendering.test.tsx +28 -0
  499. package/src/Toggle/__tests__/Toggle.controlled-state.test.tsx +60 -0
  500. package/src/Toggle/__tests__/Toggle.disabled.test.tsx +34 -0
  501. package/src/Toggle/__tests__/Toggle.keyboard-interaction.test.tsx +42 -0
  502. package/src/Toggle/__tests__/Toggle.uncontrolled-state.test.tsx +40 -0
  503. package/src/Toggle/index.ts +2 -0
  504. package/src/Toggle/types.ts +23 -0
  505. package/src/ToggleGroup/README.md +137 -0
  506. package/src/ToggleGroup/ToggleGroup.tsx +298 -0
  507. package/src/ToggleGroup/ToggleGroupContext.ts +9 -0
  508. package/src/ToggleGroup/__tests__/ToggleGroup.asChild.test.tsx +65 -0
  509. package/src/ToggleGroup/__tests__/ToggleGroup.basic-rendering.test.tsx +50 -0
  510. package/src/ToggleGroup/__tests__/ToggleGroup.disabled.test.tsx +54 -0
  511. package/src/ToggleGroup/__tests__/ToggleGroup.keyboard-interaction.test.tsx +151 -0
  512. package/src/ToggleGroup/__tests__/ToggleGroup.multiple-mode.test.tsx +144 -0
  513. package/src/ToggleGroup/__tests__/ToggleGroup.reading-direction.test.tsx +28 -0
  514. package/src/ToggleGroup/__tests__/ToggleGroup.single-mode.test.tsx +139 -0
  515. package/src/ToggleGroup/hooks/index.ts +2 -0
  516. package/src/ToggleGroup/hooks/useToggleGroupContext.ts +1 -0
  517. package/src/ToggleGroup/hooks/useToggleGroupRoot.ts +110 -0
  518. package/src/ToggleGroup/index.ts +2 -0
  519. package/src/ToggleGroup/types.ts +72 -0
  520. package/src/Tooltip/README.md +214 -0
  521. package/src/Tooltip/Tooltip.tsx +260 -0
  522. package/src/Tooltip/TooltipContext.ts +20 -0
  523. package/src/Tooltip/__tests__/Tooltip.asChild.test.tsx +77 -0
  524. package/src/Tooltip/__tests__/Tooltip.basic-rendering.test.tsx +180 -0
  525. package/src/Tooltip/__tests__/Tooltip.controlled-state.test.tsx +128 -0
  526. package/src/Tooltip/__tests__/Tooltip.escape-hatches.test.tsx +73 -0
  527. package/src/Tooltip/__tests__/Tooltip.focus-interaction.test.tsx +88 -0
  528. package/src/Tooltip/__tests__/Tooltip.hover-interaction.test.tsx +179 -0
  529. package/src/Tooltip/__tests__/Tooltip.keyboard-interaction.test.tsx +85 -0
  530. package/src/Tooltip/__tests__/Tooltip.uncontrolled-state.test.tsx +67 -0
  531. package/src/Tooltip/hooks/index.ts +4 -0
  532. package/src/Tooltip/hooks/useTooltipContent.ts +53 -0
  533. package/src/Tooltip/hooks/useTooltipProvider.ts +41 -0
  534. package/src/Tooltip/hooks/useTooltipRoot.ts +106 -0
  535. package/src/Tooltip/hooks/useTooltipTrigger.ts +44 -0
  536. package/src/Tooltip/index.ts +1 -0
  537. package/src/Tooltip/types.ts +64 -0
  538. package/src/Tree/README.md +339 -0
  539. package/src/Tree/Tree.tsx +571 -0
  540. package/src/Tree/TreeContext.ts +24 -0
  541. package/src/Tree/__tests__/Tree.aria.test.tsx +53 -0
  542. package/src/Tree/__tests__/Tree.asChild.test.tsx +134 -0
  543. package/src/Tree/__tests__/Tree.basic-rendering.test.tsx +111 -0
  544. package/src/Tree/__tests__/Tree.branch-behaviour.test.tsx +87 -0
  545. package/src/Tree/__tests__/Tree.controlled-expansion.test.tsx +92 -0
  546. package/src/Tree/__tests__/Tree.data-attributes.test.tsx +88 -0
  547. package/src/Tree/__tests__/Tree.disabled-items.test.tsx +196 -0
  548. package/src/Tree/__tests__/Tree.error-handling.test.tsx +71 -0
  549. package/src/Tree/__tests__/Tree.forceMount.test.tsx +72 -0
  550. package/src/Tree/__tests__/Tree.keyboard-interaction.test.tsx +150 -0
  551. package/src/Tree/__tests__/Tree.multiple-selection.test.tsx +151 -0
  552. package/src/Tree/__tests__/Tree.range-selection.test.tsx +200 -0
  553. package/src/Tree/__tests__/Tree.recursion-depth.test.tsx +73 -0
  554. package/src/Tree/__tests__/Tree.roving-tabindex.test.tsx +117 -0
  555. package/src/Tree/__tests__/Tree.selection-path.test.tsx +404 -0
  556. package/src/Tree/__tests__/Tree.single-selection.test.tsx +108 -0
  557. package/src/Tree/__tests__/Tree.uncontrolled-expansion.test.tsx +69 -0
  558. package/src/Tree/hooks/index.ts +3 -0
  559. package/src/Tree/hooks/useTreeItemKeyboard.ts +86 -0
  560. package/src/Tree/hooks/useTreePath.ts +68 -0
  561. package/src/Tree/hooks/useTreeRoot.ts +279 -0
  562. package/src/Tree/index.ts +3 -0
  563. package/src/Tree/types.ts +224 -0
  564. package/src/Tree/utils.ts +59 -0
  565. package/src/VisuallyHidden/README.md +58 -0
  566. package/src/VisuallyHidden/VisuallyHidden.tsx +67 -0
  567. package/src/VisuallyHidden/__tests__/VisuallyHidden.test.tsx +59 -0
  568. package/src/VisuallyHidden/index.ts +2 -0
  569. package/src/VisuallyHidden/types.ts +5 -0
  570. package/src/hooks/index.ts +3 -0
  571. package/src/hooks/useCollection.ts +74 -0
  572. package/src/hooks/useControllableState.ts +81 -0
  573. package/src/hooks/useRovingTabindex.ts +178 -0
  574. package/src/index.ts +38 -0
  575. package/src/test/intersectionObserverPolyfill.ts +83 -0
  576. package/src/test/popoverPolyfill.ts +86 -0
  577. package/src/test/scrollPolyfill.ts +23 -0
  578. package/src/types.ts +13 -0
  579. package/src/utils/__tests__/createStrictContext.test.tsx +69 -0
  580. package/src/utils/__tests__/deriveId.test.ts +28 -0
  581. package/src/utils/__tests__/getKeyToActionMap.test.ts +106 -0
  582. package/src/utils/createStrictContext.ts +49 -0
  583. package/src/utils/deriveId.ts +31 -0
  584. package/src/utils/getKeyToActionMap.ts +95 -0
  585. package/src/utils/index.ts +3 -0
@@ -0,0 +1,290 @@
1
+ import { KeyboardEvent, useCallback, useEffect, useMemo, useRef } from "react";
2
+
3
+ import { useCarouselContext } from "./useCarouselContext";
4
+
5
+ /**
6
+ * Owns the Viewport-side scroll-state sync — bidirectional.
7
+ *
8
+ * **State → scroll.** When `currentPage` flips for any reason
9
+ * (NextTrigger, Indicator click, autoplay tick, imperative goTo), the
10
+ * effect locates the first slide of the target page via the published
11
+ * `slidesRef`, reads its `getBoundingClientRect` relative to the
12
+ * viewport, and calls `scrollTo` so the visual surface tracks React
13
+ * state. The current `scrollLeft` is included in the target so the
14
+ * calculation is correct mid-scroll. The run also asserts
15
+ * `isProgrammaticScrollRef` so the IntersectionObserver fallback
16
+ * doesn't treat the in-flight animation as a user scroll and undo the
17
+ * page change; the flag clears on `scrollend` (with a timeout
18
+ * fallback for environments that don't fire it).
19
+ *
20
+ * **Scroll → state.** When the user swipes the viewport, the browser
21
+ * fires `scrollsnapchange` with the new snap target. The handler
22
+ * looks up which slide that target is, computes
23
+ * `floor(slideIndex / slidesPerPage)`, and calls `goTo` on the new
24
+ * page (skipping when the page is unchanged so consumers don't see
25
+ * spurious onPageChange callbacks, and bailing when the snap target
26
+ * isn't one of our registered slides). An `IntersectionObserver`
27
+ * provides the same page-drive for browsers without `scrollsnapchange`,
28
+ * but stands down when the event is supported (it is authoritative);
29
+ * the observer still always feeds `isInView`.
30
+ *
31
+ * **Keyboard → state.** Returns an `onKeyDown` handler that wires the
32
+ * WAI-ARIA Carousel pattern arrow keys (`ArrowRight` / `ArrowLeft` for
33
+ * next / previous) plus `Home` / `End` (first / last) onto the same
34
+ * imperative API the trigger buttons call, so smooth scroll matches
35
+ * the click path. The handler only fires when the Viewport itself is
36
+ * the focus target — focus inside a slide keeps its native arrow-key
37
+ * semantics — and respects `canGoNext` / `canGoPrevious` so it clamps
38
+ * at the ends.
39
+ */
40
+ export function useCarouselViewport() {
41
+ const {
42
+ slidesRef,
43
+ slideKeys,
44
+ effectiveSlidesPerMove,
45
+ totalPages,
46
+ currentPage,
47
+ goTo,
48
+ next,
49
+ previous,
50
+ canGoNext,
51
+ canGoPrevious,
52
+ transition,
53
+ snapAlign,
54
+ refreshTick,
55
+ visibleSlideIndicesRef,
56
+ setSlideInView,
57
+ isProgrammaticScrollRef,
58
+ } = useCarouselContext();
59
+ const internalRef = useRef<HTMLDivElement>(null);
60
+ // Set to true by the scrollsnapchange handler and the IntersectionObserver
61
+ // callback before they call goTo(), so the scroll effect knows the page
62
+ // change originated from a user scroll (CSS snap already positioned the
63
+ // viewport) and must not call scrollTo() again.
64
+ const isUserScrollRef = useRef(false);
65
+
66
+ // Callback ref so the consumer can compose their own ref with ours
67
+ // via `composeRefs` later (cycle 22 introduces asChild). For now,
68
+ // it just stashes the node.
69
+ const viewportRef = useCallback((node: HTMLDivElement | null) => {
70
+ internalRef.current = node;
71
+ }, []);
72
+
73
+ // Read prefers-reduced-motion once on mount; choose scrollTo
74
+ // behavior accordingly so we don't fight the OS-level setting.
75
+ const scrollBehavior = useMemo<ScrollBehavior>(
76
+ () =>
77
+ window.matchMedia?.("(prefers-reduced-motion: reduce)").matches
78
+ ? "instant"
79
+ : "smooth",
80
+ [],
81
+ );
82
+
83
+ useEffect(() => {
84
+ // 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];
89
+ // No slides registered yet, or page out of range: nothing to scroll to.
90
+ if (!firstSlideKey) return;
91
+
92
+ // Both viewport ref and the slide element are guaranteed populated
93
+ // here — the effect runs post-commit (after callback refs fire) and
94
+ // any key in slideKeys was just registered into slidesRef in
95
+ // lockstep by useCarouselRoot.registerSlide.
96
+ // A user swipe via CSS scroll-snap has already positioned the viewport;
97
+ // calling scrollTo on top would start a second animation and cause jank.
98
+ if (isUserScrollRef.current) {
99
+ isUserScrollRef.current = false;
100
+ return;
101
+ }
102
+
103
+ const viewport = internalRef.current!;
104
+
105
+ // Mark the scroll as programmatic so the IntersectionObserver
106
+ // doesn't treat the in-flight animation as a user scroll and undo
107
+ // the page change. next() / previous() also set this — re-asserting
108
+ // here covers indicator-driven goTo and the initial scroll on
109
+ // mount, neither of which goes through next() / previous().
110
+ isProgrammaticScrollRef.current = true;
111
+
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
+ });
123
+
124
+ // Clear the programmatic-scroll guard once the animation settles.
125
+ // `scrollend` is the reliable signal in real browsers; the setTimeout
126
+ // is a fallback for environments (jsdom, older Safari) that don't fire
127
+ // it. The timeout is longer than any typical smooth-scroll duration so
128
+ // real-browser IO entries that fire mid-animation are still suppressed.
129
+ // Re-clearing the flag is harmless, so no idempotency guard is needed.
130
+ const clearFlag = () => {
131
+ isProgrammaticScrollRef.current = false;
132
+ };
133
+ viewport.addEventListener("scrollend", clearFlag, { once: true });
134
+ const timeoutId = setTimeout(() => {
135
+ viewport.removeEventListener("scrollend", clearFlag);
136
+ clearFlag();
137
+ }, 600);
138
+ return () => {
139
+ clearTimeout(timeoutId);
140
+ viewport.removeEventListener("scrollend", clearFlag);
141
+ };
142
+ }, [
143
+ transition,
144
+ snapAlign,
145
+ currentPage,
146
+ effectiveSlidesPerMove,
147
+ slideKeys,
148
+ slidesRef,
149
+ refreshTick,
150
+ scrollBehavior,
151
+ ]);
152
+
153
+ // User-driven scroll → state. Listen for scrollsnapchange and update
154
+ // currentPage from the snapped slide's index. The viewport ref is
155
+ // guaranteed populated post-commit (callback ref runs first).
156
+ useEffect(() => {
157
+ if (transition !== "slide") return;
158
+ const viewport = internalRef.current!;
159
+
160
+ const handler = (event: Event) => {
161
+ const target = (event as Event & { snapTargetInline?: Element })
162
+ .snapTargetInline;
163
+
164
+ // findIndex returns -1 when the snap target isn't one of our
165
+ // registered slides — e.g. a consumer-wrapped element inside the
166
+ // viewport. Ignore those; only registered slides drive the page.
167
+ const slideIndex = slideKeys.findIndex(
168
+ (key) => slidesRef.current!.get(key) === target,
169
+ );
170
+ if (slideIndex < 0) return;
171
+
172
+ const targetPage = Math.floor(slideIndex / effectiveSlidesPerMove);
173
+ if (targetPage !== currentPage) {
174
+ isUserScrollRef.current = true;
175
+ goTo(targetPage);
176
+ }
177
+ };
178
+
179
+ viewport.addEventListener("scrollsnapchange", handler);
180
+ return () => viewport.removeEventListener("scrollsnapchange", handler);
181
+ }, [
182
+ transition,
183
+ slideKeys,
184
+ slidesRef,
185
+ effectiveSlidesPerMove,
186
+ currentPage,
187
+ goTo,
188
+ ]);
189
+
190
+ // IntersectionObserver fallback for browsers without scrollsnapchange,
191
+ // 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.
194
+ useEffect(() => {
195
+ if (transition !== "slide") return;
196
+
197
+ // When the browser fires scrollsnapchange (Chrome 129+), that event is
198
+ // the authoritative source of the active page — snapTargetInline is the
199
+ // precisely-snapped slide. The observer below then only feeds isInView.
200
+ // Its lowest-index-visible heuristic is a coarse page fallback for
201
+ // browsers without the event, and would mis-track a centre-aligned
202
+ // carousel that shows several slides at once (it follows the leftmost
203
+ // visible slide, not the centred one), so it must not drive the page
204
+ // when the event is available.
205
+ const supportsSnapEvents = "onscrollsnapchange" in window;
206
+
207
+ const observer = new IntersectionObserver(
208
+ (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.
214
+ for (const entry of entries) {
215
+ const idx = slideKeys.findIndex(
216
+ (key) => slidesRef.current!.get(key) === entry.target,
217
+ );
218
+ setSlideInView(
219
+ idx,
220
+ entry.isIntersecting && entry.intersectionRatio >= 0.6,
221
+ );
222
+ }
223
+
224
+ // isInView is updated above regardless; only the page-drive below
225
+ // is the fallback that scrollsnapchange supersedes.
226
+ if (supportsSnapEvents) return;
227
+
228
+ const visible = visibleSlideIndicesRef.current;
229
+ if (visible.size === 0) return;
230
+ const firstVisible = Math.min(...visible);
231
+ const targetPage = Math.floor(firstVisible / effectiveSlidesPerMove);
232
+ // Guard: if a programmatic scroll is in flight (e.g. user clicked
233
+ // 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.
236
+ if (targetPage !== currentPage && !isProgrammaticScrollRef.current) {
237
+ isUserScrollRef.current = true;
238
+ goTo(targetPage);
239
+ }
240
+ },
241
+ { threshold: 0.6 },
242
+ );
243
+
244
+ for (const key of slideKeys) {
245
+ observer.observe(slidesRef.current!.get(key)!);
246
+ }
247
+
248
+ return () => observer.disconnect();
249
+ }, [
250
+ transition,
251
+ slideKeys,
252
+ slidesRef,
253
+ effectiveSlidesPerMove,
254
+ currentPage,
255
+ goTo,
256
+ setSlideInView,
257
+ visibleSlideIndicesRef,
258
+ ]);
259
+
260
+ // Keyboard navigation per the WAI-ARIA Carousel pattern: arrow keys
261
+ // route through the same imperative API as the trigger buttons so the
262
+ // smooth scroll and loop-wrap animation match the click path. The
263
+ // event.target === currentTarget guard restricts handling to the
264
+ // Viewport itself — focus inside a slide (e.g. on a link or form
265
+ // control) keeps its native arrow-key semantics.
266
+ const onKeyDown = useCallback(
267
+ (event: KeyboardEvent<HTMLDivElement>) => {
268
+ // Restrict to keypresses originating on the Viewport itself —
269
+ // focus inside a slide (e.g. on a link or form control) keeps
270
+ // its native arrow-key semantics.
271
+ if (event.target !== event.currentTarget) return;
272
+ if (event.key === "ArrowRight") {
273
+ event.preventDefault();
274
+ if (canGoNext) next();
275
+ } else if (event.key === "ArrowLeft") {
276
+ event.preventDefault();
277
+ if (canGoPrevious) previous();
278
+ } else if (event.key === "Home") {
279
+ event.preventDefault();
280
+ goTo(0);
281
+ } else if (event.key === "End") {
282
+ event.preventDefault();
283
+ goTo(totalPages - 1);
284
+ }
285
+ },
286
+ [canGoNext, canGoPrevious, next, previous, goTo, totalPages],
287
+ );
288
+
289
+ return { viewportRef, onKeyDown };
290
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./Carousel";
2
+ export * from "./CarouselContext";
3
+ export * from "./types";
@@ -0,0 +1,400 @@
1
+ import { ComponentProps, ReactNode, RefObject } from "react";
2
+
3
+ /**
4
+ * Discriminated label shape for `Carousel.Root` — exactly one of
5
+ * `ariaLabel` or `ariaLabelledBy` must be supplied so every carousel
6
+ * has an accessible name (per the WAI-ARIA Carousel pattern). TypeScript
7
+ * rejects shapes that supply both or neither.
8
+ */
9
+ export type CarouselRootLabelProps =
10
+ | { ariaLabel: string; ariaLabelledBy?: never }
11
+ | { ariaLabel?: never; ariaLabelledBy: string };
12
+
13
+ /**
14
+ * Uncontrolled page state — the Root owns the active page internally,
15
+ * optionally seeded by `defaultPage`. The discriminated union below
16
+ * rejects passing `page` or `onPageChange` alongside it.
17
+ */
18
+ export type UncontrolledCarouselPageProps = {
19
+ /** Uncontrolled active page index. Defaults to `0`. */
20
+ defaultPage?: number;
21
+ page?: never;
22
+ onPageChange?: never;
23
+ };
24
+
25
+ /**
26
+ * Controlled page state — the parent owns the active page; the Root
27
+ * defers every state change back through `onPageChange`. Both props
28
+ * must be supplied together.
29
+ */
30
+ export type ControlledCarouselPageProps = {
31
+ /** Controlled active page index. */
32
+ page: number;
33
+ /** Callback invoked when the active page should change (e.g. when the
34
+ * user clicks `Carousel.NextTrigger` or `Carousel.PreviousTrigger`).
35
+ * The callback is responsible for re-rendering with the new `page`. */
36
+ onPageChange: (page: number) => void;
37
+ defaultPage?: never;
38
+ };
39
+
40
+ /**
41
+ * Discriminated state union — TypeScript rejects mixed shapes (e.g.
42
+ * `defaultPage` + `page`, or `page` without `onPageChange`).
43
+ */
44
+ export type CarouselRootPageStateProps =
45
+ | UncontrolledCarouselPageProps
46
+ | ControlledCarouselPageProps;
47
+
48
+ /**
49
+ * Uncontrolled playing state — the Root owns the "playing" flag
50
+ * internally, optionally seeded by `defaultPlaying`.
51
+ */
52
+ export type UncontrolledCarouselPlayingProps = {
53
+ /** Uncontrolled initial playing flag. Defaults to `false`. */
54
+ defaultPlaying?: boolean;
55
+ playing?: never;
56
+ onPlayingChange?: never;
57
+ };
58
+
59
+ /**
60
+ * Controlled playing state — the parent owns the "playing" flag; the
61
+ * Root defers every change back through `onPlayingChange`. Both props
62
+ * must be supplied together.
63
+ */
64
+ export type ControlledCarouselPlayingProps = {
65
+ /** Controlled playing flag. */
66
+ playing: boolean;
67
+ /** Callback invoked when the playing flag should toggle. The callback
68
+ * is responsible for re-rendering with the new `playing` value. */
69
+ onPlayingChange: (playing: boolean) => void;
70
+ defaultPlaying?: never;
71
+ };
72
+
73
+ /**
74
+ * Discriminated playing-state union — mirrors the page-state pattern.
75
+ */
76
+ export type CarouselRootPlayingStateProps =
77
+ | UncontrolledCarouselPlayingProps
78
+ | ControlledCarouselPlayingProps;
79
+
80
+ /**
81
+ * Autoplay configuration. Pass `true` for the default 4000ms cadence,
82
+ * `false` (default) to disable autoplay entirely, or `{ delay: N }` to
83
+ * tune the interval. The active page advances on each tick while
84
+ * `playing` is `true`; the timer stops once the active page reaches the
85
+ * last slide.
86
+ */
87
+ export type CarouselAutoplay = boolean | { delay: number };
88
+
89
+ /**
90
+ * Visual transition mode for the viewport.
91
+ *
92
+ * - `"slide"` (default) — relies on native CSS scroll-snap; the
93
+ * Viewport scrolls programmatically when the page changes and
94
+ * listens for `scrollsnapchange` to update React state when the
95
+ * user swipes.
96
+ * - `"none"` — the Viewport installs no scroll wiring at all.
97
+ * Consumer CSS owns the visual transition (e.g. crossfade,
98
+ * dissolve) via the `data-state` hook on each slide, which still
99
+ * flips with the active page.
100
+ */
101
+ export type CarouselTransition = "slide" | "none";
102
+
103
+ /**
104
+ * Scroll-snap alignment that the Viewport should target when
105
+ * programmatically scrolling to a page.
106
+ *
107
+ * - `"start"` (default) — scrolls so the first slide of the target
108
+ * page aligns with the **start** (left) edge of the Viewport.
109
+ * Matches `scroll-snap-align: start` in consumer CSS.
110
+ * - `"center"` — scrolls so the first slide of the target page is
111
+ * **centred** in the Viewport. The target is offset inward by
112
+ * `(viewportWidth − slideWidth) / 2`. Use this with
113
+ * `scroll-snap-align: center` in consumer CSS (e.g. Cover Flow
114
+ * layouts where slides are narrower than the Viewport).
115
+ */
116
+ export type CarouselSnapAlign = "start" | "center";
117
+
118
+ /**
119
+ * Pin DOM `id`s on the rendered sub-components for SSR / hydration
120
+ * stability or for external `aria-controls` references. Any keys you
121
+ * omit leave the corresponding element unidentified (or with whatever
122
+ * the consumer attaches to that sub-component directly via its own
123
+ * `id` prop, which always wins).
124
+ */
125
+ export type CarouselIds = {
126
+ root?: string;
127
+ viewport?: string;
128
+ previousTrigger?: string;
129
+ nextTrigger?: string;
130
+ playPauseTrigger?: string;
131
+ indicatorGroup?: string;
132
+ };
133
+
134
+ /**
135
+ * Override the default user-visible strings the component owns —
136
+ * intended for internationalisation. Any keys you omit fall back to
137
+ * the English defaults.
138
+ */
139
+ export type CarouselTranslations = {
140
+ /** Format used for the auto-generated slide aria-label. Receives
141
+ * 1-indexed `index` and the live `total`. Default:
142
+ * `({ index, total }) => "${index} of ${total}"`. */
143
+ slideLabel?: (params: { index: number; total: number }) => string;
144
+ /** Format used for the auto-generated indicator aria-label. Receives
145
+ * the 1-indexed page position. Default:
146
+ * `({ index }) => "Slide ${index}"`. */
147
+ indicatorLabel?: (params: { index: number }) => string;
148
+ /** Accessible name for `Carousel.PlayPauseTrigger` while paused.
149
+ * Default: `"Start automatic slide show"`. */
150
+ startSlideshow?: string;
151
+ /** Accessible name for `Carousel.PlayPauseTrigger` while playing.
152
+ * Default: `"Stop automatic slide show"`. */
153
+ stopSlideshow?: string;
154
+ };
155
+
156
+ export type CarouselRootProps = Omit<
157
+ ComponentProps<"section">,
158
+ "aria-label" | "aria-labelledby"
159
+ > &
160
+ CarouselRootLabelProps &
161
+ CarouselRootPageStateProps &
162
+ CarouselRootPlayingStateProps & {
163
+ /** Autoplay configuration — see {@link CarouselAutoplay}. */
164
+ autoplay?: CarouselAutoplay;
165
+ /** Visual transition mode — see {@link CarouselTransition}.
166
+ * Defaults to `"slide"`. */
167
+ transition?: CarouselTransition;
168
+ /** Number of slides visible per page. Defaults to `1`. With values
169
+ * greater than `1`, slides are grouped into pages of that size for
170
+ * navigation purposes: indicators auto-render per page, boundary
171
+ * clamp moves to the last page, and `Carousel.NextTrigger` /
172
+ * `Carousel.PreviousTrigger` advance one page at a time. */
173
+ slidesPerPage?: number;
174
+ /** Number of slides advanced by `Carousel.NextTrigger` /
175
+ * `Carousel.PreviousTrigger`. `"auto"` (default) advances one
176
+ * full page at a time (= `slidesPerPage`); a number advances
177
+ * exactly that many slides per click and pages are computed so
178
+ * the visible window always stays full. */
179
+ slidesPerMove?: number | "auto";
180
+ /** Override the default user-visible strings the component owns —
181
+ * see {@link CarouselTranslations}. Useful for i18n. */
182
+ translations?: CarouselTranslations;
183
+ /** Pin DOM `id`s on the rendered sub-components — see
184
+ * {@link CarouselIds}. Useful for SSR hydration stability and
185
+ * external `aria-controls` linkage. */
186
+ ids?: CarouselIds;
187
+ /** Scroll-snap alignment the Viewport targets when programmatically
188
+ * scrolling to a page — see {@link CarouselSnapAlign}.
189
+ * Defaults to `"start"`. Set to `"center"` when consumer CSS uses
190
+ * `scroll-snap-align: center` on slides (e.g. Cover Flow layouts
191
+ * where slides are narrower than the Viewport). */
192
+ snapAlign?: CarouselSnapAlign;
193
+ };
194
+
195
+ /**
196
+ * Shape of the context published by `Carousel.Root` to descendants.
197
+ * Fields are added as future cycles introduce shared state (active
198
+ * page, autoplay, etc.).
199
+ */
200
+ export type CarouselContextValue = {
201
+ /** Self-registers a slide. Called as a callback ref by `Carousel.Slide`
202
+ * with the rendered DOM node on mount and `null` on unmount. */
203
+ registerSlide: (key: string, element: HTMLDivElement | null) => void;
204
+ /** Live map from slide key to rendered DOM node. Used by the
205
+ * Viewport to read `getBoundingClientRect` on the first slide of
206
+ * the target page when programmatically scrolling. */
207
+ slidesRef: RefObject<Map<string, HTMLDivElement>>;
208
+ /** Ordered list of currently-mounted slide keys. The slide's index is
209
+ * its position in this array; the array's length is the total. */
210
+ slideKeys: string[];
211
+ /** Number of slides visible per page (default `1`). */
212
+ slidesPerPage: number;
213
+ /** Resolved slides advanced per Prev/Next click — equal to
214
+ * `slidesPerPage` when the consumer left `slidesPerMove="auto"`,
215
+ * else the numeric value. */
216
+ effectiveSlidesPerMove: number;
217
+ /** Live total page count — `ceil(total / slidesPerPage)` in `"auto"`
218
+ * mode (partial last page allowed), else
219
+ * `floor((total - slidesPerPage) / effectiveSlidesPerMove) + 1`
220
+ * (always full-windowed). Drives indicator count and boundary
221
+ * clamp. */
222
+ totalPages: number;
223
+ /** Zero-based index of the currently-active page. */
224
+ currentPage: number;
225
+ /** `true` when there is a forward navigation target (a page ahead of
226
+ * the active one). Drives the `disabled` attribute on
227
+ * `Carousel.NextTrigger` and short-circuits `next()` when there's
228
+ * nowhere to go. */
229
+ canGoNext: boolean;
230
+ /** `true` when there is a backward navigation target. Drives the
231
+ * `disabled` attribute on `Carousel.PreviousTrigger`. */
232
+ canGoPrevious: boolean;
233
+ /** Advance the active page by one step. No-op when `!canGoNext`. */
234
+ next: () => void;
235
+ /** Retreat the active page by one step. No-op when `!canGoPrevious`. */
236
+ previous: () => void;
237
+ /** Jump directly to `target` (zero-based page index). Used by
238
+ * `Carousel.Indicator` to dispatch click-to-jump. */
239
+ goTo: (target: number) => void;
240
+ /** Whether autoplay is currently in the "playing" state. */
241
+ playing: boolean;
242
+ /** Toggles `playing`. Used by `Carousel.PlayPauseTrigger`. */
243
+ togglePlaying: () => void;
244
+ /** `true` when the consumer enabled autoplay (regardless of the
245
+ * `playing` flag). Used by `Carousel.PlayPauseTrigger` to validate
246
+ * its own configuration. */
247
+ autoplayEnabled: boolean;
248
+ /** `true` when autoplay is enabled AND `playing` is `true` — i.e. the
249
+ * timer could fire any moment. Drives the Viewport's `aria-live`
250
+ * flip ("off" while auto-rotating, "polite" otherwise) so screen
251
+ * readers don't get spammed by every tick. */
252
+ isAutoRotating: boolean;
253
+ /** Translations merged with English defaults — every field is
254
+ * present, even if the consumer passed only a subset. */
255
+ translations: Required<CarouselTranslations>;
256
+ /** Custom DOM ids — every field optional. Sub-components apply
257
+ * their respective entry via spread, so consumer-supplied `id`
258
+ * props on the sub-component still win. */
259
+ ids: CarouselIds;
260
+ /** Resolved visual transition mode (defaults to `"slide"`). */
261
+ transition: CarouselTransition;
262
+ /** Resolved scroll-snap alignment (defaults to `"start"`). */
263
+ snapAlign: CarouselSnapAlign;
264
+ /** Bumped by `refresh()` to force the viewport's scroll-align
265
+ * effect to re-run without a page change. */
266
+ refreshTick: number;
267
+ /** Live set of slide indices currently visible per IntersectionObserver
268
+ * (≥ 60% intersection). Mutated by the Viewport hook and read by
269
+ * the imperative `isInView`. */
270
+ visibleSlideIndicesRef: RefObject<Set<number>>;
271
+ /** Used by the Viewport hook to record visibility transitions. */
272
+ setSlideInView: (slideIndex: number, inView: boolean) => void;
273
+ /** Set to `true` by `next()` and `previous()` the moment programmatic
274
+ * navigation begins, and cleared by the Viewport hook once the scroll
275
+ * animation settles (`scrollend` or a timeout fallback). The
276
+ * IntersectionObserver callback checks this flag before calling `goTo`
277
+ * so that IO entries firing mid-animation cannot undo the navigation. */
278
+ isProgrammaticScrollRef: RefObject<boolean>;
279
+ };
280
+
281
+ export type CarouselViewportProps = ComponentProps<"div">;
282
+
283
+ export type CarouselSlideProps = Omit<ComponentProps<"div">, "aria-label"> & {
284
+ /** Override the auto-generated `"N of M"` `aria-label`. Use this when
285
+ * the slide has a more meaningful description than its position
286
+ * (e.g. `"Hand-picked for you"`). When omitted, slides are labelled
287
+ * with their live index and total in registration order. */
288
+ ariaLabel?: string;
289
+ };
290
+
291
+ export type CarouselNextTriggerProps = ComponentProps<"button"> & {
292
+ /** Render the child element instead of the default `<button>`. All
293
+ * trigger props (onClick, disabled, ids.nextTrigger, …) are merged
294
+ * onto the child via `Slot`. The child must accept a `ref`. */
295
+ asChild?: boolean;
296
+ };
297
+
298
+ export type CarouselPreviousTriggerProps = ComponentProps<"button"> & {
299
+ /** See `CarouselNextTriggerProps.asChild`. */
300
+ asChild?: boolean;
301
+ };
302
+
303
+ /**
304
+ * Discriminated label shape for `Carousel.IndicatorGroup` — exactly one
305
+ * of `label` (becomes `aria-label`) or `ariaLabelledBy` (points at an
306
+ * external label element) must be supplied. TypeScript rejects
307
+ * both-or-neither at compile time.
308
+ */
309
+ export type CarouselIndicatorGroupProps = Omit<
310
+ ComponentProps<"div">,
311
+ "label" | "aria-labelledby"
312
+ > &
313
+ (
314
+ | { label: string; ariaLabelledBy?: never }
315
+ | { label?: never; ariaLabelledBy: string }
316
+ );
317
+
318
+ export type CarouselIndicatorProps = ComponentProps<"button"> & {
319
+ /** Zero-based page this indicator targets. Clicking jumps to it. */
320
+ index: number;
321
+ /** Render the child element instead of the default `<button>`.
322
+ * Trigger props (onClick, aria-label, aria-disabled, data-state)
323
+ * are merged onto the child via `Slot`. The child must accept a
324
+ * `ref`. */
325
+ asChild?: boolean;
326
+ };
327
+
328
+ /**
329
+ * Imperative handle exposed via `ref` on `Carousel.Root`. Routes
330
+ * through the same internal state machine the trigger components
331
+ * use, so controlled-mode `onPageChange` / `onPlayingChange` still
332
+ * fire as if the user had clicked.
333
+ */
334
+ export type CarouselImperativeApi = {
335
+ /** Advance the active page by one. No-op on the last page. */
336
+ next: () => void;
337
+ /** Retreat the active page by one. No-op on the first page. */
338
+ previous: () => void;
339
+ /** Jump directly to `target` (zero-based page index). */
340
+ goTo: (target: number) => void;
341
+ /** Set `playing` to `true`. Dismisses the hover/focus pause for the
342
+ * lifetime of the resulting playing session. */
343
+ play: () => void;
344
+ /** Set `playing` to `false`. */
345
+ pause: () => void;
346
+ /** Re-issue the viewport's scrollTo for the current page. Call when
347
+ * external layout changes (window resize, container reflow) leave
348
+ * the scroll position misaligned with React state. */
349
+ refresh: () => void;
350
+ /** Live progress snapshot: the active page, the total page count,
351
+ * and a normalised `value` in `[0, 1]` (0 when there's at most one
352
+ * page). */
353
+ getProgress: () => { page: number; totalPages: number; value: number };
354
+ /** Reports whether the slide at the zero-based index is currently
355
+ * visible in the viewport (per IntersectionObserver, ≥ 60%
356
+ * intersection). Useful for lazy-loading slide content. */
357
+ isInView: (slideIndex: number) => boolean;
358
+ };
359
+
360
+ export type CarouselProgress = {
361
+ page: number;
362
+ totalPages: number;
363
+ value: number;
364
+ };
365
+
366
+ /**
367
+ * Convenience wrapper that auto-renders one `Carousel.Indicator` per
368
+ * registered slide. Reuses the same discriminated label shape as
369
+ * `Carousel.IndicatorGroup`. `children` is reserved (the auto-mapped
370
+ * indicators take that slot) — drop down to `Carousel.IndicatorGroup`
371
+ * + `Carousel.Indicator` if you need custom indicator content.
372
+ */
373
+ export type CarouselIndicatorsProps = Omit<
374
+ ComponentProps<"div">,
375
+ "label" | "aria-labelledby" | "children"
376
+ > &
377
+ (
378
+ | { label: string; ariaLabelledBy?: never }
379
+ | { label?: never; ariaLabelledBy: string }
380
+ );
381
+
382
+ /**
383
+ * Render-prop or static children supported by
384
+ * `Carousel.PlayPauseTrigger`. The function form receives the live
385
+ * `playing` flag so consumers can swap icons / labels per state.
386
+ */
387
+ export type CarouselPlayPauseTriggerChildren =
388
+ | ReactNode
389
+ | ((state: { playing: boolean }) => ReactNode);
390
+
391
+ export type CarouselPlayPauseTriggerProps = Omit<
392
+ ComponentProps<"button">,
393
+ "children"
394
+ > & {
395
+ children?: CarouselPlayPauseTriggerChildren;
396
+ /** Render the child element instead of the default `<button>`.
397
+ * The child must accept a `ref`. The render-prop form of `children`
398
+ * is not supported under `asChild`; pass a single element instead. */
399
+ asChild?: boolean;
400
+ };