@chassis-ui/react 0.0.0-20260831090525

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +65 -0
  3. package/dist/index.d.ts +5909 -0
  4. package/dist/index.js +2 -0
  5. package/dist/style.css +1 -0
  6. package/package.json +114 -0
  7. package/src/components/accordion/Accordion.tsx +190 -0
  8. package/src/components/accordion/AccordionBody.tsx +46 -0
  9. package/src/components/accordion/AccordionHeader.tsx +21 -0
  10. package/src/components/accordion/AccordionItem.tsx +59 -0
  11. package/src/components/accordion/context.ts +8 -0
  12. package/src/components/accordion/index.ts +8 -0
  13. package/src/components/autocomplete/Autocomplete.tsx +464 -0
  14. package/src/components/autocomplete/AutocompleteGroup.tsx +20 -0
  15. package/src/components/autocomplete/AutocompleteItem.tsx +18 -0
  16. package/src/components/autocomplete/index.ts +6 -0
  17. package/src/components/avatar/Avatar.tsx +134 -0
  18. package/src/components/avatar/AvatarImage.tsx +19 -0
  19. package/src/components/avatar/AvatarStack.tsx +98 -0
  20. package/src/components/avatar/index.ts +6 -0
  21. package/src/components/badge/Badge.tsx +94 -0
  22. package/src/components/badge/index.ts +2 -0
  23. package/src/components/breadcrumb/Breadcrumb.tsx +59 -0
  24. package/src/components/breadcrumb/BreadcrumbItem.tsx +38 -0
  25. package/src/components/breadcrumb/index.ts +4 -0
  26. package/src/components/button/Button.tsx +229 -0
  27. package/src/components/button/index.ts +2 -0
  28. package/src/components/button-group/ButtonGroup.tsx +55 -0
  29. package/src/components/button-group/ButtonToolbar.tsx +47 -0
  30. package/src/components/button-group/index.ts +4 -0
  31. package/src/components/calendar/Calendar.scss +57 -0
  32. package/src/components/calendar/Calendar.tsx +324 -0
  33. package/src/components/calendar/CalendarMonthBlock.tsx +57 -0
  34. package/src/components/calendar/CalendarMonthGrid.tsx +92 -0
  35. package/src/components/calendar/CalendarMonthYearPicker.tsx +162 -0
  36. package/src/components/calendar/CalendarNavButton.tsx +24 -0
  37. package/src/components/calendar/CalendarWeekGrid.tsx +58 -0
  38. package/src/components/calendar/CalendarYearGrid.tsx +126 -0
  39. package/src/components/calendar/RangeCalendar.scss +95 -0
  40. package/src/components/calendar/RangeCalendar.tsx +361 -0
  41. package/src/components/calendar/index.ts +5 -0
  42. package/src/components/card/Card.tsx +173 -0
  43. package/src/components/card/CardBody.tsx +75 -0
  44. package/src/components/card/CardFooter.tsx +47 -0
  45. package/src/components/card/CardGroup.tsx +51 -0
  46. package/src/components/card/CardHeader.tsx +47 -0
  47. package/src/components/card/CardImage.tsx +70 -0
  48. package/src/components/card/CardImageOverlay.tsx +47 -0
  49. package/src/components/card/CardLink.tsx +40 -0
  50. package/src/components/card/CardSubtitle.tsx +47 -0
  51. package/src/components/card/CardText.tsx +47 -0
  52. package/src/components/card/CardTitle.tsx +47 -0
  53. package/src/components/card/index.ts +22 -0
  54. package/src/components/carousel/Carousel.tsx +742 -0
  55. package/src/components/carousel/CarouselControlButton.tsx +74 -0
  56. package/src/components/carousel/CarouselControlNext.tsx +22 -0
  57. package/src/components/carousel/CarouselControlPrev.tsx +22 -0
  58. package/src/components/carousel/CarouselIndicators.tsx +44 -0
  59. package/src/components/carousel/CarouselInner.tsx +56 -0
  60. package/src/components/carousel/CarouselItem.tsx +51 -0
  61. package/src/components/carousel/CarouselOverlay.tsx +51 -0
  62. package/src/components/carousel/CarouselPlayPause.tsx +61 -0
  63. package/src/components/carousel/carouselEngine.ts +170 -0
  64. package/src/components/carousel/context.ts +29 -0
  65. package/src/components/carousel/index.ts +21 -0
  66. package/src/components/checkbox/Checkbox.tsx +236 -0
  67. package/src/components/checkbox/CheckboxGroup.tsx +145 -0
  68. package/src/components/checkbox/context.ts +14 -0
  69. package/src/components/checkbox/index.ts +4 -0
  70. package/src/components/chip/Chip.tsx +183 -0
  71. package/src/components/chip/index.ts +2 -0
  72. package/src/components/chip-input/ChipInput.tsx +363 -0
  73. package/src/components/chip-input/ChipList.tsx +95 -0
  74. package/src/components/chip-input/index.ts +2 -0
  75. package/src/components/close-button/CloseButton.tsx +205 -0
  76. package/src/components/close-button/index.ts +2 -0
  77. package/src/components/collapse/Collapse.tsx +128 -0
  78. package/src/components/collapse/index.ts +2 -0
  79. package/src/components/color-input/ColorInput.tsx +106 -0
  80. package/src/components/color-input/index.ts +2 -0
  81. package/src/components/combobox/Combobox.tsx +320 -0
  82. package/src/components/combobox/ComboboxGroup.tsx +27 -0
  83. package/src/components/combobox/ComboboxItem.tsx +44 -0
  84. package/src/components/combobox/ComboboxListBox.tsx +98 -0
  85. package/src/components/combobox/index.ts +6 -0
  86. package/src/components/datepicker/CalendarToggleButton.tsx +58 -0
  87. package/src/components/datepicker/ClearButton.tsx +35 -0
  88. package/src/components/datepicker/DateField.tsx +54 -0
  89. package/src/components/datepicker/DatePicker.scss +51 -0
  90. package/src/components/datepicker/DatePicker.tsx +574 -0
  91. package/src/components/datepicker/DateRangePicker.scss +11 -0
  92. package/src/components/datepicker/DateRangePicker.tsx +343 -0
  93. package/src/components/datepicker/index.ts +4 -0
  94. package/src/components/datepicker/renderDatePickerShell.tsx +104 -0
  95. package/src/components/drawer/Drawer.tsx +211 -0
  96. package/src/components/drawer/DrawerBody.tsx +47 -0
  97. package/src/components/drawer/DrawerFooter.tsx +51 -0
  98. package/src/components/drawer/DrawerHeader.tsx +60 -0
  99. package/src/components/drawer/DrawerTitle.tsx +49 -0
  100. package/src/components/drawer/index.ts +10 -0
  101. package/src/components/file-input/FileInput.tsx +100 -0
  102. package/src/components/file-input/index.ts +2 -0
  103. package/src/components/flex/Flex.tsx +134 -0
  104. package/src/components/flex/index.ts +2 -0
  105. package/src/components/floating-input/FloatingInput.tsx +94 -0
  106. package/src/components/floating-input/index.ts +2 -0
  107. package/src/components/form/Form.tsx +26 -0
  108. package/src/components/form/FormFeedback.tsx +64 -0
  109. package/src/components/form/FormHelp.tsx +46 -0
  110. package/src/components/form/FormLabel.tsx +26 -0
  111. package/src/components/form/index.ts +8 -0
  112. package/src/components/form/renderFormCheck.tsx +86 -0
  113. package/src/components/form-field/FormField.tsx +84 -0
  114. package/src/components/form-field/index.ts +2 -0
  115. package/src/components/form-field/renderFormField.tsx +74 -0
  116. package/src/components/grid/Col.tsx +69 -0
  117. package/src/components/grid/Container.tsx +59 -0
  118. package/src/components/grid/Grid.tsx +86 -0
  119. package/src/components/grid/GridItem.tsx +123 -0
  120. package/src/components/grid/Row.tsx +71 -0
  121. package/src/components/grid/gap.ts +8 -0
  122. package/src/components/grid/index.ts +10 -0
  123. package/src/components/icon/Icon.tsx +74 -0
  124. package/src/components/icon/index.ts +2 -0
  125. package/src/components/input-adorn/InputAdorn.tsx +55 -0
  126. package/src/components/input-adorn/index.ts +2 -0
  127. package/src/components/input-group/InputGroup.tsx +50 -0
  128. package/src/components/input-group/InputGroupAddon.tsx +50 -0
  129. package/src/components/input-group/index.ts +4 -0
  130. package/src/components/link/Link.tsx +148 -0
  131. package/src/components/link/index.ts +2 -0
  132. package/src/components/list/List.tsx +193 -0
  133. package/src/components/list/ListItem.tsx +91 -0
  134. package/src/components/list/index.ts +4 -0
  135. package/src/components/menu/Menu.tsx +398 -0
  136. package/src/components/menu/MenuDivider.tsx +19 -0
  137. package/src/components/menu/MenuHeader.tsx +55 -0
  138. package/src/components/menu/MenuItem.tsx +85 -0
  139. package/src/components/menu/MenuItemDef.ts +83 -0
  140. package/src/components/menu/MenuList.tsx +142 -0
  141. package/src/components/menu/MenuSubmenu.tsx +284 -0
  142. package/src/components/menu/MenuSubmenuBack.tsx +39 -0
  143. package/src/components/menu/MenuText.tsx +47 -0
  144. package/src/components/menu/MenuToggle.tsx +148 -0
  145. package/src/components/menu/index.ts +19 -0
  146. package/src/components/menu/menuNavigation.ts +106 -0
  147. package/src/components/menu/submenuGroup.ts +43 -0
  148. package/src/components/modal/Modal.tsx +162 -0
  149. package/src/components/modal/ModalBody.tsx +47 -0
  150. package/src/components/modal/ModalFooter.tsx +51 -0
  151. package/src/components/modal/ModalHeader.tsx +60 -0
  152. package/src/components/modal/ModalTitle.tsx +49 -0
  153. package/src/components/modal/index.ts +10 -0
  154. package/src/components/nav/Nav.tsx +97 -0
  155. package/src/components/nav/NavItem.tsx +42 -0
  156. package/src/components/nav/NavLink.tsx +48 -0
  157. package/src/components/nav/NavTitle.tsx +22 -0
  158. package/src/components/nav/index.ts +7 -0
  159. package/src/components/navbar/Navbar.tsx +112 -0
  160. package/src/components/navbar/NavbarBrand.tsx +55 -0
  161. package/src/components/navbar/NavbarNav.tsx +47 -0
  162. package/src/components/navbar/NavbarText.tsx +47 -0
  163. package/src/components/navbar/NavbarToggler.tsx +34 -0
  164. package/src/components/navbar/index.ts +10 -0
  165. package/src/components/notification/Notification.scss +15 -0
  166. package/src/components/notification/Notification.tsx +187 -0
  167. package/src/components/notification/NotificationIcon.tsx +21 -0
  168. package/src/components/notification/NotificationStack.tsx +66 -0
  169. package/src/components/notification/NotificationText.tsx +47 -0
  170. package/src/components/notification/NotificationTitle.tsx +47 -0
  171. package/src/components/notification/context.ts +14 -0
  172. package/src/components/notification/index.ts +17 -0
  173. package/src/components/notification/notificationQueue.ts +48 -0
  174. package/src/components/otp-input/OtpBox.tsx +68 -0
  175. package/src/components/otp-input/OtpInput.tsx +341 -0
  176. package/src/components/otp-input/index.ts +2 -0
  177. package/src/components/pagination/Pagination.tsx +199 -0
  178. package/src/components/pagination/PaginationItem.tsx +133 -0
  179. package/src/components/pagination/index.ts +4 -0
  180. package/src/components/password-strength/PasswordStrength.tsx +155 -0
  181. package/src/components/password-strength/index.ts +2 -0
  182. package/src/components/password-strength/strengthScore.ts +79 -0
  183. package/src/components/placeholder/Placeholder.tsx +178 -0
  184. package/src/components/placeholder/index.ts +2 -0
  185. package/src/components/popover/Popover.tsx +243 -0
  186. package/src/components/popover/index.ts +2 -0
  187. package/src/components/progress/Progress.tsx +154 -0
  188. package/src/components/progress/ProgressBar.tsx +88 -0
  189. package/src/components/progress/index.ts +4 -0
  190. package/src/components/radio/Radio.tsx +88 -0
  191. package/src/components/radio/RadioGroup.tsx +142 -0
  192. package/src/components/radio/context.ts +14 -0
  193. package/src/components/radio/index.ts +4 -0
  194. package/src/components/range-input/RangeInput.tsx +109 -0
  195. package/src/components/range-input/index.ts +2 -0
  196. package/src/components/select/Select.tsx +277 -0
  197. package/src/components/select/index.ts +2 -0
  198. package/src/components/skeleton/Skeleton.tsx +89 -0
  199. package/src/components/skeleton/SkeletonLoader.tsx +91 -0
  200. package/src/components/skeleton/index.ts +4 -0
  201. package/src/components/spinner/Spinner.tsx +77 -0
  202. package/src/components/spinner/index.ts +2 -0
  203. package/src/components/stack/Stack.tsx +81 -0
  204. package/src/components/stack/index.ts +2 -0
  205. package/src/components/stepper/Stepper.tsx +172 -0
  206. package/src/components/stepper/StepperItem.tsx +66 -0
  207. package/src/components/stepper/index.ts +4 -0
  208. package/src/components/switch/Switch.tsx +246 -0
  209. package/src/components/switch/index.ts +2 -0
  210. package/src/components/table/Table.css +39 -0
  211. package/src/components/table/Table.tsx +390 -0
  212. package/src/components/table/TableBody.tsx +25 -0
  213. package/src/components/table/TableCell.tsx +27 -0
  214. package/src/components/table/TableColumn.tsx +31 -0
  215. package/src/components/table/TableHeader.tsx +26 -0
  216. package/src/components/table/TableRow.tsx +24 -0
  217. package/src/components/table/index.ts +12 -0
  218. package/src/components/tabs/Tab.tsx +36 -0
  219. package/src/components/tabs/TabList.tsx +76 -0
  220. package/src/components/tabs/TabPanel.tsx +68 -0
  221. package/src/components/tabs/Tabs.tsx +136 -0
  222. package/src/components/tabs/context.ts +24 -0
  223. package/src/components/tabs/index.ts +8 -0
  224. package/src/components/text-input/TextInput.tsx +188 -0
  225. package/src/components/text-input/index.ts +2 -0
  226. package/src/components/textarea/Textarea.tsx +145 -0
  227. package/src/components/textarea/index.ts +2 -0
  228. package/src/components/toast/Toast.tsx +219 -0
  229. package/src/components/toast/ToastBody.tsx +64 -0
  230. package/src/components/toast/ToastFooter.tsx +47 -0
  231. package/src/components/toast/ToastHeader.tsx +104 -0
  232. package/src/components/toast/ToastIcon.tsx +21 -0
  233. package/src/components/toast/Toaster.tsx +87 -0
  234. package/src/components/toast/context.ts +14 -0
  235. package/src/components/toast/index.ts +18 -0
  236. package/src/components/toast/toastQueue.ts +56 -0
  237. package/src/components/tooltip/Tooltip.tsx +155 -0
  238. package/src/components/tooltip/index.ts +2 -0
  239. package/src/global.d.ts +2 -0
  240. package/src/hooks/index.ts +57 -0
  241. package/src/hooks/useAutoDismiss.ts +88 -0
  242. package/src/hooks/useButtonSemantics.ts +33 -0
  243. package/src/hooks/useControllableState.ts +24 -0
  244. package/src/hooks/useDialogElement.ts +216 -0
  245. package/src/hooks/useDisabledAnchorGuard.ts +22 -0
  246. package/src/hooks/useDismissibleTransition.ts +126 -0
  247. package/src/hooks/useDrawer.ts +9 -0
  248. package/src/hooks/useFloatingOverlay.ts +115 -0
  249. package/src/hooks/useForkedRef.ts +50 -0
  250. package/src/hooks/useFormField.ts +89 -0
  251. package/src/hooks/useIsomorphicLayoutEffect.ts +9 -0
  252. package/src/hooks/useModal.ts +9 -0
  253. package/src/hooks/useNotification.ts +10 -0
  254. package/src/hooks/useOverlayPlacement.ts +93 -0
  255. package/src/hooks/usePagination.ts +85 -0
  256. package/src/hooks/useToast.ts +9 -0
  257. package/src/hooks/useToastRegionQueue.ts +27 -0
  258. package/src/index.ts +276 -0
  259. package/src/types.ts +133 -0
  260. package/src/utils/breakpoints.ts +43 -0
  261. package/src/utils/comboboxCollection.tsx +152 -0
  262. package/src/utils/dateRangePresets.ts +6 -0
  263. package/src/utils/dialogTransition.ts +39 -0
  264. package/src/utils/focusRedirect.ts +29 -0
  265. package/src/utils/isWholeUnitDisabled.ts +17 -0
  266. package/src/utils/mergeIsDateUnavailable.ts +15 -0
  267. package/src/utils/overlayPlacement.ts +93 -0
  268. package/src/utils/pointerInteraction.ts +21 -0
  269. package/src/utils/polymorphic.ts +36 -0
  270. package/src/utils/renderMenuItemContent.tsx +62 -0
  271. package/src/utils/setVisibleRangeStart.ts +64 -0
  272. package/src/utils/spacingClassName.ts +14 -0
  273. package/src/utils/suppressFocusRingGlobally.ts +69 -0
  274. package/src/utils/validationClassName.ts +8 -0
  275. package/src/utils/virtualFocusStyle.ts +16 -0
