{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"menu","title":"Menu","description":"Keyboard-navigable action and selection menu.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/switch"],"files":[{"path":"packages/ui/src/components/menu/menu.tsx","content":"\"use client\";\n\nimport { Menu as MenuPrimitive } from \"@base-ui/react/menu\";\nimport { ChevronRightIcon } from \"lucide-react\";\nimport type * as React from \"react\";\n\nimport {\n  type SiteplaneControlSize,\n  SiteplaneUIPortalScope,\n} from \"../../system/provider/siteplane-ui-provider\";\nimport {\n  ChoiceMenuItemCheck,\n  choiceMenuItemCheckGridClassName,\n  choiceMenuItemClassName,\n  choiceMenuItemEndGridClassName,\n  choiceMenuItemEndLabelClassName,\n  choiceMenuItemLabelClassName,\n  choiceMenuItemSwitchGridClassName,\n  choiceMenuItemSwitchLabelClassName,\n  choiceMenuSwitchSize,\n  menuRowLabel,\n} from \"../../system/styles/choice-menu-item\";\nimport { cn, type WithStringClassName } from \"../../system/utils/cn\";\nimport { slotClass } from \"../../system/utils/slot-class\";\nimport {\n  menuGroupLabelClassName,\n  menuItemClassName,\n  menuPopupVariantClassNames,\n  menuPopupWidthClassNames,\n  menuSeparatorClassName,\n  menuShortcutClassName,\n  menuSubTriggerClassName,\n  type MenuPopupVariant,\n  type MenuPopupWidth,\n} from \"./menu-shared\";\nimport {\n  switchMenuTrackClassName,\n  switchThumbClassName,\n  switchVariants,\n} from \"../switch/switch\";\n\nexport const MenuCreateHandle: typeof MenuPrimitive.createHandle =\n  MenuPrimitive.createHandle;\nexport const Menu: typeof MenuPrimitive.Root = MenuPrimitive.Root;\n// Base UI's own Portal type, with className narrowed to a string.\nexport const MenuPortal: React.ForwardRefExoticComponent<\n  WithStringClassName<Omit<MenuPrimitive.Portal.Props, \"ref\">> &\n    React.RefAttributes<HTMLDivElement>\n> = MenuPrimitive.Portal;\n\nexport function MenuTrigger({\n  className,\n  children,\n  ...props\n}: WithStringClassName<MenuPrimitive.Trigger.Props>) {\n  return (\n    <MenuPrimitive.Trigger\n      className={cn(slotClass(\"menu-trigger\", props), className)}\n      data-slot=\"menu-trigger\"\n      {...props}\n    >\n      {children}\n    </MenuPrimitive.Trigger>\n  );\n}\n\nexport function MenuPopup({\n  children,\n  className,\n  sideOffset = 4,\n  align = \"center\",\n  alignOffset,\n  side = \"bottom\",\n  anchor,\n  portalProps,\n  width = \"auto\",\n  variant = \"default\",\n  ...props\n}: WithStringClassName<MenuPrimitive.Popup.Props> & {\n  align?: MenuPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: MenuPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: MenuPrimitive.Positioner.Props[\"alignOffset\"];\n  side?: MenuPrimitive.Positioner.Props[\"side\"];\n  anchor?: MenuPrimitive.Positioner.Props[\"anchor\"];\n  portalProps?: WithStringClassName<MenuPrimitive.Portal.Props>;\n  width?: MenuPopupWidth;\n  variant?: MenuPopupVariant;\n}) {\n  return (\n    <MenuPortal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <MenuPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          anchor={anchor}\n          className=\"sp-menu-positioner z-50\"\n          data-slot=\"menu-positioner\"\n          side={side}\n          sideOffset={sideOffset}\n        >\n          <MenuPrimitive.Popup\n            className={cn(\n              slotClass(\"menu-popup\", props),\n              menuPopupVariantClassNames[variant],\n              menuPopupWidthClassNames[width],\n              className,\n            )}\n            data-slot=\"menu-popup\"\n            data-variant={variant === \"glass\" ? variant : undefined}\n            {...props}\n          >\n            <div className=\"max-h-(--available-height) w-full overflow-y-auto p-1\">\n              {children}\n            </div>\n          </MenuPrimitive.Popup>\n        </MenuPrimitive.Positioner>\n      </SiteplaneUIPortalScope>\n    </MenuPortal>\n  );\n}\n\nexport function MenuGroup(\n  props: WithStringClassName<MenuPrimitive.Group.Props>,\n) {\n  return (\n    <MenuPrimitive.Group\n      data-slot=\"menu-group\"\n      {...props}\n      className={cn(slotClass(\"menu-group\", props), props.className)}\n    />\n  );\n}\nexport function MenuItem({\n  className,\n  children,\n  inset,\n  variant = \"default\",\n  ...props\n}: WithStringClassName<MenuPrimitive.Item.Props> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <MenuPrimitive.Item\n      className={cn(\n        slotClass(\"menu-item\", props),\n        menuItemClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"menu-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {menuRowLabel(children)}\n    </MenuPrimitive.Item>\n  );\n}\nexport function MenuLinkItem({\n  className,\n  children,\n  inset,\n  variant = \"default\",\n  closeOnClick = true,\n  ...props\n}: WithStringClassName<MenuPrimitive.LinkItem.Props> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <MenuPrimitive.LinkItem\n      className={cn(\n        slotClass(\"menu-link-item\", props),\n        menuItemClassName,\n        className,\n      )}\n      closeOnClick={closeOnClick}\n      data-inset={inset || undefined}\n      data-slot=\"menu-link-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {menuRowLabel(children)}\n    </MenuPrimitive.LinkItem>\n  );\n}\nexport function MenuCheckboxItem({\n  className,\n  children,\n  checked,\n  size,\n  variant = \"default\",\n  ...props\n}: WithStringClassName<MenuPrimitive.CheckboxItem.Props> & {\n  size?: SiteplaneControlSize;\n  variant?: \"default\" | \"switch\";\n}) {\n  // The switch keeps the size that fits the compact row.\n  const resolvedSize = size ?? choiceMenuSwitchSize;\n  return (\n    <MenuPrimitive.CheckboxItem\n      checked={checked}\n      className={cn(\n        slotClass(\"menu-checkbox-item\", props),\n        choiceMenuItemClassName,\n        variant === \"switch\"\n          ? choiceMenuItemSwitchGridClassName\n          : choiceMenuItemCheckGridClassName,\n        className,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"menu-checkbox-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {variant === \"switch\" ? (\n        <>\n          <span className={choiceMenuItemSwitchLabelClassName}>\n            {menuRowLabel(children)}\n          </span>\n          <MenuPrimitive.CheckboxItemIndicator\n            className={cn(\n              \"sp-switch-track sp-base-switch\",\n              switchVariants({ size: resolvedSize }),\n              switchMenuTrackClassName,\n            )}\n            data-foundation=\"switch\"\n            data-slot=\"switch-track\"\n            keepMounted\n          >\n            <span\n              className={cn(\"sp-switch-thumb\", switchThumbClassName)}\n              data-slot=\"switch-thumb\"\n            />\n          </MenuPrimitive.CheckboxItemIndicator>\n        </>\n      ) : (\n        <>\n          <MenuPrimitive.CheckboxItemIndicator\n            className=\"col-start-1\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </MenuPrimitive.CheckboxItemIndicator>\n          <span className={choiceMenuItemLabelClassName}>{children}</span>\n        </>\n      )}\n    </MenuPrimitive.CheckboxItem>\n  );\n}\nexport function MenuRadioGroup(\n  props: WithStringClassName<MenuPrimitive.RadioGroup.Props>,\n) {\n  return (\n    <MenuPrimitive.RadioGroup\n      data-slot=\"menu-radio-group\"\n      {...props}\n      className={cn(slotClass(\"menu-radio-group\", props), props.className)}\n    />\n  );\n}\nexport function MenuRadioItem({\n  className,\n  children,\n  indicatorPosition = \"start\",\n  ...props\n}: WithStringClassName<MenuPrimitive.RadioItem.Props> & {\n  /**\n   * Where the row shows its check: in the leading slot (`start`), or at the\n   * end of the row (`end`) for identity rows that lead with a logo or an\n   * avatar, such as the workspaces of a switcher.\n   */\n  indicatorPosition?: \"start\" | \"end\" | undefined;\n}) {\n  const end = indicatorPosition === \"end\";\n  return (\n    <MenuPrimitive.RadioItem\n      className={cn(\n        slotClass(\"menu-radio-item\", props),\n        choiceMenuItemClassName,\n        end ? choiceMenuItemEndGridClassName : choiceMenuItemCheckGridClassName,\n        className,\n      )}\n      data-indicator-position={end ? \"end\" : undefined}\n      data-slot=\"menu-radio-item\"\n      {...props}\n    >\n      {end ? (\n        <>\n          <span className={choiceMenuItemEndLabelClassName}>\n            {menuRowLabel(children)}\n          </span>\n          <MenuPrimitive.RadioItemIndicator className=\"col-start-2\" keepMounted>\n            <ChoiceMenuItemCheck />\n          </MenuPrimitive.RadioItemIndicator>\n        </>\n      ) : (\n        <>\n          <MenuPrimitive.RadioItemIndicator className=\"col-start-1\" keepMounted>\n            <ChoiceMenuItemCheck />\n          </MenuPrimitive.RadioItemIndicator>\n          <span className={choiceMenuItemLabelClassName}>{children}</span>\n        </>\n      )}\n    </MenuPrimitive.RadioItem>\n  );\n}\nexport function MenuGroupLabel({\n  className,\n  inset,\n  ...props\n}: WithStringClassName<MenuPrimitive.GroupLabel.Props> & { inset?: boolean }) {\n  return (\n    <MenuPrimitive.GroupLabel\n      className={cn(\n        slotClass(\"menu-label\", props),\n        menuGroupLabelClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"menu-label\"\n      {...props}\n    />\n  );\n}\nexport function MenuSeparator({\n  className,\n  ...props\n}: WithStringClassName<MenuPrimitive.Separator.Props>) {\n  return (\n    <MenuPrimitive.Separator\n      className={cn(\n        slotClass(\"menu-separator\", props),\n        menuSeparatorClassName,\n        className,\n      )}\n      data-slot=\"menu-separator\"\n      {...props}\n    />\n  );\n}\nexport function MenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      className={cn(\n        slotClass(\"menu-shortcut\", props),\n        menuShortcutClassName,\n        className,\n      )}\n      data-slot=\"menu-shortcut\"\n      {...props}\n    />\n  );\n}\nexport function MenuSub(props: MenuPrimitive.SubmenuRoot.Props) {\n  return <MenuPrimitive.SubmenuRoot data-slot=\"menu-sub\" {...props} />;\n}\nexport function MenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: WithStringClassName<MenuPrimitive.SubmenuTrigger.Props> & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.SubmenuTrigger\n      className={cn(\n        slotClass(\"menu-sub-trigger\", props),\n        menuSubTriggerClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"menu-sub-trigger\"\n      {...props}\n    >\n      {menuRowLabel(children)}\n      <ChevronRightIcon />\n    </MenuPrimitive.SubmenuTrigger>\n  );\n}\nexport function MenuSubPopup({\n  className,\n  sideOffset = 0,\n  alignOffset,\n  align = \"start\",\n  ...props\n}: WithStringClassName<MenuPrimitive.Popup.Props> & {\n  align?: MenuPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: MenuPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: MenuPrimitive.Positioner.Props[\"alignOffset\"];\n  width?: MenuPopupWidth;\n  variant?: MenuPopupVariant;\n}) {\n  const defaultAlignOffset = align !== \"center\" ? -5 : undefined;\n  return (\n    <MenuPopup\n      align={align}\n      alignOffset={alignOffset ?? defaultAlignOffset}\n      className={className}\n      data-slot=\"menu-sub-content\"\n      side=\"inline-end\"\n      sideOffset={sideOffset}\n      {...props}\n    />\n  );\n}\n\n// The DropdownMenu names are the shadcn names of the Menu parts. They differ\n// where shadcn names a part differently: DropdownMenuContent is MenuPopup,\n// DropdownMenuLabel is MenuGroupLabel and DropdownMenuSubContent is\n// MenuSubPopup.\nexport {\n  MenuPrimitive,\n  MenuCreateHandle as DropdownMenuCreateHandle,\n  Menu as DropdownMenu,\n  MenuPortal as DropdownMenuPortal,\n  MenuTrigger as DropdownMenuTrigger,\n  MenuPopup as DropdownMenuContent,\n  MenuGroup as DropdownMenuGroup,\n  MenuItem as DropdownMenuItem,\n  MenuCheckboxItem as DropdownMenuCheckboxItem,\n  MenuRadioGroup as DropdownMenuRadioGroup,\n  MenuRadioItem as DropdownMenuRadioItem,\n  MenuGroupLabel as DropdownMenuLabel,\n  MenuSeparator as DropdownMenuSeparator,\n  MenuShortcut as DropdownMenuShortcut,\n  MenuSub as DropdownMenuSub,\n  MenuSubTrigger as DropdownMenuSubTrigger,\n  MenuSubPopup as DropdownMenuSubContent,\n};\n","type":"registry:ui","target":"~/src/siteplane/components/menu/menu.tsx"},{"path":"packages/ui/src/components/menu/menu-shared.tsx","content":"import { surfaceClassNames } from \"../../system/styles/surface\";\nimport { cn } from \"../../system/utils/cn\";\nconst shortcutClassName =\n  \"ms-auto font-medium font-sans text-muted-foreground/72 text-xs\";\n\n// cn() drops the default min-w-32 when a width mode or className sets a min width.\nconst menuPopupLayoutClassName =\n  \"flex min-w-32 origin-(--transform-origin) outline-none before:rounded-[inherit] focus:outline-none\";\nexport const menuPopupClassName = cn(\n  surfaceClassNames.floatingOverlay,\n  menuPopupLayoutClassName,\n);\nexport type MenuPopupWidth =\n  \"auto\" | \"trigger\" | \"content\" | \"at-least-trigger\";\n// `glass` lays the shared glass surface over whatever sits behind the menu.\nexport type MenuPopupVariant = \"default\" | \"glass\";\n// Glass keeps the popup's frame and layout and takes the glass surface in\n// place of the opaque one, whose fill, edge and shadow the glass rule draws.\nexport const menuPopupVariantClassNames: Record<MenuPopupVariant, string> = {\n  default: menuPopupClassName,\n  glass: cn(\n    surfaceClassNames.floatingOverlayFrame,\n    surfaceClassNames.glass,\n    menuPopupLayoutClassName,\n  ),\n};\n// Every popup stays within the space beside its anchor and is at most 20rem\n// wide, a step of the spacing scale like its minimum (min-w-32); a longer row\n// label ends in an ellipsis instead (rulebook K5). A trigger-wide popup keeps\n// its trigger's width, and one at least as wide as its trigger grows to it.\n// ContextMenu repeats these classes for its trigger area: Tailwind reads\n// class names only as written.\nexport const menuPopupWidthClassNames: Record<MenuPopupWidth, string> = {\n  auto: \"max-w-[min(--spacing(80),var(--available-width))]\",\n  content: \"w-max max-w-[min(--spacing(80),var(--available-width))]\",\n  trigger: \"min-w-0 w-(--anchor-width) max-w-(--available-width)\",\n  \"at-least-trigger\":\n    \"min-w-(--anchor-width) w-max max-w-[min(max(--spacing(80),var(--anchor-width)),var(--available-width))]\",\n};\n// Menus stay compact at every control size: rows, group labels and separators\n// keep the default look, and the shared menu-row recipe in design-rework.css\n// sets their height, text and icon.\nexport const menuItemClassName =\n  \"flex cursor-pointer select-none items-center whitespace-nowrap outline-none data-disabled:cursor-not-allowed data-disabled:opacity-(--sp-rw-disabled-opacity-action) [&>svg]:pointer-events-none [&>svg]:shrink-0\";\n// A group label wraps a long unbroken value, such as an email address, within\n// the popup's width (rulebook K5); only rows stay on one line.\nexport const menuGroupLabelClassName =\n  \"font-(--sp-rw-font-weight-label) wrap-anywhere\";\nexport const menuSeparatorClassName = \"\";\nexport const menuShortcutClassName = shortcutClassName;\nexport const menuSubTriggerClassName =\n  \"flex cursor-pointer items-center whitespace-nowrap outline-none data-disabled:cursor-not-allowed data-disabled:opacity-(--sp-rw-disabled-opacity-action) [&_svg]:pointer-events-none\";\n","type":"registry:ui","target":"~/src/siteplane/components/menu/menu-shared.tsx"},{"path":"packages/ui/src/components/menu/index.ts","content":"export * from \"./menu\";\n","type":"registry:ui","target":"~/src/siteplane/components/menu/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/menu.md","categories":["Overlays & disclosure"],"meta":{"avoidWhen":["Avoid for site navigation with panels of links; use Navigation Menu.","Avoid for choosing a value in a form; use Select."],"description":"Keyboard-navigable action and selection menu.","knownLimitations":["Selection in the menu is menu state, not native form submission."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["menu"],"useWhen":["Use for a compact list of actions or choices opened from a button, such as an account menu or the actions of a row."]}}