{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"context-menu","title":"Context Menu","description":"Pointer and keyboard context menu sharing Menu presentation.","dependencies":["@base-ui/react@1.7.0","lucide-react@1.18.0"],"registryDependencies":["@siteplane/base","@siteplane/menu","@siteplane/switch"],"files":[{"path":"packages/ui/src/components/context-menu/context-menu.tsx","content":"\"use client\";\n\nimport { ContextMenu as ContextMenuPrimitive } from \"@base-ui/react/context-menu\";\nimport { ChevronRightIcon } from \"lucide-react\";\nimport * 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  switchMenuTrackClassName,\n  switchThumbClassName,\n  switchVariants,\n} from \"../switch/switch\";\nimport {\n  menuGroupLabelClassName,\n  menuItemClassName,\n  menuPopupVariantClassNames,\n  menuPopupWidthClassNames,\n  menuSeparatorClassName,\n  menuShortcutClassName,\n  menuSubTriggerClassName,\n  type MenuPopupVariant,\n  type MenuPopupWidth,\n} from \"../menu/menu-shared\";\n\ntype ContextMenuTriggerArea = {\n  element: HTMLElement | null;\n  setElement: (element: HTMLElement | null) => void;\n};\n\n// Base UI anchors the popup to the pointer, which has no width. The root\n// shares the trigger area so the trigger-based popup widths can measure it.\nconst ContextMenuTriggerAreaContext =\n  React.createContext<ContextMenuTriggerArea | null>(null);\n\nexport function ContextMenu(\n  props: ContextMenuPrimitive.Root.Props,\n): React.ReactElement {\n  const [element, setElement] = React.useState<HTMLElement | null>(null);\n  const triggerArea = React.useMemo(() => ({ element, setElement }), [element]);\n  return (\n    <ContextMenuTriggerAreaContext.Provider value={triggerArea}>\n      <ContextMenuPrimitive.Root {...props} />\n    </ContextMenuTriggerAreaContext.Provider>\n  );\n}\n// Base UI's own Portal type, with className narrowed to a string.\nexport const ContextMenuPortal: React.ForwardRefExoticComponent<\n  WithStringClassName<Omit<ContextMenuPrimitive.Portal.Props, \"ref\">> &\n    React.RefAttributes<HTMLDivElement>\n> = ContextMenuPrimitive.Portal;\n\nexport function ContextMenuTrigger({\n  className,\n  children,\n  ref,\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.Trigger.Props>) {\n  const setTriggerArea = React.useContext(\n    ContextMenuTriggerAreaContext,\n  )?.setElement;\n  const triggerRef = React.useCallback(\n    (element: HTMLDivElement | null) => {\n      setTriggerArea?.(element);\n      if (typeof ref === \"function\") ref(element);\n      else if (ref) ref.current = element;\n    },\n    [ref, setTriggerArea],\n  );\n  return (\n    <ContextMenuPrimitive.Trigger\n      className={cn(slotClass(\"context-menu-trigger\", props), className)}\n      data-slot=\"context-menu-trigger\"\n      ref={triggerRef}\n      {...props}\n    >\n      {children}\n    </ContextMenuPrimitive.Trigger>\n  );\n}\n\nfunction useElementWidth(element: HTMLElement | null): number | undefined {\n  const [width, setWidth] = React.useState<number>();\n  React.useEffect(() => {\n    if (!element) return;\n    const measure = () => setWidth(element.getBoundingClientRect().width);\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(element);\n    return () => observer.disconnect();\n  }, [element]);\n  return element ? width : undefined;\n}\n\nconst contextMenuTriggerAreaWidthClassNames: Record<MenuPopupWidth, string> = {\n  ...menuPopupWidthClassNames,\n  trigger: \"min-w-0 w-(--context-menu-trigger-width) max-w-(--available-width)\",\n  \"at-least-trigger\":\n    \"min-w-(--context-menu-trigger-width) w-max max-w-[min(max(--spacing(80),var(--context-menu-trigger-width)),var(--available-width))]\",\n};\nexport function ContextMenuPopup({\n  children,\n  className,\n  sideOffset = 0,\n  align = \"start\",\n  alignOffset,\n  side = \"bottom\",\n  anchor,\n  portalProps,\n  width = \"auto\",\n  variant = \"default\",\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.Popup.Props> & {\n  align?: ContextMenuPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: ContextMenuPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: ContextMenuPrimitive.Positioner.Props[\"alignOffset\"];\n  side?: ContextMenuPrimitive.Positioner.Props[\"side\"];\n  anchor?: ContextMenuPrimitive.Positioner.Props[\"anchor\"];\n  portalProps?: WithStringClassName<ContextMenuPrimitive.Portal.Props>;\n  width?: MenuPopupWidth;\n  variant?: MenuPopupVariant;\n}) {\n  // A custom anchor or a submenu trigger has a real width; only the pointer\n  // anchor falls back to the trigger area.\n  const triggerArea = React.useContext(ContextMenuTriggerAreaContext);\n  const measuresTriggerArea =\n    triggerArea !== null &&\n    anchor === undefined &&\n    (width === \"trigger\" || width === \"at-least-trigger\");\n  const triggerAreaWidth = useElementWidth(\n    measuresTriggerArea ? triggerArea.element : null,\n  );\n  return (\n    <ContextMenuPortal {...portalProps}>\n      <SiteplaneUIPortalScope>\n        <ContextMenuPrimitive.Positioner\n          align={align}\n          alignOffset={alignOffset}\n          anchor={anchor}\n          className=\"sp-context-menu-positioner z-50\"\n          data-slot=\"context-menu-positioner\"\n          side={side}\n          sideOffset={sideOffset}\n          style={\n            triggerAreaWidth === undefined\n              ? undefined\n              : ({\n                  \"--context-menu-trigger-width\": `${triggerAreaWidth}px`,\n                } as React.CSSProperties)\n          }\n        >\n          <ContextMenuPrimitive.Popup\n            className={cn(\n              slotClass(\"context-menu-popup\", props),\n              menuPopupVariantClassNames[variant],\n              measuresTriggerArea\n                ? contextMenuTriggerAreaWidthClassNames[width]\n                : menuPopupWidthClassNames[width],\n              className,\n            )}\n            data-slot=\"context-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          </ContextMenuPrimitive.Popup>\n        </ContextMenuPrimitive.Positioner>\n      </SiteplaneUIPortalScope>\n    </ContextMenuPortal>\n  );\n}\nexport function ContextMenuGroup(\n  props: WithStringClassName<ContextMenuPrimitive.Group.Props>,\n) {\n  return (\n    <ContextMenuPrimitive.Group\n      data-slot=\"context-menu-group\"\n      {...props}\n      className={cn(slotClass(\"context-menu-group\", props), props.className)}\n    />\n  );\n}\nexport function ContextMenuItem({\n  className,\n  children,\n  inset,\n  variant = \"default\",\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.Item.Props> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <ContextMenuPrimitive.Item\n      className={cn(\n        slotClass(\"context-menu-item\", props),\n        menuItemClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"context-menu-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {menuRowLabel(children)}\n    </ContextMenuPrimitive.Item>\n  );\n}\nexport function ContextMenuLinkItem({\n  className,\n  children,\n  inset,\n  variant = \"default\",\n  closeOnClick = true,\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.LinkItem.Props> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <ContextMenuPrimitive.LinkItem\n      className={cn(\n        slotClass(\"context-menu-link-item\", props),\n        menuItemClassName,\n        className,\n      )}\n      closeOnClick={closeOnClick}\n      data-inset={inset || undefined}\n      data-slot=\"context-menu-link-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {menuRowLabel(children)}\n    </ContextMenuPrimitive.LinkItem>\n  );\n}\nexport function ContextMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  size,\n  variant = \"default\",\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.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    <ContextMenuPrimitive.CheckboxItem\n      checked={checked}\n      className={cn(\n        slotClass(\"context-menu-checkbox-item\", props),\n        choiceMenuItemClassName,\n        variant === \"switch\"\n          ? choiceMenuItemSwitchGridClassName\n          : choiceMenuItemCheckGridClassName,\n        className,\n      )}\n      data-size={resolvedSize}\n      data-slot=\"context-menu-checkbox-item\"\n      data-variant={variant}\n      {...props}\n    >\n      {variant === \"switch\" ? (\n        <>\n          <span className={choiceMenuItemSwitchLabelClassName}>\n            {menuRowLabel(children)}\n          </span>\n          <ContextMenuPrimitive.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          </ContextMenuPrimitive.CheckboxItemIndicator>\n        </>\n      ) : (\n        <>\n          <ContextMenuPrimitive.CheckboxItemIndicator\n            className=\"col-start-1\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </ContextMenuPrimitive.CheckboxItemIndicator>\n          <span className={choiceMenuItemLabelClassName}>{children}</span>\n        </>\n      )}\n    </ContextMenuPrimitive.CheckboxItem>\n  );\n}\nexport function ContextMenuRadioGroup(\n  props: WithStringClassName<ContextMenuPrimitive.RadioGroup.Props>,\n) {\n  return (\n    <ContextMenuPrimitive.RadioGroup\n      data-slot=\"context-menu-radio-group\"\n      {...props}\n      className={cn(\n        slotClass(\"context-menu-radio-group\", props),\n        props.className,\n      )}\n    />\n  );\n}\nexport function ContextMenuRadioItem({\n  className,\n  children,\n  indicatorPosition = \"start\",\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.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.\n   */\n  indicatorPosition?: \"start\" | \"end\" | undefined;\n}) {\n  const end = indicatorPosition === \"end\";\n  return (\n    <ContextMenuPrimitive.RadioItem\n      className={cn(\n        slotClass(\"context-menu-radio-item\", props),\n        choiceMenuItemClassName,\n        end ? choiceMenuItemEndGridClassName : choiceMenuItemCheckGridClassName,\n        className,\n      )}\n      data-indicator-position={end ? \"end\" : undefined}\n      data-slot=\"context-menu-radio-item\"\n      {...props}\n    >\n      {end ? (\n        <>\n          <span className={choiceMenuItemEndLabelClassName}>\n            {menuRowLabel(children)}\n          </span>\n          <ContextMenuPrimitive.RadioItemIndicator\n            className=\"col-start-2\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </ContextMenuPrimitive.RadioItemIndicator>\n        </>\n      ) : (\n        <>\n          <ContextMenuPrimitive.RadioItemIndicator\n            className=\"col-start-1\"\n            keepMounted\n          >\n            <ChoiceMenuItemCheck />\n          </ContextMenuPrimitive.RadioItemIndicator>\n          <span className={choiceMenuItemLabelClassName}>{children}</span>\n        </>\n      )}\n    </ContextMenuPrimitive.RadioItem>\n  );\n}\nexport function ContextMenuGroupLabel({\n  className,\n  inset,\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.GroupLabel.Props> & {\n  inset?: boolean;\n}) {\n  return (\n    <ContextMenuPrimitive.GroupLabel\n      className={cn(\n        slotClass(\"context-menu-label\", props),\n        menuGroupLabelClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"context-menu-label\"\n      {...props}\n    />\n  );\n}\nexport function ContextMenuSeparator({\n  className,\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.Separator.Props>) {\n  return (\n    <ContextMenuPrimitive.Separator\n      className={cn(\n        slotClass(\"context-menu-separator\", props),\n        menuSeparatorClassName,\n        className,\n      )}\n      data-slot=\"context-menu-separator\"\n      {...props}\n    />\n  );\n}\nexport function ContextMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"kbd\">) {\n  return (\n    <kbd\n      className={cn(\n        slotClass(\"context-menu-shortcut\", props),\n        menuShortcutClassName,\n        className,\n      )}\n      data-slot=\"context-menu-shortcut\"\n      {...props}\n    />\n  );\n}\nexport function ContextMenuSub(props: ContextMenuPrimitive.SubmenuRoot.Props) {\n  return (\n    <ContextMenuPrimitive.SubmenuRoot data-slot=\"context-menu-sub\" {...props} />\n  );\n}\nexport function ContextMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.SubmenuTrigger.Props> & {\n  inset?: boolean;\n}) {\n  return (\n    <ContextMenuPrimitive.SubmenuTrigger\n      className={cn(\n        slotClass(\"context-menu-sub-trigger\", props),\n        menuSubTriggerClassName,\n        className,\n      )}\n      data-inset={inset || undefined}\n      data-slot=\"context-menu-sub-trigger\"\n      {...props}\n    >\n      {menuRowLabel(children)}\n      <ChevronRightIcon />\n    </ContextMenuPrimitive.SubmenuTrigger>\n  );\n}\nexport function ContextMenuSubPopup({\n  className,\n  sideOffset = 0,\n  alignOffset,\n  align = \"start\",\n  ...props\n}: WithStringClassName<ContextMenuPrimitive.Popup.Props> & {\n  align?: ContextMenuPrimitive.Positioner.Props[\"align\"];\n  sideOffset?: ContextMenuPrimitive.Positioner.Props[\"sideOffset\"];\n  alignOffset?: ContextMenuPrimitive.Positioner.Props[\"alignOffset\"];\n  width?: MenuPopupWidth;\n  variant?: MenuPopupVariant;\n}) {\n  const defaultAlignOffset = align !== \"center\" ? -5 : undefined;\n  // A submenu is anchored to its trigger row, which Base UI measures.\n  return (\n    <ContextMenuTriggerAreaContext.Provider value={null}>\n      <ContextMenuPopup\n        align={align}\n        alignOffset={alignOffset ?? defaultAlignOffset}\n        className={className}\n        data-slot=\"context-menu-sub-content\"\n        side=\"inline-end\"\n        sideOffset={sideOffset}\n        {...props}\n      />\n    </ContextMenuTriggerAreaContext.Provider>\n  );\n}\n\nexport { ContextMenuPrimitive };\n","type":"registry:ui","target":"~/src/siteplane/components/context-menu/context-menu.tsx"},{"path":"packages/ui/src/components/context-menu/index.ts","content":"export * from \"./context-menu\";\n","type":"registry:ui","target":"~/src/siteplane/components/context-menu/index.ts"}],"type":"registry:ui","docs":"https://ui.siteplane.io/docs-markdown/components/context-menu.md","categories":["Overlays & disclosure"],"meta":{"avoidWhen":["Avoid as the only way to reach an action; people who do not find right-click need a visible Menu button as well.","Avoid for navigation; use real links or Navigation Menu."],"description":"Pointer and keyboard context menu sharing Menu presentation.","knownLimitations":["Selection in the menu is menu state, not native form submission."],"providerNotes":[],"releaseStatus":"stable","searchTerms":["context-menu","context menu","context","menu"],"useWhen":["Use for secondary actions on an element, opened by right-click or long press."]}}