@@ -0,0 +1,742 @@
1
+ import React, {
2
+ CSSProperties,
3
+ forwardRef,
4
+ HTMLAttributes,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState
9
+ } from 'react'
10
+ import classNames from 'classnames'
11
+
12
+ import { useControllableState, useForkedRef, useIsomorphicLayoutEffect } from '../../hooks'
13
+ import { focusRedirect } from '../../utils/focusRedirect'
14
+ import { consumePointerClick } from '../../utils/pointerInteraction'
15
+ import { CarouselContext, CarouselContextProps, CarouselEnds } from './context'
16
+ import {
17
+ animateScrollTo,
18
+ canLoop,
19
+ directionBetween,
20
+ getCarouselItems,
21
+ isElementVisible,
22
+ isRTL,
23
+ isViewportScrollable,
24
+ loopDirection,
25
+ normalizeIndex,
26
+ prefersReducedMotion,
27
+ scrollDeltaFor
28
+ } from './carouselEngine'
29
+
30
+ export type { CarouselEnds }
31
+ export type CarouselTransition = 'scroll' | 'fade'
32
+
33
+ export interface CarouselSlideDetail {
34
+ /**
35
+ * Direction of travel, mirrored in RTL.
36
+ */
37
+ direction: 'left' | 'right'
38
+ /**
39
+ * Index of the outgoing slide.
40
+ */
41
+ from: number
42
+ /**
43
+ * Index of the incoming slide.
44
+ */
45
+ to: number
46
+ }
47
+
48
+ export interface CarouselProps extends HTMLAttributes<HTMLDivElement> {
49
+ /**
50
+ * The active slide's index, for controlled usage. Pair with `onSlide` or `onSlid` to feed the
51
+ * new index back — they're the only way a controlled carousel's `activeIndex` gets updated.
52
+ */
53
+ activeIndex?: number
54
+ /**
55
+ * Let each CarouselItem size itself instead of dividing the track evenly; snap points still land on every slide.
56
+ */
57
+ auto?: boolean
58
+ /**
59
+ * Cycle through slides automatically on mount.
60
+ */
61
+ autoplay?: boolean
62
+ /**
63
+ * Snap the active slide to the center of the viewport instead of its start.
64
+ */
65
+ center?: boolean
66
+ /**
67
+ * A string of all className you want applied to the base component.
68
+ */
69
+ className?: string
70
+ /**
71
+ * The active slide's index, for uncontrolled usage.
72
+ */
73
+ defaultActiveIndex?: number
74
+ /**
75
+ * Behavior at the first/last slide. `loop` continues seamlessly past the ends for a single-slide layout, falling back to `wrap` for multi-item, peek, centered, or variable-width layouts, and under reduced motion. `wrap` jumps from the last slide back to the first, and vice versa. `stop` hard-stops and disables the previous/next controls at each end.
76
+ */
77
+ ends?: CarouselEnds
78
+ /**
79
+ * Milliseconds to wait before automatically advancing to the next slide. Override per slide with CarouselItem's own `interval` prop.
80
+ */
81
+ interval?: number
82
+ /**
83
+ * Number of whole slides visible per view, applied as the `--cx-carousel-items` custom property.
84
+ */
85
+ items?: number
86
+ /**
87
+ * Space between slides, applied as the `--cx-carousel-items-gap` custom property.
88
+ */
89
+ itemsGap?: string
90
+ /**
91
+ * How much of the neighboring slides to reveal, applied as the `--cx-carousel-items-peek` custom property.
92
+ */
93
+ itemsPeek?: string
94
+ /**
95
+ * Move to the previous/next slide with the Left/Right arrow keys while focus is inside the carousel.
96
+ */
97
+ keyboard?: boolean
98
+ /**
99
+ * Callback fired once a slide transition completes.
100
+ */
101
+ onSlid?: (detail: CarouselSlideDetail) => void
102
+ /**
103
+ * Callback fired when a slide transition begins.
104
+ */
105
+ onSlide?: (detail: CarouselSlideDetail) => void
106
+ /**
107
+ * Pause autoplay on `mouseenter`, resuming on `mouseleave`. Set to `false` to disable pause-on-hover.
108
+ */
109
+ pause?: 'hover' | false
110
+ /**
111
+ * Replace the scroll transition with a crossfade.
112
+ */
113
+ transition?: CarouselTransition
114
+ }
115
+
116
+ const ACTIVE_RATIO_TOLERANCE = 0.05
117
+
118
+ export const Carousel = forwardRef<HTMLDivElement, CarouselProps>(
119
+ (
120
+ {
121
+ children,
122
+ activeIndex: activeIndexProp,
123
+ auto,
124
+ autoplay = false,
125
+ center,
126
+ className,
127
+ defaultActiveIndex = 0,
128
+ ends = 'loop',
129
+ interval = 5000,
130
+ items,
131
+ itemsGap,
132
+ itemsPeek,
133
+ keyboard = true,
134
+ onSlid,
135
+ onSlide,
136
+ pause = 'hover',
137
+ style,
138
+ transition = 'scroll',
139
+ ...rest
140
+ },
141
+ ref
142
+ ) => {
143
+ // `onSlide`/`onSlid` — not a dedicated `onChange` — are the only way a controlled
144
+ // `activeIndex` gets fed back (see the CarouselProps `activeIndex` doc and carousel.mdx's
145
+ // "Controlled usage" section). Missing both means every click/swipe/autoplay advance updates
146
+ // internal state that `useControllableState` immediately discards (controlled mode always
147
+ // mirrors the prop), so the carousel silently freezes instead of erroring.
148
+ if (activeIndexProp !== undefined && !onSlide && !onSlid) {
149
+ console.warn(
150
+ 'Carousel: `activeIndex` is set (controlled) but neither `onSlide` nor `onSlid` is — ' +
151
+ 'nothing feeds the new index back to your state, so the carousel will visually freeze ' +
152
+ 'on click/swipe/autoplay. Pass `onSlide` or `onSlid` and update `activeIndex` from it.'
153
+ )
154
+ }
155
+
156
+ const carouselRef = useRef<HTMLDivElement>(null)
157
+ const forkedRef = useForkedRef(ref, carouselRef)
158
+
159
+ const [viewportEl, setViewportEl] = useState<HTMLDivElement | null>(null)
160
+ // Stable identity: this goes into `contextValue` and on to `useForkedRef` in `CarouselInner`,
161
+ // whose memo is keyed on the refs array — a fresh closure every render would keep detaching
162
+ // and reattaching the viewport ref instead of running once per mount.
163
+ const registerViewport = useCallback((node: HTMLDivElement | null) => setViewportEl(node), [])
164
+
165
+ const [activeIndex, setActiveIndex] = useControllableState(activeIndexProp, defaultActiveIndex)
166
+ const activeIndexRef = useRef(activeIndex)
167
+ useIsomorphicLayoutEffect(() => {
168
+ activeIndexRef.current = activeIndex
169
+ }, [activeIndex])
170
+
171
+ const [itemCount, setItemCount] = useState(0)
172
+ const itemCountRef = useRef(itemCount)
173
+ useIsomorphicLayoutEffect(() => {
174
+ itemCountRef.current = itemCount
175
+ }, [itemCount])
176
+
177
+ const [playing, setPlaying] = useState(autoplay)
178
+ const [cycling, setCycling] = useState(false)
179
+ const [atStart, setAtStart] = useState(true)
180
+ const [atEnd, setAtEnd] = useState(false)
181
+
182
+ const loopingRef = useRef(false)
183
+ // True while a programmatic scroll-sync animation is in flight, so the IntersectionObserver
184
+ // doesn't treat a slide passing through mid-scroll (e.g. slide 2 on the way from 1 to 3) as
185
+ // the new active slide and hijack the navigation before it reaches its real target.
186
+ const syncingScrollRef = useRef(false)
187
+ // Set right before an `activeIndex` change originates from the IntersectionObserver (i.e. the
188
+ // user physically scrolled/swiped there) so the scroll-sync effect below can skip it. Without
189
+ // this, every threshold the observer crosses mid-gesture — before the browser's own
190
+ // scroll-snap has actually settled the position — would make that effect "correct" the
191
+ // still-in-progress native scroll, fighting the user's own touch/wheel input.
192
+ const skipScrollSyncRef = useRef(false)
193
+ const cancelScrollSyncRef = useRef<(() => void) | null>(null)
194
+ const observerRef = useRef<IntersectionObserver | null>(null)
195
+ const visibilityRef = useRef(new Map<Element, number>())
196
+ const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
197
+ const scrollSyncedRef = useRef(false)
198
+ const mountedForSlidRef = useRef(false)
199
+ const mountedForLiveRegionRef = useRef(false)
200
+ const [liveMessage, setLiveMessage] = useState('')
201
+ // Set while the scroll-sync effect below owns firing `onSlid` for the in-flight
202
+ // `animateScrollTo` animation, so the generic effect doesn't fire it early — before the slide
203
+ // has actually finished sliding into place.
204
+ const deferOnSlidRef = useRef(false)
205
+ const lastTransitionRef = useRef<CarouselSlideDetail>({
206
+ from: activeIndex,
207
+ to: activeIndex,
208
+ direction: 'left'
209
+ })
210
+ const prevControlsRef = useRef<Set<HTMLButtonElement>>(new Set())
211
+ const nextControlsRef = useRef<Set<HTMLButtonElement>>(new Set())
212
+
213
+ const isFade = transition === 'fade'
214
+ const itemsVisible = Math.max(1, items ?? 1)
215
+ const wraps = ends !== 'stop'
216
+
217
+ const setActiveIndexInternal = (next: number) => {
218
+ // Update the ref synchronously, not just via the layout effect below — that effect only
219
+ // runs after React re-renders and commits, so two `next()` calls fired back-to-back in the
220
+ // same tick (a fast double-click, or any rapid programmatic call) would both read the same
221
+ // stale `activeIndexRef.current` and target the same index instead of advancing twice.
222
+ activeIndexRef.current = next
223
+ setActiveIndex(next)
224
+ }
225
+
226
+ // Index math alone (not real geometry) drives `atStart`/`atEnd` so end-control state and
227
+ // navigation clamping are correct even before layout exists (SSR, jsdom tests). The scroll
228
+ // listener below refines it further for peek/variable-width layouts, where a whole number of
229
+ // "visible items" can't be derived from `items` alone.
230
+ useIsomorphicLayoutEffect(() => {
231
+ const maxIndex = Math.max(0, itemCount - itemsVisible)
232
+ setAtStart(activeIndex <= 0)
233
+ setAtEnd(activeIndex >= maxIndex)
234
+ }, [activeIndex, itemCount, itemsVisible])
235
+
236
+ useEffect(() => {
237
+ if (!viewportEl) return undefined
238
+
239
+ const handleScroll = () => {
240
+ if (loopingRef.current || !isViewportScrollable(viewportEl)) return
241
+ const maxScroll = viewportEl.scrollWidth - viewportEl.clientWidth
242
+ // Elastic overscroll (iOS Safari/macOS trackpad rubber-banding) can push `scrollLeft`
243
+ // slightly past either edge mid-gesture. Clamp to the real [0, maxScroll] range (mirrored
244
+ // for RTL's negative `scrollLeft` convention) before the RTL-normalizing `abs()` below, so
245
+ // a bounce at the start edge doesn't briefly read as "away from start" - which flickered
246
+ // `ends="stop"` controls enabled while still at the first/last slide.
247
+ const rawScrollLeft = viewportEl.scrollLeft
248
+ const clampedScrollLeft = isRTL(viewportEl)
249
+ ? Math.max(-maxScroll, Math.min(0, rawScrollLeft))
250
+ : Math.max(0, Math.min(maxScroll, rawScrollLeft))
251
+ const scrollLeft = Math.abs(clampedScrollLeft)
252
+ setAtStart(scrollLeft <= 1)
253
+ setAtEnd(scrollLeft >= maxScroll - 1)
254
+ }
255
+
256
+ viewportEl.addEventListener('scroll', handleScroll, { passive: true })
257
+ return () => viewportEl.removeEventListener('scroll', handleScroll)
258
+ }, [viewportEl])
259
+
260
+ // A layout effect, not a plain effect: `itemCount` must be measured before paint so the
261
+ // atStart/atEnd layout effect above (which runs first, in declaration order) sees the real
262
+ // count once React re-renders to flush this effect's `setItemCount` — otherwise the first
263
+ // paint briefly shows `atEnd=true` (itemCount still 0) even on a carousel with many slides.
264
+ useIsomorphicLayoutEffect(() => {
265
+ if (!viewportEl) return undefined
266
+
267
+ const syncItems = () => {
268
+ const nodes = getCarouselItems(viewportEl)
269
+ setItemCount(nodes.length)
270
+
271
+ observerRef.current?.disconnect()
272
+ observerRef.current = null
273
+ visibilityRef.current = new Map()
274
+
275
+ if (isFade || typeof IntersectionObserver === 'undefined') return
276
+
277
+ observerRef.current = new IntersectionObserver(
278
+ (entries) => {
279
+ // Always merge entries so `visibilityRef` tracks reality even mid-animation —
280
+ // dropping them here would leave the outgoing slide's last-known ratio stale (e.g.
281
+ // still 1.0 from before the scroll started), which the comparison below would then
282
+ // wrongly treat as still the most-visible slide once the animation completes. Only
283
+ // acting on the result is guarded, so a slide passing through mid-programmatic-scroll
284
+ // can't hijack the navigation before it reaches its real target.
285
+ for (const entry of entries) {
286
+ visibilityRef.current.set(
287
+ entry.target,
288
+ entry.isIntersecting ? entry.intersectionRatio : 0
289
+ )
290
+ }
291
+
292
+ if (loopingRef.current || syncingScrollRef.current) return
293
+
294
+ const currentNodes = getCarouselItems(viewportEl)
295
+ const ratios = currentNodes.map((node) => visibilityRef.current.get(node) ?? 0)
296
+ const maxRatio = Math.max(0, ...ratios)
297
+ let bestIndex = activeIndexRef.current
298
+
299
+ if (maxRatio > 0) {
300
+ const found = ratios.findIndex((ratio) => ratio >= maxRatio - ACTIVE_RATIO_TOLERANCE)
301
+ if (found !== -1) bestIndex = found
302
+ }
303
+
304
+ if (bestIndex !== activeIndexRef.current) {
305
+ const from = activeIndexRef.current
306
+ const detail = {
307
+ from,
308
+ to: bestIndex,
309
+ direction: directionBetween(from, bestIndex, isRTL(viewportEl))
310
+ }
311
+ lastTransitionRef.current = detail
312
+ onSlide?.(detail)
313
+ skipScrollSyncRef.current = true
314
+ setActiveIndexInternal(bestIndex)
315
+ }
316
+ },
317
+ { root: viewportEl, threshold: [0, 0.25, 0.5, 0.75, 1] }
318
+ )
319
+
320
+ for (const node of nodes) observerRef.current.observe(node)
321
+ }
322
+
323
+ syncItems()
324
+ const mutationObserver = new MutationObserver(syncItems)
325
+ mutationObserver.observe(viewportEl, { childList: true })
326
+
327
+ return () => {
328
+ mutationObserver.disconnect()
329
+ observerRef.current?.disconnect()
330
+ }
331
+ // eslint-disable-next-line react-hooks/exhaustive-deps
332
+ }, [viewportEl, isFade])
333
+
334
+ // `activeIndex` is the source of truth for which slide is active — applied directly to the
335
+ // DOM here rather than through per-item props, so it stays correct even when there's no real
336
+ // layout to scroll (SSR, jsdom tests). Fade mode's crossfade is driven entirely by this class.
337
+ // The WAI-ARIA carousel pattern's per-slide `aria-roledescription`/positional `aria-label` are
338
+ // applied the same imperative way, for the same reason — `CarouselItem` itself has no idea
339
+ // where it sits among its siblings or how many there are.
340
+ useIsomorphicLayoutEffect(() => {
341
+ if (!viewportEl) return
342
+ const nodes = getCarouselItems(viewportEl)
343
+ for (const [index, node] of nodes.entries()) {
344
+ node.classList.toggle('active', index === activeIndex)
345
+ node.setAttribute('aria-roledescription', 'slide')
346
+ node.setAttribute('aria-label', `${index + 1} of ${nodes.length}`)
347
+ }
348
+ }, [viewportEl, activeIndex, itemCount])
349
+
350
+ // Best-effort visual sync: scrolls the real DOM position to match `activeIndex` after a
351
+ // programmatic navigation. A user-driven drag/swipe goes the other way (observer above), which
352
+ // sets `skipScrollSyncRef` so this effect no-ops for it instead of fighting a gesture that's
353
+ // still physically in progress — see the ref's own comment for why that matters.
354
+ useIsomorphicLayoutEffect(() => {
355
+ if (skipScrollSyncRef.current) {
356
+ skipScrollSyncRef.current = false
357
+ return
358
+ }
359
+ if (!viewportEl || loopingRef.current || isFade) return
360
+ const nodes = getCarouselItems(viewportEl)
361
+ const target = nodes[activeIndex]
362
+ if (!target || !isViewportScrollable(viewportEl)) return
363
+
364
+ // Marked as soon as this effect has a real viewport to sync against — on mount, even if
365
+ // there's nothing to scroll (delta below is ~0). Otherwise the mount pass never sets it
366
+ // (it returns early below without scrolling), and the *first real navigation* ends up
367
+ // mistaken for the initial sync and forced instant instead of animated.
368
+ const instant = !scrollSyncedRef.current
369
+ scrollSyncedRef.current = true
370
+
371
+ const delta = scrollDeltaFor(viewportEl, target, !!center)
372
+ if (Math.abs(delta) < 1) return
373
+
374
+ // Cancel any previous scroll-sync still animating so it can't finish later and clobber
375
+ // this newer target, and so `syncingScrollRef` isn't cleared early while this one runs.
376
+ cancelScrollSyncRef.current?.()
377
+
378
+ syncingScrollRef.current = true
379
+ viewportEl.style.scrollSnapType = 'none'
380
+
381
+ if (instant) {
382
+ viewportEl.scrollTo({ left: viewportEl.scrollLeft + delta, behavior: 'instant' })
383
+ viewportEl.style.scrollSnapType = ''
384
+ syncingScrollRef.current = false
385
+ cancelScrollSyncRef.current = null
386
+ } else {
387
+ deferOnSlidRef.current = true
388
+ cancelScrollSyncRef.current = animateScrollTo(
389
+ viewportEl,
390
+ viewportEl.scrollLeft + delta,
391
+ () => {
392
+ viewportEl.style.scrollSnapType = ''
393
+ syncingScrollRef.current = false
394
+ cancelScrollSyncRef.current = null
395
+ deferOnSlidRef.current = false
396
+ onSlid?.(lastTransitionRef.current)
397
+ }
398
+ )
399
+ }
400
+ // eslint-disable-next-line react-hooks/exhaustive-deps
401
+ }, [viewportEl, activeIndex, itemCount, center, isFade])
402
+
403
+ useEffect(() => {
404
+ if (!mountedForSlidRef.current) {
405
+ mountedForSlidRef.current = true
406
+ return
407
+ }
408
+ if (deferOnSlidRef.current) return
409
+ onSlid?.(lastTransitionRef.current)
410
+ // eslint-disable-next-line react-hooks/exhaustive-deps
411
+ }, [activeIndex])
412
+
413
+ // Announces the active slide's position to assistive tech, per the WAI-ARIA carousel pattern.
414
+ // Gated the same way as the `onSlid` effect above so mounting with a non-zero
415
+ // `defaultActiveIndex`/`activeIndex` doesn't announce anything before the user has interacted.
416
+ useEffect(() => {
417
+ if (!mountedForLiveRegionRef.current) {
418
+ mountedForLiveRegionRef.current = true
419
+ return
420
+ }
421
+ setLiveMessage(`Slide ${activeIndex + 1} of ${itemCount}`)
422
+ // eslint-disable-next-line react-hooks/exhaustive-deps
423
+ }, [activeIndex])
424
+
425
+ useEffect(() => {
426
+ return () => {
427
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
428
+ observerRef.current?.disconnect()
429
+ cancelScrollSyncRef.current?.()
430
+ }
431
+ }, [])
432
+
433
+ const performLoopTransition = (isNext: boolean) => {
434
+ const viewport = viewportEl
435
+ if (!viewport) return
436
+
437
+ // Drop any in-flight regular scroll-sync (e.g. a fast second navigation crossing the loop
438
+ // boundary) so its stale onSettled can't fire mid-loop or after this transition completes.
439
+ cancelScrollSyncRef.current?.()
440
+ syncingScrollRef.current = false
441
+
442
+ const nodes = getCarouselItems(viewport)
443
+ const last = nodes.length - 1
444
+ const from = activeIndexRef.current
445
+ const to = isNext ? 0 : last
446
+ const direction = loopDirection(isNext, isRTL(viewport))
447
+
448
+ onSlide?.({ from, to, direction })
449
+ loopingRef.current = true
450
+
451
+ const finish = () => {
452
+ lastTransitionRef.current = { from, to, direction }
453
+ setActiveIndexInternal(to)
454
+ loopingRef.current = false
455
+ }
456
+
457
+ const source = isNext ? nodes[0] : nodes[last]
458
+ const fromItem = nodes[from]
459
+ const toItem = nodes[to]
460
+
461
+ if (!isViewportScrollable(viewport) || !source || !fromItem || !toItem) {
462
+ finish()
463
+ return
464
+ }
465
+
466
+ const clone = source.cloneNode(true) as HTMLElement
467
+ clone.classList.add('carousel-item-clone')
468
+ clone.classList.remove('active')
469
+ clone.removeAttribute('id')
470
+ for (const node of Array.from(clone.querySelectorAll('[id]'))) node.removeAttribute('id')
471
+ clone.setAttribute('aria-hidden', 'true')
472
+ clone.setAttribute('inert', '')
473
+
474
+ viewport.style.scrollSnapType = 'none'
475
+
476
+ if (isNext) {
477
+ viewport.append(clone)
478
+ } else {
479
+ viewport.prepend(clone)
480
+ // Prepending shifts the real slides right; realign instantly so the insertion doesn't
481
+ // flash. `scrollTo(..., 'instant')` rather than a raw `scrollLeft +=` — the compound
482
+ // assignment's read/write isn't reliably observed by the browser right after a DOM
483
+ // mutation changes `scrollWidth` (see the matching note by the final jump below).
484
+ viewport.scrollTo({
485
+ left: viewport.scrollLeft + scrollDeltaFor(viewport, fromItem, !!center),
486
+ behavior: 'instant'
487
+ })
488
+ }
489
+
490
+ const targetDelta = scrollDeltaFor(viewport, clone, !!center)
491
+ animateScrollTo(viewport, viewport.scrollLeft + targetDelta, () => {
492
+ clone.remove()
493
+
494
+ // `clone.remove()` shrinks `scrollWidth`, and the browser clamps `scrollLeft` to the new
495
+ // max as a side effect — sometimes before a subsequent `scrollLeft +=` on the same tick
496
+ // observes it, landing short of the real target. `scrollTo(..., 'instant')` with the full
497
+ // absolute target doesn't have that race.
498
+ const finalDelta = scrollDeltaFor(viewport, toItem, !!center)
499
+ viewport.scrollTo({ left: viewport.scrollLeft + finalDelta, behavior: 'instant' })
500
+
501
+ viewport.style.scrollSnapType = ''
502
+ finish()
503
+ })
504
+ }
505
+
506
+ const navigate = (rawTarget: number) => {
507
+ if (loopingRef.current) return
508
+ const length = itemCountRef.current
509
+ if (length === 0) return
510
+
511
+ const effectiveLast = Math.max(0, length - itemsVisible)
512
+
513
+ if (
514
+ ends === 'loop' &&
515
+ !isFade &&
516
+ !prefersReducedMotion() &&
517
+ carouselRef.current &&
518
+ canLoop(carouselRef.current, length, isFade, !!center, !!auto)
519
+ ) {
520
+ if (rawTarget > effectiveLast) {
521
+ performLoopTransition(true)
522
+ return
523
+ }
524
+ if (rawTarget < 0) {
525
+ performLoopTransition(false)
526
+ return
527
+ }
528
+ }
529
+
530
+ const target = normalizeIndex(rawTarget, effectiveLast + 1, wraps)
531
+ const from = activeIndexRef.current
532
+ if (target === null || target === from) return
533
+
534
+ // `rawTarget`, not the post-wrap `target`, so a wrap-around still reports the direction the
535
+ // caller actually asked for (e.g. Next wrapping last -> 0 stays "next", not "previous").
536
+ const direction = directionBetween(from, rawTarget, viewportEl ? isRTL(viewportEl) : false)
537
+ onSlide?.({ from, to: target, direction })
538
+ lastTransitionRef.current = { from, to: target, direction }
539
+ setActiveIndexInternal(target)
540
+ }
541
+
542
+ const goNext = () => navigate(activeIndexRef.current + 1)
543
+ const goPrev = () => navigate(activeIndexRef.current - 1)
544
+ const goTo = (index: number) => navigate(index)
545
+
546
+ const pauseCycle = () => {
547
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
548
+ timeoutRef.current = null
549
+ setCycling(false)
550
+ }
551
+
552
+ const scheduleAutoplay = (fromIndex: number = activeIndexRef.current) => {
553
+ if (timeoutRef.current) clearTimeout(timeoutRef.current)
554
+
555
+ const nodes = viewportEl ? getCarouselItems(viewportEl) : []
556
+ const attr = nodes[fromIndex]?.dataset.interval
557
+ const ms = attr && !Number.isNaN(Number(attr)) ? Number(attr) : interval
558
+ carouselRef.current?.style.setProperty('--cx-carousel-interval', `${ms}ms`)
559
+
560
+ timeoutRef.current = setTimeout(() => {
561
+ const length = itemCountRef.current
562
+ const effectiveLast = Math.max(0, length - itemsVisible)
563
+ const upcoming = normalizeIndex(activeIndexRef.current + 1, effectiveLast + 1, wraps)
564
+ const canAdvance =
565
+ typeof document === 'undefined' ||
566
+ (document.visibilityState === 'visible' && isElementVisible(carouselRef.current))
567
+
568
+ if (!canAdvance) {
569
+ scheduleAutoplay(fromIndex)
570
+ return
571
+ }
572
+
573
+ goNext()
574
+
575
+ if (upcoming === null) {
576
+ pauseCycle()
577
+ return
578
+ }
579
+
580
+ scheduleAutoplay(upcoming)
581
+ }, ms)
582
+ }
583
+
584
+ const startCycle = () => {
585
+ scheduleAutoplay()
586
+ setCycling(true)
587
+ }
588
+
589
+ const stopPlayingForGood = () => {
590
+ setPlaying(false)
591
+ pauseCycle()
592
+ }
593
+
594
+ const togglePlayPause = () => {
595
+ if (playing) {
596
+ stopPlayingForGood()
597
+ return
598
+ }
599
+ setPlaying(true)
600
+ startCycle()
601
+ }
602
+
603
+ useEffect(() => {
604
+ if (!viewportEl) return undefined
605
+ if (playing) startCycle()
606
+ return () => pauseCycle()
607
+ // eslint-disable-next-line react-hooks/exhaustive-deps
608
+ }, [viewportEl])
609
+
610
+ useEffect(() => {
611
+ if (!viewportEl) return undefined
612
+ const handlePointerDown = () => stopPlayingForGood()
613
+ viewportEl.addEventListener('pointerdown', handlePointerDown)
614
+ return () => viewportEl.removeEventListener('pointerdown', handlePointerDown)
615
+ // eslint-disable-next-line react-hooks/exhaustive-deps
616
+ }, [viewportEl])
617
+
618
+ // a11y: when a control at a disabled end is focused, move focus to the opposite control (if
619
+ // it stays enabled) or the viewport, so focus never lands on nothing.
620
+ useEffect(() => {
621
+ if (ends !== 'stop') return
622
+ const focused = document.activeElement as HTMLButtonElement | null
623
+ const focusedIsPrev = atStart && !!focused && prevControlsRef.current.has(focused)
624
+ const focusedIsNext = atEnd && !!focused && nextControlsRef.current.has(focused)
625
+ if (!focusedIsPrev && !focusedIsNext) return
626
+
627
+ const oppositeSet = focusedIsPrev ? nextControlsRef.current : prevControlsRef.current
628
+ const oppositeStaysEnabled = focusedIsPrev ? !atEnd : !atStart
629
+ const fallback = oppositeStaysEnabled ? Array.from(oppositeSet)[0] : null
630
+ // `focused` is the control whose click (if any) just drove this redirect — `consumePointerClick`
631
+ // reads (and clears) the pointer-vs-keyboard mark left on it, if `CarouselControlPrev`/
632
+ // `CarouselControlNext` left one, so a stale mark from an earlier, unrelated interaction can't
633
+ // leak into this one. See `focusRedirect`'s own comment for why this distinction matters
634
+ // (Safari-only: a script-focused element still shows a ring there, unlike Chromium/Firefox).
635
+ const viaPointer = consumePointerClick(focused)
636
+
637
+ if (fallback) {
638
+ focusRedirect(fallback, viaPointer, { preventScroll: true })
639
+ return
640
+ }
641
+
642
+ if (viewportEl) {
643
+ if (!viewportEl.hasAttribute('tabindex')) viewportEl.setAttribute('tabindex', '-1')
644
+ focusRedirect(viewportEl, viaPointer, { preventScroll: true })
645
+ }
646
+ }, [atStart, atEnd, ends, viewportEl])
647
+
648
+ // Stable identity for the same reason as `registerViewport` above — the controls' own
649
+ // registration effect depends on it.
650
+ const registerControl = useCallback((kind: 'prev' | 'next', element: HTMLButtonElement) => {
651
+ const set = kind === 'prev' ? prevControlsRef.current : nextControlsRef.current
652
+ set.add(element)
653
+ return () => {
654
+ set.delete(element)
655
+ }
656
+ }, [])
657
+
658
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
659
+ if (!keyboard) return
660
+ const target = event.target as HTMLElement
661
+ if (target.closest('input, textarea, select, [contenteditable="true"]')) return
662
+ if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
663
+
664
+ event.preventDefault()
665
+ stopPlayingForGood()
666
+ if (event.key === 'ArrowLeft') goPrev()
667
+ else goNext()
668
+ }
669
+
670
+ const handleMouseEnter = () => {
671
+ if (pause === 'hover') pauseCycle()
672
+ }
673
+
674
+ const handleMouseLeave = () => {
675
+ if (pause === 'hover' && playing) startCycle()
676
+ }
677
+
678
+ const contextValue: CarouselContextProps = {
679
+ activeIndex,
680
+ atEnd,
681
+ atStart,
682
+ ends,
683
+ itemCount,
684
+ itemsVisible,
685
+ next: () => {
686
+ stopPlayingForGood()
687
+ goNext()
688
+ },
689
+ playing,
690
+ prev: () => {
691
+ stopPlayingForGood()
692
+ goPrev()
693
+ },
694
+ registerControl,
695
+ registerViewport,
696
+ to: (index: number) => {
697
+ stopPlayingForGood()
698
+ goTo(index)
699
+ },
700
+ togglePlayPause
701
+ }
702
+
703
+ const _className = classNames(
704
+ 'carousel',
705
+ {
706
+ 'carousel-fade': isFade,
707
+ 'carousel-center': center,
708
+ 'carousel-auto': auto,
709
+ 'carousel-playing': cycling
710
+ },
711
+ className
712
+ )
713
+
714
+ const _style = {
715
+ ...style,
716
+ ...(items != null && { '--cx-carousel-items': items }),
717
+ ...(itemsGap != null && { '--cx-carousel-items-gap': itemsGap }),
718
+ ...(itemsPeek != null && { '--cx-carousel-items-peek': itemsPeek })
719
+ } as CSSProperties
720
+
721
+ return (
722
+ <div
723
+ role="region"
724
+ aria-roledescription="carousel"
725
+ className={_className}
726
+ style={_style}
727
+ onKeyDown={handleKeyDown}
728
+ onMouseEnter={handleMouseEnter}
729
+ onMouseLeave={handleMouseLeave}
730
+ {...rest}
731
+ ref={forkedRef}
732
+ >
733
+ <CarouselContext.Provider value={contextValue}>{children}</CarouselContext.Provider>
734
+ <span role="status" className="visually-hidden">
735
+ {liveMessage}
736
+ </span>
737
+ </div>
738
+ )
739
+ }
740
+ )
741
+
742
+ Carousel.displayName = 'Carousel'