@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,474 @@
1
+ import { Ref, useEffect, useId } from "react";
2
+
3
+ import { Portal } from "../Portal";
4
+ import { Slot, composeEventHandlers, composeRefs } from "../Slot";
5
+
6
+ import { ModalProvider } from "./ModalContext";
7
+ import {
8
+ useModalContent,
9
+ useModalContext,
10
+ useModalRoot,
11
+ useModalTrigger,
12
+ } from "./hooks";
13
+ import {
14
+ ModalCloseProps,
15
+ ModalContentProps,
16
+ ModalDescriptionProps,
17
+ ModalOverlayProps,
18
+ ModalPortalProps,
19
+ ModalRootProps,
20
+ ModalTitleProps,
21
+ ModalTriggerProps,
22
+ } from "./types";
23
+
24
+ /**
25
+ * The root of a Modal — owns open state, registers the dialog's id,
26
+ * title id, and description id, and exposes an imperative handle for
27
+ * opening / closing from outside the React subtree.
28
+ *
29
+ * Supports two state modes, statically discriminated at the type level:
30
+ *
31
+ * - **Uncontrolled** — pass {@link ModalRootProps.defaultOpen | `defaultOpen`}
32
+ * (or omit it for closed-on-mount). The component owns the open flag
33
+ * internally.
34
+ * - **Controlled** — pass {@link ModalRootProps.open | `open`} *and*
35
+ * {@link ModalRootProps.onOpenChange | `onOpenChange`} together. The
36
+ * parent owns the flag; the component defers every state change back
37
+ * through the callback.
38
+ *
39
+ * The imperative API is exposed via `ref`:
40
+ *
41
+ * ```tsx
42
+ * const ref = useRef<ModalImperativeApi>(null);
43
+ * ref.current?.open();
44
+ * ref.current?.close();
45
+ * ```
46
+ *
47
+ * In controlled mode the imperative methods call `onOpenChange` rather
48
+ * than flipping any internal state — the parent stays in charge.
49
+ *
50
+ * @example Uncontrolled
51
+ * ```tsx
52
+ * <Modal.Root defaultOpen>
53
+ * <Modal.Content>…</Modal.Content>
54
+ * </Modal.Root>
55
+ * ```
56
+ *
57
+ * @example Controlled
58
+ * ```tsx
59
+ * const [open, setOpen] = useState(false);
60
+ *
61
+ * <Modal.Root open={open} onOpenChange={setOpen}>
62
+ * <Modal.Trigger>Open</Modal.Trigger>
63
+ * <Modal.Portal>
64
+ * <Modal.Overlay />
65
+ * <Modal.Content>…</Modal.Content>
66
+ * </Modal.Portal>
67
+ * </Modal.Root>
68
+ * ```
69
+ */
70
+ function ModalRoot({
71
+ ref,
72
+ children,
73
+ defaultOpen,
74
+ open,
75
+ onOpenChange,
76
+ }: ModalRootProps) {
77
+ const { contextValue } = useModalRoot(
78
+ { defaultOpen, open, onOpenChange },
79
+ ref,
80
+ );
81
+
82
+ return <ModalProvider value={contextValue}>{children}</ModalProvider>;
83
+ }
84
+
85
+ ModalRoot.displayName = "ModalRoot";
86
+
87
+ /**
88
+ * A button that toggles the modal open. Renders a
89
+ * `<button type="button">` with full ARIA wiring:
90
+ *
91
+ * - `aria-haspopup="dialog"`
92
+ * - `aria-expanded` tracks open state
93
+ * - `aria-controls` points at the `Modal.Content` dialog's id
94
+ *
95
+ * **`asChild` prop.** Pass `asChild` to render any consumer-supplied
96
+ * element (e.g. a router `<Link>`) with the trigger's ARIA attributes,
97
+ * composed event handlers, and ref merged in.
98
+ *
99
+ * @example
100
+ * ```tsx
101
+ * <Modal.Trigger>Open</Modal.Trigger>
102
+ *
103
+ * <Modal.Trigger asChild>
104
+ * <Link to="/upgrade">Upgrade</Link>
105
+ * </Modal.Trigger>
106
+ * ```
107
+ */
108
+ function ModalTrigger({
109
+ onClick,
110
+ type,
111
+ asChild = false,
112
+ ...rest
113
+ }: ModalTriggerProps) {
114
+ const { getTriggerProps } = useModalTrigger(onClick, rest);
115
+
116
+ if (asChild) {
117
+ return <Slot {...getTriggerProps()} />;
118
+ }
119
+
120
+ return <button type={type ?? "button"} {...getTriggerProps()} />;
121
+ }
122
+
123
+ ModalTrigger.displayName = "ModalTrigger";
124
+
125
+ /**
126
+ * Renders its children through `React.createPortal` so the dialog is
127
+ * detached from wherever `Modal.Root` lives in the React tree and
128
+ * becomes a direct child of {@link ModalPortalProps.container | `container`}
129
+ * (default `document.body`).
130
+ *
131
+ * By default the portal only renders while the modal is open. Pass
132
+ * {@link ModalPortalProps.forceMount | `forceMount`} to keep the
133
+ * subtree in the DOM after `open` flips false — useful for CSS exit
134
+ * animations driven by `data-state="closed"`.
135
+ *
136
+ * @example
137
+ * ```tsx
138
+ * <Modal.Portal container={document.getElementById("modal-root")!}>
139
+ * <Modal.Overlay />
140
+ * <Modal.Content>…</Modal.Content>
141
+ * </Modal.Portal>
142
+ * ```
143
+ */
144
+ function ModalPortal({ children, container, forceMount }: ModalPortalProps) {
145
+ const { open } = useModalContext();
146
+
147
+ if (!open && !forceMount) return null;
148
+
149
+ return <Portal container={container}>{children}</Portal>;
150
+ }
151
+
152
+ ModalPortal.displayName = "ModalPortal";
153
+
154
+ /**
155
+ * A decorative, animation-friendly backdrop rendered as a **sibling**
156
+ * of `Modal.Content`. It is **not** an event surface for
157
+ * click-outside — opening a native `<dialog>` via `showModal()`
158
+ * promotes it to the top layer with a browser-painted `::backdrop`
159
+ * that sits *above* this overlay, so clicks on the visible backdrop
160
+ * never reach the overlay div. Click-outside-to-close is wired on
161
+ * the dialog itself via {@link ModalContentCallbacks.onPointerDownOutside | `onPointerDownOutside`}.
162
+ *
163
+ * - `aria-hidden="true"` (the backdrop is decorative).
164
+ * - `data-state="open" | "closed"` follows the modal's open state.
165
+ * - Consumer `onClick` (and any other event handler) is forwarded
166
+ * through — the overlay does not compose or intercept them.
167
+ *
168
+ * **`asChild` prop.** Pass `asChild` to render a consumer-supplied
169
+ * element (e.g. a motion wrapper) with the overlay's ARIA and
170
+ * data-state merged in.
171
+ *
172
+ * **`forceMount` prop.** Pass `forceMount` to keep the overlay in the
173
+ * DOM while `open` is false so a CSS exit animation can play.
174
+ *
175
+ * @example
176
+ * ```tsx
177
+ * <Modal.Overlay />
178
+ *
179
+ * <Modal.Overlay asChild forceMount>
180
+ * <motion.div
181
+ * initial={{ opacity: 0 }}
182
+ * animate={{ opacity: 1 }}
183
+ * exit={{ opacity: 0 }}
184
+ * />
185
+ * </Modal.Overlay>
186
+ * ```
187
+ */
188
+ function ModalOverlay({
189
+ asChild = false,
190
+ forceMount,
191
+ ...rest
192
+ }: ModalOverlayProps) {
193
+ const { open } = useModalContext();
194
+ if (!open && !forceMount) return null;
195
+ const overlayProps = {
196
+ ...rest,
197
+ "aria-hidden": "true" as const,
198
+ "data-state": (open ? "open" : "closed") as "open" | "closed",
199
+ };
200
+
201
+ if (asChild) {
202
+ return <Slot {...overlayProps} />;
203
+ }
204
+
205
+ return <div {...overlayProps} />;
206
+ }
207
+
208
+ ModalOverlay.displayName = "ModalOverlay";
209
+
210
+ /**
211
+ * The native `<dialog>` element. React drives its `showModal()` and
212
+ * `close()` calls in response to `open` state changes, letting the
213
+ * browser own focus trapping, inert background, top-layer rendering,
214
+ * and the `Esc` key.
215
+ *
216
+ * **ARIA wiring.** `aria-labelledby` and `aria-describedby` are set
217
+ * automatically when `Modal.Title` and `Modal.Description` are rendered
218
+ * as descendants. The dialog has an implicit `role="dialog"` — do not
219
+ * pass one explicitly (WCAG redundancy rule).
220
+ *
221
+ * **Escape hatches.**
222
+ * - {@link ModalContentCallbacks.onEscapeKeyDown | `onEscapeKeyDown`}
223
+ * fires on the native `cancel` event (Esc). Consumers can
224
+ * `event.preventDefault()` to keep the modal open.
225
+ * - {@link ModalContentCallbacks.onPointerDownOutside | `onPointerDownOutside`}
226
+ * fires on a `pointerdown` whose coordinates land outside the
227
+ * dialog's bounding rect — i.e. on the native `::backdrop`. Consumers
228
+ * can `event.preventDefault()` to keep the modal open. The native
229
+ * `PointerEvent` is passed through, not a synthetic React event.
230
+ *
231
+ * **`asChild` is intentionally not supported.** The native dialog
232
+ * primitive is what provides the focus trap and the inert background;
233
+ * swapping it for a `<div>` would break both.
234
+ *
235
+ * **Styling hook.** `data-state="open" | "closed"` on the dialog.
236
+ *
237
+ * @example
238
+ * ```tsx
239
+ * <Modal.Content
240
+ * onEscapeKeyDown={(event) => {
241
+ * if (hasUnsavedChanges) event.preventDefault();
242
+ * }}
243
+ * >
244
+ * …
245
+ * </Modal.Content>
246
+ * ```
247
+ */
248
+ function ModalContent({
249
+ children,
250
+ id,
251
+ onEscapeKeyDown,
252
+ onPointerDownOutside,
253
+ ref: externalRef,
254
+ ...rest
255
+ }: ModalContentProps & { ref?: Ref<HTMLDialogElement> }) {
256
+ const { ref: innerRef, open, contentId } = useModalContent();
257
+ const { contentCallbacksRef, titleId, descriptionId } = useModalContext();
258
+ // Keep the ref pointed at the latest callbacks so event handlers wired
259
+ // through context (dialog pointerdown, native cancel) always see the most
260
+ // recent consumer props across re-renders.
261
+ contentCallbacksRef.current = { onEscapeKeyDown, onPointerDownOutside };
262
+ const composedRef = externalRef
263
+ ? composeRefs(innerRef, externalRef)
264
+ : innerRef;
265
+
266
+ return (
267
+ <dialog
268
+ ref={composedRef}
269
+ id={id ?? contentId}
270
+ data-state={open ? "open" : "closed"}
271
+ aria-labelledby={titleId}
272
+ aria-describedby={descriptionId}
273
+ {...rest}
274
+ >
275
+ {children}
276
+ </dialog>
277
+ );
278
+ }
279
+
280
+ ModalContent.displayName = "ModalContent";
281
+
282
+ /**
283
+ * The dialog's accessible name. Renders an `<h2>` by default and
284
+ * auto-registers its generated id with `Modal.Root` so
285
+ * `Modal.Content` can wire it up as `aria-labelledby`.
286
+ *
287
+ * Pass `asChild` to render the consumer's own heading element (e.g.
288
+ * an `<h3>` for a nested dialog or a styled heading component); the
289
+ * id is still registered.
290
+ *
291
+ * @example
292
+ * ```tsx
293
+ * <Modal.Title>Payment</Modal.Title>
294
+ *
295
+ * <Modal.Title asChild>
296
+ * <h3>Nested section</h3>
297
+ * </Modal.Title>
298
+ * ```
299
+ */
300
+ function ModalTitle({ children, asChild = false, ...rest }: ModalTitleProps) {
301
+ const { registerTitle } = useModalContext();
302
+ const id = useId();
303
+
304
+ useEffect(() => {
305
+ registerTitle(id);
306
+ return () => registerTitle(undefined);
307
+ }, [registerTitle, id]);
308
+
309
+ if (asChild) {
310
+ return (
311
+ <Slot id={id} {...rest}>
312
+ {children}
313
+ </Slot>
314
+ );
315
+ }
316
+
317
+ return (
318
+ <h2 id={id} {...rest}>
319
+ {children}
320
+ </h2>
321
+ );
322
+ }
323
+
324
+ ModalTitle.displayName = "ModalTitle";
325
+
326
+ /**
327
+ * The dialog's accessible description. Renders a `<p>` by default and
328
+ * auto-registers its generated id with `Modal.Root` so
329
+ * `Modal.Content` can wire it up as `aria-describedby`.
330
+ *
331
+ * Pass `asChild` to render any consumer-supplied element; the id is
332
+ * still registered.
333
+ *
334
+ * @example
335
+ * ```tsx
336
+ * <Modal.Description>
337
+ * Enter your card details below. You can change your plan later.
338
+ * </Modal.Description>
339
+ * ```
340
+ */
341
+ function ModalDescription({
342
+ children,
343
+ asChild = false,
344
+ ...rest
345
+ }: ModalDescriptionProps) {
346
+ const { registerDescription } = useModalContext();
347
+ const id = useId();
348
+
349
+ useEffect(() => {
350
+ registerDescription(id);
351
+ return () => registerDescription(undefined);
352
+ }, [registerDescription, id]);
353
+
354
+ if (asChild) {
355
+ return (
356
+ <Slot id={id} {...rest}>
357
+ {children}
358
+ </Slot>
359
+ );
360
+ }
361
+
362
+ return (
363
+ <p id={id} {...rest}>
364
+ {children}
365
+ </p>
366
+ );
367
+ }
368
+
369
+ ModalDescription.displayName = "ModalDescription";
370
+
371
+ /**
372
+ * A button that closes the modal. Renders a `<button type="button">`
373
+ * whose `onClick` is composed with `setOpen(false)` — consumer
374
+ * handlers run first and can `event.preventDefault()` to veto closing.
375
+ *
376
+ * **`asChild` prop.** Pass `asChild` to render any element (e.g. a
377
+ * text link or an icon-only `<button>` of your own) with the close
378
+ * behaviour merged in.
379
+ *
380
+ * @example
381
+ * ```tsx
382
+ * <Modal.Close>Cancel</Modal.Close>
383
+ *
384
+ * <Modal.Close asChild>
385
+ * <IconButton aria-label="Close" icon={<XIcon />} />
386
+ * </Modal.Close>
387
+ * ```
388
+ */
389
+ function ModalClose({ onClick, asChild = false, ...rest }: ModalCloseProps) {
390
+ const { setOpen } = useModalContext();
391
+ const closeProps = {
392
+ ...rest,
393
+ onClick: composeEventHandlers(onClick, () => setOpen(false)),
394
+ };
395
+
396
+ if (asChild) {
397
+ return <Slot {...closeProps} />;
398
+ }
399
+
400
+ return <button type="button" {...closeProps} />;
401
+ }
402
+
403
+ ModalClose.displayName = "ModalClose";
404
+
405
+ type ModalCompound = typeof ModalRoot & {
406
+ Root: typeof ModalRoot;
407
+ Trigger: typeof ModalTrigger;
408
+ Portal: typeof ModalPortal;
409
+ Overlay: typeof ModalOverlay;
410
+ Content: typeof ModalContent;
411
+ Title: typeof ModalTitle;
412
+ Description: typeof ModalDescription;
413
+ Close: typeof ModalClose;
414
+ };
415
+
416
+ /**
417
+ * Headless, accessible **Modal** — a compound component built on the
418
+ * native `<dialog>` element and its `showModal()` API. Implements the
419
+ * [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)
420
+ * with zero styles.
421
+ *
422
+ * `Modal` is both callable (it's an alias of {@link ModalRoot | `Modal.Root`})
423
+ * and carries its sub-components as static properties. Prefer the
424
+ * namespaced form in application code for readability and grep-ability.
425
+ *
426
+ * - {@link ModalRoot | `Modal.Root`} — state owner, context provider, imperative API holder.
427
+ * - {@link ModalTrigger | `Modal.Trigger`} — `<button>` that opens the modal.
428
+ * - {@link ModalPortal | `Modal.Portal`} — `createPortal` wrapper with `container` + `forceMount`.
429
+ * - {@link ModalOverlay | `Modal.Overlay`} — click-outside backdrop sibling of the dialog.
430
+ * - {@link ModalContent | `Modal.Content`} — native `<dialog>` with escape hatches and auto-ARIA.
431
+ * - {@link ModalTitle | `Modal.Title`} — accessible name; auto-wires `aria-labelledby`.
432
+ * - {@link ModalDescription | `Modal.Description`} — auto-wires `aria-describedby`.
433
+ * - {@link ModalClose | `Modal.Close`} — `<button>` that closes the modal.
434
+ *
435
+ * @example Minimal usage
436
+ * ```tsx
437
+ * import { Modal } from "@primitiv-ui/react";
438
+ *
439
+ * <Modal.Root>
440
+ * <Modal.Trigger>Open</Modal.Trigger>
441
+ * <Modal.Portal>
442
+ * <Modal.Overlay />
443
+ * <Modal.Content>
444
+ * <Modal.Title>Payment</Modal.Title>
445
+ * <Modal.Description>Enter your card details</Modal.Description>
446
+ * <Modal.Close>Cancel</Modal.Close>
447
+ * </Modal.Content>
448
+ * </Modal.Portal>
449
+ * </Modal.Root>;
450
+ * ```
451
+ *
452
+ * @example Scroll lock (CSS only)
453
+ * ```css
454
+ * html:has(dialog[open]) { overflow: hidden; }
455
+ * ```
456
+ *
457
+ * @see {@link ModalRoot} for state modes and the imperative API.
458
+ * @see {@link ModalContent} for the escape hatches and ARIA auto-wiring.
459
+ * @see {@link ModalOverlay} for the click-outside contract.
460
+ */
461
+ const ModalCompound: ModalCompound = Object.assign(ModalRoot, {
462
+ Root: ModalRoot,
463
+ Trigger: ModalTrigger,
464
+ Portal: ModalPortal,
465
+ Overlay: ModalOverlay,
466
+ Content: ModalContent,
467
+ Title: ModalTitle,
468
+ Description: ModalDescription,
469
+ Close: ModalClose,
470
+ });
471
+
472
+ ModalCompound.displayName = "Modal";
473
+
474
+ export { ModalCompound as Modal };
@@ -0,0 +1,13 @@
1
+ import { createStrictContext } from "../utils";
2
+
3
+ import { ModalContextValue } from "./types";
4
+
5
+ export const [ModalContext, useModalContext] =
6
+ createStrictContext<ModalContextValue>(
7
+ "Component must be rendered as a child of Modal.Root",
8
+ "ModalContext",
9
+ );
10
+
11
+ const ModalProvider = ModalContext.Provider;
12
+
13
+ export { ModalProvider };
@@ -0,0 +1,207 @@
1
+ # Modal
2
+
3
+ A headless, accessible compound component implementing the
4
+ [WAI-ARIA Modal Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/).
5
+
6
+ Modal is built on the native
7
+ [`<dialog>`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog)
8
+ element and its `showModal()` API, so focus trapping, inert background,
9
+ top-layer stacking, and Esc-to-close are handled by the browser. The
10
+ React layer adds what native `<dialog>` doesn't give you:
11
+
12
+ - Click-outside-to-close via a `pointerdown` listener on the dialog
13
+ that checks the pointer against `getBoundingClientRect()` — coords
14
+ outside the rect mean the pointer landed on the native `::backdrop`.
15
+ - A portal for placing the dialog at the end of `document.body`.
16
+ - Controlled / uncontrolled state plumbing.
17
+ - `asChild` composition for every slot-able sub-component.
18
+ - An imperative API for firing open/close from outside the subtree.
19
+ - `forceMount` hooks for driving CSS exit animations.
20
+
21
+ ### Why is click-outside on the dialog, not the overlay?
22
+
23
+ `showModal()` promotes the `<dialog>` into the browser's **top layer**
24
+ and paints a `::backdrop` pseudo-element underneath it — also in the
25
+ top layer. Any sibling `Modal.Overlay` sits _below_ that backdrop
26
+ visually, so clicks on the "outside" area never reach it. Detection
27
+ therefore has to live on the dialog itself: a `pointerdown` whose
28
+ coordinates fall outside the dialog's bounding rect is, by
29
+ elimination, a click on the backdrop. `Modal.Overlay` stays in the
30
+ tree as a cosmetic + animation surface (for custom backdrop styling
31
+ or motion wrappers that exceed what `::backdrop` can express).
32
+
33
+ ```tsx
34
+ import { Modal } from "@primitiv-ui/react";
35
+
36
+ <Modal.Root open={open} onOpenChange={setOpen}>
37
+ <Modal.Trigger>Open</Modal.Trigger>
38
+ <Modal.Portal>
39
+ <Modal.Overlay />
40
+ <Modal.Content>
41
+ <Modal.Title>Payment</Modal.Title>
42
+ <Modal.Description>Enter your card details</Modal.Description>
43
+ {/* body */}
44
+ <Modal.Close>Cancel</Modal.Close>
45
+ </Modal.Content>
46
+ </Modal.Portal>
47
+ </Modal.Root>;
48
+ ```
49
+
50
+ ## Sub-components
51
+
52
+ | Export | Element | Notes |
53
+ | ------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
54
+ | `Modal.Root` | context provider | Owns open state. Exposes `ModalImperativeApi` via `ref` |
55
+ | `Modal.Trigger` | `<button>` | `aria-haspopup="dialog"`, `aria-expanded`, `aria-controls`. `asChild` |
56
+ | `Modal.Portal` | `createPortal` | `container?: HTMLElement` (default `document.body`). `forceMount` |
57
+ | `Modal.Overlay` | `<div>` (sibling) | Decorative / animation target — **not** a click-outside event surface (see below). `aria-hidden="true"`. `data-state`. `asChild`, `forceMount` |
58
+ | `Modal.Content` | `<dialog>` | Native modal dialog. `data-state`. Escape hatches for Esc / click-outside |
59
+ | `Modal.Title` | `<h2>` | Auto-wires `aria-labelledby` on Content. `asChild` |
60
+ | `Modal.Description` | `<p>` | Auto-wires `aria-describedby` on Content. `asChild` |
61
+ | `Modal.Close` | `<button>` | Closes the modal. `asChild` |
62
+
63
+ ## Keyboard interaction
64
+
65
+ | Key | Behaviour |
66
+ | ----- | ----------------------------------------------------------------------------- |
67
+ | `Esc` | Closes the modal (native `cancel` event). Preventable via `onEscapeKeyDown` |
68
+ | `Tab` | Focus is trapped inside the dialog by the browser's native modal dialog logic |
69
+
70
+ ## State modes
71
+
72
+ - **Uncontrolled** — pass `defaultOpen` (or omit for closed on mount).
73
+ - **Controlled** — pass `open` **and** `onOpenChange` together.
74
+
75
+ The two shapes are statically discriminated at the type level; TypeScript
76
+ rejects mixing them.
77
+
78
+ ```tsx
79
+ // Uncontrolled
80
+ <Modal.Root defaultOpen>…</Modal.Root>;
81
+
82
+ // Controlled
83
+ const [open, setOpen] = useState(false);
84
+ <Modal.Root open={open} onOpenChange={setOpen}>
85
+
86
+ </Modal.Root>;
87
+ ```
88
+
89
+ ## Escape hatches
90
+
91
+ Both close paths — `Esc` and click-outside — fire observable callbacks
92
+ on `Modal.Content`. Call `event.preventDefault()` to keep the modal
93
+ open. `onPointerDownOutside` receives the native `PointerEvent`
94
+ (fires on `pointerdown`, not `click`, matching the prop name).
95
+
96
+ ```tsx
97
+ <Modal.Content
98
+ onEscapeKeyDown={(event) => {
99
+ if (hasUnsavedChanges) event.preventDefault();
100
+ }}
101
+ onPointerDownOutside={(event) => {
102
+ if (isRequiredFlow) event.preventDefault();
103
+ }}
104
+ >
105
+
106
+ </Modal.Content>
107
+ ```
108
+
109
+ ## Imperative API
110
+
111
+ ```tsx
112
+ import { Modal, type ModalImperativeApi } from "@primitiv-ui/react";
113
+
114
+ const ref = useRef<ModalImperativeApi>(null);
115
+
116
+ <Modal.Root ref={ref}>…</Modal.Root>;
117
+
118
+ ref.current?.open();
119
+ ref.current?.close();
120
+ ```
121
+
122
+ In controlled mode the imperative methods delegate to `onOpenChange` —
123
+ the parent stays in charge of the actual state update.
124
+
125
+ ## `asChild` composition
126
+
127
+ `Modal.Trigger`, `Modal.Close`, `Modal.Title`, `Modal.Description`, and
128
+ `Modal.Overlay` accept an `asChild` boolean. When set, the component
129
+ delegates rendering to its single child element and merges its own ARIA
130
+ attributes, ids, composed event handlers, and ref onto the child
131
+ following the asChild pattern (child handler runs first, then the
132
+ library's). `Modal.Overlay` is decorative and has no library-side
133
+ handlers, so only its ARIA and `data-state` are forwarded.
134
+
135
+ ```tsx
136
+ // Router link with dialog-trigger semantics
137
+ <Modal.Trigger asChild>
138
+ <Link to="/upgrade">Upgrade</Link>
139
+ </Modal.Trigger>
140
+
141
+ // Alternate heading level
142
+ <Modal.Title asChild>
143
+ <h3>Payment</h3>
144
+ </Modal.Title>
145
+
146
+ // Motion wrapper on the backdrop
147
+ <Modal.Overlay asChild>
148
+ <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
149
+ </Modal.Overlay>
150
+ ```
151
+
152
+ `Modal.Content` is intentionally **not** slot-able. Its native
153
+ `<dialog>` element is the whole reason Modal works without a focus
154
+ trap or a scroll-lock library, so we don't expose a way to swap it.
155
+
156
+ ## Animation hooks
157
+
158
+ `Modal.Portal` and `Modal.Overlay` accept a `forceMount` boolean. When
159
+ set, the subtree stays in the DOM regardless of `open` state so a CSS
160
+ animation can play against the `data-state="closed"` attribute:
161
+
162
+ ```tsx
163
+ <Modal.Portal forceMount>
164
+ <Modal.Overlay forceMount />
165
+ <Modal.Content>…</Modal.Content>
166
+ </Modal.Portal>
167
+ ```
168
+
169
+ ```css
170
+ [data-state="open"] {
171
+ animation: fade-in 150ms ease-out;
172
+ }
173
+ [data-state="closed"] {
174
+ animation: fade-out 120ms ease-in forwards;
175
+ }
176
+ ```
177
+
178
+ Consumers who use `forceMount` own the unmount lifecycle themselves
179
+ (e.g. by tracking a separate `presence` state that flips false only
180
+ once the exit animation ends).
181
+
182
+ ## Scroll lock
183
+
184
+ Modal intentionally ships no JavaScript scroll lock. The one-line CSS
185
+ equivalent works in every modern browser:
186
+
187
+ ```css
188
+ html:has(dialog[open]) {
189
+ overflow: hidden;
190
+ }
191
+ ```
192
+
193
+ ## Styling hooks
194
+
195
+ `data-state="open" | "closed"` is set on `Modal.Overlay` and
196
+ `Modal.Content`, letting any CSS system target the two phases.
197
+
198
+ ```css
199
+ dialog[data-state="open"] {
200
+ display: grid;
201
+ place-items: center;
202
+ }
203
+
204
+ dialog::backdrop {
205
+ background: oklch(0 0 0 / 0.6);
206
+ }
207
+ ```