@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,1004 @@
1
+ import {
2
+ useContext,
3
+ useCallback,
4
+ useEffect,
5
+ useId,
6
+ useMemo,
7
+ useRef,
8
+ useState,
9
+ } from "react";
10
+
11
+ import { useCheckboxRoot } from "../Checkbox/hooks";
12
+ import { useDirection } from "../DirectionProvider";
13
+ import { useRadioGroupRoot } from "../RadioGroup/hooks";
14
+ import { useControllableState } from "../hooks";
15
+ import { Slot, composeEventHandlers } from "../Slot";
16
+
17
+ import {
18
+ ContextMenuContext,
19
+ ContextMenuPosition,
20
+ useContextMenuContext,
21
+ } from "./ContextMenuContext";
22
+ import { ContextMenuContentContext } from "./ContextMenuContentContext";
23
+ import { ContextMenuGroupContext } from "./ContextMenuGroupContext";
24
+ import { ContextMenuItemIndicatorContext } from "./ContextMenuItemIndicatorContext";
25
+ import { ContextMenuRadioGroupContext } from "./ContextMenuRadioGroupContext";
26
+ import {
27
+ ContextMenuSubContext,
28
+ useContextMenuSubContext,
29
+ } from "./ContextMenuSubContext";
30
+ import {
31
+ ContextMenuCheckboxItemProps,
32
+ ContextMenuContentProps,
33
+ ContextMenuGroupProps,
34
+ ContextMenuItemIndicatorProps,
35
+ ContextMenuItemProps,
36
+ ContextMenuLabelProps,
37
+ ContextMenuRadioGroupProps,
38
+ ContextMenuRadioItemProps,
39
+ ContextMenuRootProps,
40
+ ContextMenuSeparatorProps,
41
+ ContextMenuSubContentProps,
42
+ ContextMenuSubProps,
43
+ ContextMenuSubTriggerProps,
44
+ ContextMenuTriggerProps,
45
+ } from "./types";
46
+ import { MENUITEM_SELECTOR, TYPEAHEAD_RESET_MS } from "./constants";
47
+
48
+ /**
49
+ * The root of a ContextMenu — owns the open state and the position the
50
+ * menu should open at, and provides context to descendants. Renders no
51
+ * DOM of its own; it is a context boundary.
52
+ *
53
+ * Supports two state modes, statically discriminated at the type level so
54
+ * only one shape is accepted by TypeScript:
55
+ *
56
+ * - **Uncontrolled** — pass {@link ContextMenuRootProps.defaultOpen | `defaultOpen`}
57
+ * (or omit it to start closed). The component owns and updates the open
58
+ * state internally. Optional {@link ContextMenuRootProps.onOpenChange | `onOpenChange`}
59
+ * observes transitions.
60
+ * - **Controlled** — pass {@link ContextMenuRootProps.open | `open`} *and*
61
+ * {@link ContextMenuRootProps.onOpenChange | `onOpenChange`} together. The
62
+ * parent owns the state; the component defers every transition back through
63
+ * the callback.
64
+ *
65
+ * **Reading direction.** Pass {@link ContextMenuRootProps.dir | `dir`} to
66
+ * set `"ltr"` or `"rtl"`, which inverts the submenu open / close arrow
67
+ * keys (`ArrowRight` ↔ `ArrowLeft`). When omitted, the component reads
68
+ * the inherited {@link DirectionProvider} value, falling back to `"ltr"`.
69
+ */
70
+ function ContextMenuRoot({
71
+ defaultOpen = false,
72
+ open: controlledOpen,
73
+ onOpenChange,
74
+ dir,
75
+ children,
76
+ }: ContextMenuRootProps) {
77
+ const contentId = useId();
78
+ const triggerRef = useRef<HTMLElement | null>(null);
79
+ const [position, setPosition] = useState<ContextMenuPosition | null>(null);
80
+ const inheritedDir = useDirection();
81
+ const resolvedDir = dir ?? inheritedDir;
82
+ const [open, setOpenBase] = useControllableState<boolean>(
83
+ controlledOpen,
84
+ defaultOpen,
85
+ onOpenChange,
86
+ );
87
+ const openRef = useRef(open);
88
+ useEffect(() => {
89
+ openRef.current = open;
90
+ });
91
+
92
+ const setOpen = useCallback(
93
+ (next: boolean) => {
94
+ if (openRef.current === next) return;
95
+ openRef.current = next;
96
+ setOpenBase(next);
97
+ },
98
+ [setOpenBase],
99
+ );
100
+
101
+ const contextValue = useMemo(
102
+ () => ({
103
+ open,
104
+ setOpen,
105
+ position,
106
+ setPosition,
107
+ contentId,
108
+ triggerRef,
109
+ dir: resolvedDir,
110
+ }),
111
+ [open, setOpen, position, contentId, resolvedDir],
112
+ );
113
+
114
+ return (
115
+ <ContextMenuContext.Provider value={contextValue}>
116
+ {children}
117
+ </ContextMenuContext.Provider>
118
+ );
119
+ }
120
+
121
+ ContextMenuRoot.displayName = "ContextMenuRoot";
122
+
123
+ /**
124
+ * Returns a callback that closes any direct-child sub-menu registered with
125
+ * the enclosing Content / SubContent. Items invoke this on mouseEnter so
126
+ * hovering a sibling dismisses an open sub, mirroring the keyboard contract.
127
+ */
128
+ function useCloseSiblingSub() {
129
+ const content = useContext(ContextMenuContentContext);
130
+ return () => content?.closeOpenSubRef.current?.();
131
+ }
132
+
133
+ /**
134
+ * The area that responds to right-click. Renders a `<span>` by default
135
+ * (a non-button host so the wrapped content keeps its native semantics);
136
+ * pass `asChild` to render any element.
137
+ *
138
+ * When the user opens the platform context menu over this element (via
139
+ * right-click, long-press on touch, or the keyboard context-menu key),
140
+ * the native menu is suppressed and the ContextMenu opens, positioned at
141
+ * the pointer.
142
+ */
143
+ function ContextMenuTrigger({
144
+ children,
145
+ onContextMenu,
146
+ asChild = false,
147
+ disabled,
148
+ ...rest
149
+ }: ContextMenuTriggerProps) {
150
+ const { setOpen, setPosition, triggerRef } = useContextMenuContext();
151
+
152
+ const handleContextMenu = (event: React.MouseEvent<HTMLElement>) => {
153
+ if (disabled) return;
154
+ event.preventDefault();
155
+ setPosition({ x: event.clientX, y: event.clientY });
156
+ setOpen(true);
157
+ };
158
+
159
+ const triggerProps = {
160
+ ...rest,
161
+ ref: triggerRef,
162
+ "data-disabled": disabled ? "" : undefined,
163
+ "data-state": disabled ? undefined : ("closed" as const),
164
+ onContextMenu: composeEventHandlers(onContextMenu, handleContextMenu),
165
+ };
166
+
167
+ if (asChild) {
168
+ return <Slot {...triggerProps}>{children}</Slot>;
169
+ }
170
+
171
+ return <span {...triggerProps}>{children}</span>;
172
+ }
173
+
174
+ ContextMenuTrigger.displayName = "ContextMenuTrigger";
175
+
176
+ /**
177
+ * The menu panel rendered with the native HTML
178
+ * [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API)
179
+ * in **manual** mode (`popover="manual"`) — no portal, no floating-ui. The
180
+ * browser still layers the menu via the top layer, but the component owns
181
+ * the close flow rather than the browser's light-dismiss algorithm.
182
+ *
183
+ * Manual mode is required because the right-click gesture's pointerdown
184
+ * fires before the popover exists; with `popover="auto"`, the browser
185
+ * treats the matching pointerup as an outside click and closes the menu
186
+ * the instant the user releases the button. Outside-click close and
187
+ * Escape are handled explicitly here instead.
188
+ *
189
+ * Renders a `<menu role="menu">` positioned at the pointer coordinates
190
+ * captured when the Trigger fired its `contextmenu` event. The cursor
191
+ * position is exposed twice on the element style: as explicit `left` /
192
+ * `top` insets (so the menu renders at the cursor with zero consumer
193
+ * CSS), and as `--primitiv-context-menu-x` / `--primitiv-context-menu-y`
194
+ * custom properties so consumer CSS can write `@position-try` fallbacks
195
+ * that flip the menu to the opposite side when it would overflow the
196
+ * viewport. Pass `asChild` to render any element with menu semantics.
197
+ */
198
+ function ContextMenuContent({
199
+ children,
200
+ style,
201
+ onKeyDown,
202
+ asChild = false,
203
+ ...rest
204
+ }: ContextMenuContentProps) {
205
+ const { open, setOpen, position, contentId, triggerRef } =
206
+ useContextMenuContext();
207
+ const menuRef = useRef<HTMLMenuElement | null>(null);
208
+ const typeaheadRef = useRef<{ query: string; timer: number | null }>({
209
+ query: "",
210
+ timer: null,
211
+ });
212
+
213
+ useEffect(() => {
214
+ const menu = menuRef.current!;
215
+ if (open) {
216
+ menu.showPopover();
217
+ if (!menu.contains(document.activeElement)) {
218
+ const firstItem = menu.querySelector<HTMLElement>(MENUITEM_SELECTOR);
219
+ firstItem?.focus();
220
+ }
221
+ } else {
222
+ try {
223
+ menu.hidePopover();
224
+ } catch {
225
+ // already hidden — no-op
226
+ }
227
+ }
228
+ }, [open]);
229
+
230
+ useEffect(() => {
231
+ const menu = menuRef.current!;
232
+ const handleToggle = (event: Event) => {
233
+ if ((event as ToggleEvent).newState === "closed") setOpen(false);
234
+ };
235
+ menu.addEventListener("toggle", handleToggle);
236
+ return () => menu.removeEventListener("toggle", handleToggle);
237
+ }, [setOpen]);
238
+
239
+ useEffect(() => {
240
+ if (!open) return;
241
+ const handleClick = (event: MouseEvent) => {
242
+ const target = event.target as Element;
243
+ if (triggerRef.current?.contains(target)) return;
244
+ if (target.closest?.("[popover]")) return;
245
+ setOpen(false);
246
+ };
247
+ document.addEventListener("click", handleClick);
248
+ return () => document.removeEventListener("click", handleClick);
249
+ }, [open, setOpen, triggerRef]);
250
+
251
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLMenuElement>) => {
252
+ const menu = menuRef.current!;
253
+ const focused = document.activeElement as HTMLElement | null;
254
+ const scope = (focused?.closest("[popover]") as HTMLElement | null) ?? menu;
255
+ const items = Array.from(
256
+ scope.querySelectorAll<HTMLElement>(MENUITEM_SELECTOR),
257
+ ).filter((el) => el.closest("[popover]") === scope);
258
+ if (items.length === 0) return;
259
+ const currentIndex = items.indexOf(document.activeElement as HTMLElement);
260
+
261
+ let nextIndex: number | null = null;
262
+ if (event.key === "ArrowDown") {
263
+ nextIndex = (currentIndex + 1) % items.length;
264
+ } else if (event.key === "ArrowUp") {
265
+ nextIndex = currentIndex <= 0 ? items.length - 1 : currentIndex - 1;
266
+ } else if (event.key === "Home") {
267
+ nextIndex = 0;
268
+ } else if (event.key === "End") {
269
+ nextIndex = items.length - 1;
270
+ }
271
+
272
+ if (nextIndex !== null) {
273
+ event.preventDefault();
274
+ items[nextIndex].focus();
275
+ return;
276
+ }
277
+
278
+ if (event.key === "Enter" || event.key === " ") {
279
+ if (currentIndex < 0) return;
280
+ event.preventDefault();
281
+ items[currentIndex].click();
282
+ return;
283
+ }
284
+
285
+ if (event.key === "Escape") {
286
+ event.preventDefault();
287
+ setOpen(false);
288
+ triggerRef.current?.focus();
289
+ return;
290
+ }
291
+
292
+ if (event.key.length === 1 && event.key !== " ") {
293
+ const state = typeaheadRef.current;
294
+ if (state.timer !== null) window.clearTimeout(state.timer);
295
+ state.query = (state.query + event.key).toLowerCase();
296
+ state.timer = window.setTimeout(() => {
297
+ state.query = "";
298
+ state.timer = null;
299
+ }, TYPEAHEAD_RESET_MS);
300
+
301
+ const isRepeat =
302
+ state.query.length > 1 &&
303
+ state.query.split("").every((c) => c === state.query[0]);
304
+ const searchQuery = isRepeat ? state.query[0] : state.query;
305
+ const startIndex = currentIndex < 0 ? 0 : currentIndex;
306
+ const offset = searchQuery.length === 1 || isRepeat ? 1 : 0;
307
+ for (let i = 0; i < items.length; i++) {
308
+ const index = (startIndex + offset + i) % items.length;
309
+ const text = items[index].textContent!.trim().toLowerCase();
310
+ if (text.startsWith(searchQuery)) {
311
+ event.preventDefault();
312
+ items[index].focus();
313
+ return;
314
+ }
315
+ }
316
+ }
317
+ };
318
+
319
+ // Both `left`/`top` and `--primitiv-context-menu-x`/`-y` are set: the
320
+ // explicit insets position the menu at the cursor by default, and the
321
+ // custom properties let consumer CSS write `@position-try` fallbacks
322
+ // that flip the menu (e.g. `right: calc(100vw - var(--primitiv-context-menu-x))`)
323
+ // when the primary position would overflow the viewport.
324
+ const positionedStyle = position
325
+ ? ({
326
+ position: "fixed" as const,
327
+ left: position.x,
328
+ top: position.y,
329
+ margin: 0,
330
+ "--primitiv-context-menu-x": `${position.x}px`,
331
+ "--primitiv-context-menu-y": `${position.y}px`,
332
+ ...style,
333
+ } as React.CSSProperties)
334
+ : style;
335
+
336
+ const closeOpenSubRef = useRef<(() => void) | null>(null);
337
+ const contentContextValue = useMemo(() => ({ closeOpenSubRef }), []);
338
+
339
+ // Cascade-close: when our own popover closes, also close any registered
340
+ // direct-child sub so its state doesn't leak into the next open. Without
341
+ // this, clicking an item inside a SubContent closes Root but leaves the
342
+ // child Sub.open=true, which then briefly drives a stale popover render
343
+ // the next time the menu opens. Each SubContent runs the same cascade
344
+ // against its own child sub, so the chain unwinds bottom-up.
345
+ useEffect(() => {
346
+ if (!open) closeOpenSubRef.current?.();
347
+ }, [open]);
348
+
349
+ const contentProps = {
350
+ ...rest,
351
+ ref: menuRef,
352
+ id: contentId,
353
+ role: "menu" as const,
354
+ // Manual mode: the right-click gesture's pointerdown fires before the
355
+ // popover opens, so popover="auto"'s light-dismiss algorithm treats
356
+ // the matching pointerup as an outside click and closes the menu the
357
+ // instant the user releases the button. We close on outside click
358
+ // (document listener below) and Escape (key handler) ourselves.
359
+ popover: "manual" as const,
360
+ "data-state": (open ? "open" : "closed") as "open" | "closed",
361
+ style: positionedStyle,
362
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
363
+ };
364
+
365
+ return (
366
+ <ContextMenuContentContext.Provider value={contentContextValue}>
367
+ {asChild ? (
368
+ <Slot {...contentProps}>{children}</Slot>
369
+ ) : (
370
+ <menu {...contentProps}>{children}</menu>
371
+ )}
372
+ </ContextMenuContentContext.Provider>
373
+ );
374
+ }
375
+
376
+ ContextMenuContent.displayName = "ContextMenuContent";
377
+
378
+ /**
379
+ * A standard menu item. Renders a `<li role="menuitem">` by default; pass
380
+ * `asChild` to render any element with menuitem semantics.
381
+ *
382
+ * Clicking the item (or pressing Enter / Space while focused) fires
383
+ * {@link ContextMenuItemProps.onSelect | `onSelect`} with a cancellable
384
+ * `Event`. The menu auto-closes after selection; call
385
+ * `event.preventDefault()` inside `onSelect` to keep it open.
386
+ *
387
+ * Disabled items receive `aria-disabled="true"` and no-op on activation.
388
+ */
389
+ function ContextMenuItem({
390
+ children,
391
+ onClick,
392
+ onSelect,
393
+ disabled,
394
+ asChild = false,
395
+ ...rest
396
+ }: ContextMenuItemProps) {
397
+ const { setOpen } = useContextMenuContext();
398
+ const closeSiblingSub = useCloseSiblingSub();
399
+ const [highlighted, setHighlighted] = useState(false);
400
+
401
+ const handleClick = () => {
402
+ if (disabled) return;
403
+ const event = new Event("contextmenu.select", { cancelable: true });
404
+ onSelect?.(event);
405
+ if (!event.defaultPrevented) {
406
+ setOpen(false);
407
+ }
408
+ };
409
+
410
+ const itemProps = {
411
+ ...rest,
412
+ role: "menuitem" as const,
413
+ tabIndex: -1,
414
+ "aria-disabled": disabled || undefined,
415
+ "data-highlighted": highlighted ? "" : undefined,
416
+ onClick: composeEventHandlers(onClick, handleClick),
417
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
418
+ setHighlighted(true);
419
+ closeSiblingSub();
420
+ }),
421
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
422
+ setHighlighted(false),
423
+ ),
424
+ };
425
+
426
+ if (asChild) {
427
+ return <Slot {...itemProps}>{children}</Slot>;
428
+ }
429
+
430
+ return <li {...itemProps}>{children}</li>;
431
+ }
432
+
433
+ ContextMenuItem.displayName = "ContextMenuItem";
434
+
435
+ /**
436
+ * A visual separator between groups of items. Renders a `<li role="separator">`
437
+ * by default. Non-interactive — skipped by focus, arrow navigation, and
438
+ * typeahead.
439
+ */
440
+ function ContextMenuSeparator({
441
+ asChild = false,
442
+ children,
443
+ ...rest
444
+ }: ContextMenuSeparatorProps) {
445
+ const separatorProps = { ...rest, role: "separator" as const };
446
+
447
+ if (asChild) {
448
+ return <Slot {...separatorProps}>{children}</Slot>;
449
+ }
450
+
451
+ return <li {...separatorProps} />;
452
+ }
453
+
454
+ ContextMenuSeparator.displayName = "ContextMenuSeparator";
455
+
456
+ /**
457
+ * A semantic grouping of related items. Renders as a `<li role="group">`
458
+ * wrapping an inner `<ul role="none">`, or — with `asChild` — a single
459
+ * grouping element composed onto the provided child.
460
+ *
461
+ * Generates a stable id for its accompanying {@link ContextMenuLabel |
462
+ * `ContextMenu.Label`}, wired automatically via `aria-labelledby`.
463
+ */
464
+ function ContextMenuGroup({
465
+ children,
466
+ asChild = false,
467
+ ...rest
468
+ }: ContextMenuGroupProps) {
469
+ const labelId = useId();
470
+ const contextValue = useMemo(() => ({ labelId }), [labelId]);
471
+ const groupProps = {
472
+ ...rest,
473
+ role: "group" as const,
474
+ "aria-labelledby": labelId,
475
+ };
476
+
477
+ return (
478
+ <ContextMenuGroupContext.Provider value={contextValue}>
479
+ {asChild ? (
480
+ <Slot {...groupProps}>{children}</Slot>
481
+ ) : (
482
+ <li {...groupProps}>
483
+ <ul role="none">{children}</ul>
484
+ </li>
485
+ )}
486
+ </ContextMenuGroupContext.Provider>
487
+ );
488
+ }
489
+
490
+ ContextMenuGroup.displayName = "ContextMenuGroup";
491
+
492
+ /**
493
+ * A non-interactive label, typically used inside a {@link ContextMenuGroup |
494
+ * `ContextMenu.Group`} to give that group an accessible name. When nested in
495
+ * a group, the label's `id` is auto-wired to the group's `aria-labelledby` —
496
+ * consumers don't need to thread ids manually. A caller-supplied `id` takes
497
+ * precedence over the auto-generated one.
498
+ */
499
+ function ContextMenuLabel({
500
+ id,
501
+ children,
502
+ asChild = false,
503
+ ...rest
504
+ }: ContextMenuLabelProps) {
505
+ const group = useContext(ContextMenuGroupContext);
506
+ const labelProps = { ...rest, id: id ?? group?.labelId };
507
+
508
+ if (asChild) {
509
+ return <Slot {...labelProps}>{children}</Slot>;
510
+ }
511
+
512
+ return <li {...labelProps}>{children}</li>;
513
+ }
514
+
515
+ ContextMenuLabel.displayName = "ContextMenuLabel";
516
+
517
+ /**
518
+ * A toggleable menu item. Renders a `<li role="menuitemcheckbox">` with
519
+ * `aria-checked` reflecting the current state. Supports a WAI-ARIA tri-state:
520
+ * `true`, `false`, or `"indeterminate"` (encoded as `aria-checked="mixed"`).
521
+ * An indeterminate item resolves to `true` on the next activation.
522
+ *
523
+ * Activation (click) toggles the checked state, then fires
524
+ * {@link ContextMenuCheckboxItemProps.onSelect | `onSelect`} with a
525
+ * cancellable `Event`. Call `event.preventDefault()` to keep the menu open.
526
+ *
527
+ * Disabled items receive `aria-disabled="true"` and no-op on activation.
528
+ */
529
+ function ContextMenuCheckboxItem({
530
+ children,
531
+ onClick,
532
+ onSelect,
533
+ disabled,
534
+ defaultChecked,
535
+ checked: controlledChecked,
536
+ onCheckedChange,
537
+ asChild = false,
538
+ ...rest
539
+ }: ContextMenuCheckboxItemProps) {
540
+ const { setOpen } = useContextMenuContext();
541
+ const closeSiblingSub = useCloseSiblingSub();
542
+ const [highlighted, setHighlighted] = useState(false);
543
+ const { checked, toggle } = useCheckboxRoot({
544
+ defaultChecked,
545
+ checked: controlledChecked,
546
+ onCheckedChange,
547
+ });
548
+ const ariaChecked: "mixed" | "true" | "false" =
549
+ checked === "indeterminate" ? "mixed" : checked ? "true" : "false";
550
+
551
+ const handleClick = () => {
552
+ if (disabled) return;
553
+ toggle();
554
+ const event = new Event("contextmenu.select", { cancelable: true });
555
+ onSelect?.(event);
556
+ if (!event.defaultPrevented) {
557
+ setOpen(false);
558
+ }
559
+ };
560
+
561
+ const itemProps = {
562
+ ...rest,
563
+ role: "menuitemcheckbox" as const,
564
+ tabIndex: -1,
565
+ "aria-checked": ariaChecked,
566
+ "aria-disabled": disabled || undefined,
567
+ "data-highlighted": highlighted ? "" : undefined,
568
+ onClick: composeEventHandlers(onClick, handleClick),
569
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
570
+ setHighlighted(true);
571
+ closeSiblingSub();
572
+ }),
573
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
574
+ setHighlighted(false),
575
+ ),
576
+ };
577
+
578
+ const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
579
+ const content = asChild ? (
580
+ <Slot {...itemProps}>{children}</Slot>
581
+ ) : (
582
+ <li {...itemProps}>{children}</li>
583
+ );
584
+
585
+ return (
586
+ <ContextMenuItemIndicatorContext.Provider value={indicatorContextValue}>
587
+ {content}
588
+ </ContextMenuItemIndicatorContext.Provider>
589
+ );
590
+ }
591
+
592
+ ContextMenuCheckboxItem.displayName = "ContextMenuCheckboxItem";
593
+
594
+ /**
595
+ * The visible mark (usually a checkmark) rendered inside a
596
+ * {@link ContextMenuCheckboxItem | `ContextMenu.CheckboxItem`} (or radio item).
597
+ * Must be a descendant of one; rendering it anywhere else throws a
598
+ * descriptive error.
599
+ *
600
+ * Renders a `<span>` by default. Exposes `data-state` reflecting the parent
601
+ * item's live state: `"checked"`, `"unchecked"`, or `"indeterminate"`.
602
+ *
603
+ * By default the indicator unmounts when its parent is unchecked. Pass
604
+ * {@link ContextMenuItemIndicatorProps.forceMount | `forceMount`} to keep
605
+ * the DOM node mounted in both states for animation use cases.
606
+ */
607
+ function ContextMenuItemIndicator({
608
+ children,
609
+ asChild = false,
610
+ forceMount = false,
611
+ ...rest
612
+ }: ContextMenuItemIndicatorProps) {
613
+ const context = useContext(ContextMenuItemIndicatorContext);
614
+ if (!context) {
615
+ throw new Error(
616
+ "ContextMenu.ItemIndicator must be rendered inside a <ContextMenu.CheckboxItem> or <ContextMenu.RadioItem>.",
617
+ );
618
+ }
619
+
620
+ const { checked } = context;
621
+ const dataState =
622
+ checked === "indeterminate"
623
+ ? "indeterminate"
624
+ : checked
625
+ ? "checked"
626
+ : "unchecked";
627
+
628
+ if (!forceMount && checked === false) return null;
629
+
630
+ const indicatorProps = { ...rest, "data-state": dataState };
631
+
632
+ if (asChild) {
633
+ return <Slot {...indicatorProps}>{children}</Slot>;
634
+ }
635
+
636
+ return <span {...indicatorProps}>{children}</span>;
637
+ }
638
+
639
+ ContextMenuItemIndicator.displayName = "ContextMenuItemIndicator";
640
+
641
+ /**
642
+ * A single-selection group of menu items. Children must be
643
+ * {@link ContextMenuRadioItem | `ContextMenu.RadioItem`} elements. Renders a
644
+ * `<li role="group">` wrapping `<ul role="none">`.
645
+ */
646
+ function ContextMenuRadioGroup({
647
+ defaultValue,
648
+ value: controlledValue,
649
+ onValueChange,
650
+ children,
651
+ asChild = false,
652
+ ...rest
653
+ }: ContextMenuRadioGroupProps) {
654
+ const { value, select } = useRadioGroupRoot({
655
+ defaultValue,
656
+ value: controlledValue,
657
+ onValueChange,
658
+ });
659
+ const contextValue = useMemo(() => ({ value, select }), [value, select]);
660
+ const groupProps = { ...rest, role: "group" as const };
661
+
662
+ return (
663
+ <ContextMenuRadioGroupContext.Provider value={contextValue}>
664
+ {asChild ? (
665
+ <Slot {...groupProps}>{children}</Slot>
666
+ ) : (
667
+ <li {...groupProps}>
668
+ <ul role="none">{children}</ul>
669
+ </li>
670
+ )}
671
+ </ContextMenuRadioGroupContext.Provider>
672
+ );
673
+ }
674
+
675
+ ContextMenuRadioGroup.displayName = "ContextMenuRadioGroup";
676
+
677
+ /**
678
+ * A single radio choice. Must be rendered inside a {@link ContextMenuRadioGroup |
679
+ * `ContextMenu.RadioGroup`}; rendering it outside one throws a descriptive
680
+ * error.
681
+ *
682
+ * Renders a `<li role="menuitemradio">` with `aria-checked` reflecting whether
683
+ * this item's `value` matches the group's active value.
684
+ *
685
+ * Activation (click) selects this item, updating the group's value, then fires
686
+ * {@link ContextMenuRadioItemProps.onSelect | `onSelect`} with a cancellable
687
+ * `Event`. Call `event.preventDefault()` to keep the menu open.
688
+ */
689
+ function ContextMenuRadioItem({
690
+ children,
691
+ onClick,
692
+ onSelect,
693
+ disabled,
694
+ value: itemValue,
695
+ asChild = false,
696
+ ...rest
697
+ }: ContextMenuRadioItemProps) {
698
+ const { setOpen } = useContextMenuContext();
699
+ const closeSiblingSub = useCloseSiblingSub();
700
+ const [highlighted, setHighlighted] = useState(false);
701
+ const group = useContext(ContextMenuRadioGroupContext);
702
+ if (!group) {
703
+ throw new Error(
704
+ "ContextMenu.RadioItem must be rendered inside a <ContextMenu.RadioGroup>.",
705
+ );
706
+ }
707
+ const checked = group.value === itemValue;
708
+
709
+ const handleClick = () => {
710
+ if (disabled) return;
711
+ group.select(itemValue);
712
+ const event = new Event("contextmenu.select", { cancelable: true });
713
+ onSelect?.(event);
714
+ if (!event.defaultPrevented) {
715
+ setOpen(false);
716
+ }
717
+ };
718
+
719
+ const itemProps = {
720
+ ...rest,
721
+ role: "menuitemradio" as const,
722
+ tabIndex: -1,
723
+ "aria-checked": checked,
724
+ "aria-disabled": disabled || undefined,
725
+ "data-highlighted": highlighted ? "" : undefined,
726
+ onClick: composeEventHandlers(onClick, handleClick),
727
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
728
+ setHighlighted(true);
729
+ closeSiblingSub();
730
+ }),
731
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
732
+ setHighlighted(false),
733
+ ),
734
+ };
735
+
736
+ const indicatorContextValue = useMemo(() => ({ checked }), [checked]);
737
+ const content = asChild ? (
738
+ <Slot {...itemProps}>{children}</Slot>
739
+ ) : (
740
+ <li {...itemProps}>{children}</li>
741
+ );
742
+
743
+ return (
744
+ <ContextMenuItemIndicatorContext.Provider value={indicatorContextValue}>
745
+ {content}
746
+ </ContextMenuItemIndicatorContext.Provider>
747
+ );
748
+ }
749
+
750
+ ContextMenuRadioItem.displayName = "ContextMenuRadioItem";
751
+
752
+ /**
753
+ * A submenu boundary. Wrap a {@link ContextMenuSubTrigger | `ContextMenu.SubTrigger`}
754
+ * and its {@link ContextMenuSubContent | `ContextMenu.SubContent`} in a
755
+ * `ContextMenu.Sub` to establish an independent open state for the nested
756
+ * menu. Supports uncontrolled (`defaultOpen`) and controlled (`open` +
757
+ * `onOpenChange`) modes.
758
+ */
759
+ function ContextMenuSub({
760
+ defaultOpen,
761
+ open: controlledOpen,
762
+ onOpenChange,
763
+ children,
764
+ }: ContextMenuSubProps) {
765
+ const contentId = useId();
766
+ const triggerRef = useRef<HTMLLIElement | null>(null);
767
+ const [open, setOpenBase] = useControllableState<boolean>(
768
+ controlledOpen,
769
+ defaultOpen ?? false,
770
+ onOpenChange,
771
+ );
772
+ const openRef = useRef(open);
773
+ useEffect(() => {
774
+ openRef.current = open;
775
+ });
776
+ const setOpen = useCallback(
777
+ (next: boolean) => {
778
+ if (openRef.current === next) return;
779
+ openRef.current = next;
780
+ setOpenBase(next);
781
+ },
782
+ [setOpenBase],
783
+ );
784
+ const contextValue = useMemo(
785
+ () => ({ open, setOpen, contentId, triggerRef }),
786
+ [open, setOpen, contentId],
787
+ );
788
+
789
+ // Register with the enclosing Content/SubContent so sibling items can close
790
+ // this sub on hover (mirroring the keyboard behaviour where focus returning
791
+ // to the parent dismisses it). If another sibling sub is already
792
+ // registered as open, close it first so a hover-to-open transition onto our
793
+ // SubTrigger supplants the prior sub rather than stacking it.
794
+ const parentContent = useContext(ContextMenuContentContext);
795
+ useEffect(() => {
796
+ if (!open || !parentContent) return;
797
+ const close = () => setOpen(false);
798
+ const prev = parentContent.closeOpenSubRef.current;
799
+ if (prev && prev !== close) prev();
800
+ parentContent.closeOpenSubRef.current = close;
801
+ return () => {
802
+ if (parentContent.closeOpenSubRef.current === close) {
803
+ parentContent.closeOpenSubRef.current = null;
804
+ }
805
+ };
806
+ }, [open, parentContent, setOpen]);
807
+
808
+ return (
809
+ <ContextMenuSubContext.Provider value={contextValue}>
810
+ {children}
811
+ </ContextMenuSubContext.Provider>
812
+ );
813
+ }
814
+
815
+ ContextMenuSub.displayName = "ContextMenuSub";
816
+
817
+ /**
818
+ * The submenu trigger. Must be rendered inside a {@link ContextMenuSub |
819
+ * `ContextMenu.Sub`}.
820
+ *
821
+ * Renders a `<li role="menuitem">` with `aria-haspopup="menu"`,
822
+ * `aria-expanded`, and `aria-controls` wiring it to the sibling
823
+ * {@link ContextMenuSubContent | `ContextMenu.SubContent`}.
824
+ *
825
+ * Opens the submenu on click, the inline-forward arrow key, or pointer
826
+ * hover. The open key follows the resolved reading direction —
827
+ * `ArrowRight` in `"ltr"`, `ArrowLeft` in `"rtl"`. Disabled triggers
828
+ * ignore both click and the open arrow key.
829
+ */
830
+ function ContextMenuSubTrigger({
831
+ children,
832
+ onClick,
833
+ onKeyDown,
834
+ disabled,
835
+ asChild = false,
836
+ ...rest
837
+ }: ContextMenuSubTriggerProps) {
838
+ const sub = useContextMenuSubContext();
839
+ const { dir } = useContextMenuContext();
840
+ const openKey = dir === "rtl" ? "ArrowLeft" : "ArrowRight";
841
+ const closeSiblingSub = useCloseSiblingSub();
842
+ const [hovered, setHovered] = useState(false);
843
+ const toggle = () => {
844
+ if (disabled) return;
845
+ sub.setOpen(true);
846
+ };
847
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLLIElement>) => {
848
+ if (disabled) return;
849
+ if (event.key !== openKey) return;
850
+ event.preventDefault();
851
+ event.stopPropagation();
852
+ sub.setOpen(true);
853
+ };
854
+ const subTriggerProps = {
855
+ ...rest,
856
+ ref: sub.triggerRef,
857
+ role: "menuitem" as const,
858
+ tabIndex: -1,
859
+ "aria-haspopup": "menu" as const,
860
+ "aria-expanded": sub.open,
861
+ "aria-controls": sub.contentId,
862
+ "aria-disabled": disabled || undefined,
863
+ "data-highlighted": hovered || sub.open ? "" : undefined,
864
+ onClick: composeEventHandlers(onClick, toggle),
865
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
866
+ onMouseEnter: composeEventHandlers(rest.onMouseEnter, () => {
867
+ setHovered(true);
868
+ closeSiblingSub();
869
+ if (!disabled) sub.setOpen(true);
870
+ }),
871
+ onMouseLeave: composeEventHandlers(rest.onMouseLeave, () =>
872
+ setHovered(false),
873
+ ),
874
+ };
875
+ if (asChild) {
876
+ return <Slot {...subTriggerProps}>{children}</Slot>;
877
+ }
878
+ return <li {...subTriggerProps}>{children}</li>;
879
+ }
880
+
881
+ ContextMenuSubTrigger.displayName = "ContextMenuSubTrigger";
882
+
883
+ /**
884
+ * The submenu panel. Must be rendered inside a {@link ContextMenuSub |
885
+ * `ContextMenu.Sub`}.
886
+ *
887
+ * Renders a `<menu role="menu" popover="auto">` by default. When the submenu
888
+ * opens, focus moves to its first enabled item. The inline-backward arrow
889
+ * key closes the submenu and returns focus to the SubTrigger —
890
+ * `ArrowLeft` in `"ltr"`, `ArrowRight` in `"rtl"`.
891
+ */
892
+ function ContextMenuSubContent({
893
+ children,
894
+ onKeyDown,
895
+ asChild = false,
896
+ ...rest
897
+ }: ContextMenuSubContentProps) {
898
+ const sub = useContextMenuSubContext();
899
+ const { dir } = useContextMenuContext();
900
+ const closeKey = dir === "rtl" ? "ArrowRight" : "ArrowLeft";
901
+ const menuRef = useRef<HTMLMenuElement | null>(null);
902
+
903
+ useEffect(() => {
904
+ const menu = menuRef.current!;
905
+ if (sub.open) {
906
+ menu.showPopover();
907
+ const firstItem = menu.querySelector<HTMLElement>(MENUITEM_SELECTOR);
908
+ firstItem?.focus();
909
+ } else {
910
+ try {
911
+ menu.hidePopover();
912
+ } catch {
913
+ // already hidden — no-op
914
+ }
915
+ }
916
+ }, [sub.open]);
917
+
918
+ useEffect(() => {
919
+ const menu = menuRef.current!;
920
+ const handleToggle = (event: Event) => {
921
+ if ((event as ToggleEvent).newState === "closed") sub.setOpen(false);
922
+ };
923
+ menu.addEventListener("toggle", handleToggle);
924
+ return () => menu.removeEventListener("toggle", handleToggle);
925
+ }, [sub.setOpen]);
926
+
927
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLMenuElement>) => {
928
+ if (event.key !== closeKey) return;
929
+ event.preventDefault();
930
+ event.stopPropagation();
931
+ sub.setOpen(false);
932
+ sub.triggerRef.current?.focus();
933
+ };
934
+
935
+ const closeOpenSubRef = useRef<(() => void) | null>(null);
936
+ const contentContextValue = useMemo(() => ({ closeOpenSubRef }), []);
937
+
938
+ // Cascade-close — see the matching effect on ContextMenuContent.
939
+ useEffect(() => {
940
+ if (!sub.open) closeOpenSubRef.current?.();
941
+ }, [sub.open]);
942
+
943
+ const subContentProps = {
944
+ ...rest,
945
+ ref: menuRef,
946
+ id: sub.contentId,
947
+ role: "menu" as const,
948
+ popover: "auto" as const,
949
+ onKeyDown: composeEventHandlers(onKeyDown, handleKeyDown),
950
+ };
951
+
952
+ return (
953
+ <ContextMenuContentContext.Provider value={contentContextValue}>
954
+ {asChild ? (
955
+ <Slot {...subContentProps}>{children}</Slot>
956
+ ) : (
957
+ <menu {...subContentProps}>{children}</menu>
958
+ )}
959
+ </ContextMenuContentContext.Provider>
960
+ );
961
+ }
962
+
963
+ ContextMenuSubContent.displayName = "ContextMenuSubContent";
964
+
965
+ type TContextMenuCompound = typeof ContextMenuRoot & {
966
+ Root: typeof ContextMenuRoot;
967
+ Trigger: typeof ContextMenuTrigger;
968
+ Content: typeof ContextMenuContent;
969
+ Item: typeof ContextMenuItem;
970
+ Separator: typeof ContextMenuSeparator;
971
+ Group: typeof ContextMenuGroup;
972
+ Label: typeof ContextMenuLabel;
973
+ CheckboxItem: typeof ContextMenuCheckboxItem;
974
+ ItemIndicator: typeof ContextMenuItemIndicator;
975
+ RadioGroup: typeof ContextMenuRadioGroup;
976
+ RadioItem: typeof ContextMenuRadioItem;
977
+ Sub: typeof ContextMenuSub;
978
+ SubTrigger: typeof ContextMenuSubTrigger;
979
+ SubContent: typeof ContextMenuSubContent;
980
+ };
981
+
982
+ const ContextMenuCompound: TContextMenuCompound = Object.assign(
983
+ ContextMenuRoot,
984
+ {
985
+ Root: ContextMenuRoot,
986
+ Trigger: ContextMenuTrigger,
987
+ Content: ContextMenuContent,
988
+ Item: ContextMenuItem,
989
+ Separator: ContextMenuSeparator,
990
+ Group: ContextMenuGroup,
991
+ Label: ContextMenuLabel,
992
+ CheckboxItem: ContextMenuCheckboxItem,
993
+ ItemIndicator: ContextMenuItemIndicator,
994
+ RadioGroup: ContextMenuRadioGroup,
995
+ RadioItem: ContextMenuRadioItem,
996
+ Sub: ContextMenuSub,
997
+ SubTrigger: ContextMenuSubTrigger,
998
+ SubContent: ContextMenuSubContent,
999
+ },
1000
+ );
1001
+
1002
+ ContextMenuCompound.displayName = "ContextMenu";
1003
+
1004
+ export { ContextMenuCompound as ContextMenu };