{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "Context-menu",
  "description": "A context-menu component.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons"
  ],
  "registryDependencies": [
    "@cubby-ui/scroll-area",
    "@cubby-ui/switch",
    "@cubby-ui/elevated",
    "@cubby-ui/popup-search"
  ],
  "files": [
    {
      "path": "registry/default/context-menu/context-menu.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { ContextMenu as BaseContextMenu } from \"@base-ui/react/context-menu\";\nimport { Menu as BaseMenu } from \"@base-ui/react/menu\";\nimport { cn } from \"@/lib/utils\";\nimport {\n  solidSurface,\n  type SurfaceLevel,\n} from \"@/registry/default/lib/elevated\";\nimport {\n  POPUP_ITEM,\n  POPUP_SEARCH_CLEAR,\n  POPUP_SEARCH_FIELD,\n  POPUP_SEARCH_ICON,\n  POPUP_SEARCH_INPUT,\n  POPUP_SEARCH_MENU_FIELD,\n} from \"@/registry/default/lib/popup-search\";\nimport { ScrollArea } from \"@/registry/default/scroll-area/scroll-area\";\n\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport {\n  Cancel01Icon,\n  CheckIcon,\n  ChevronRightIcon,\n  Search01Icon,\n} from \"@hugeicons/core-free-icons\";\nimport {\n  SwitchVisual,\n  type SwitchVisualProps,\n} from \"@/registry/default/switch/switch\";\n\n// Shared shell for checkbox and radio items. Padding matches the plain menu\n// item so labels line up across every item type; the indicator lives in a\n// reserved right-hand column so toggling never shifts the label.\nconst toggleItemClasses = [\n  POPUP_ITEM,\n  \"group/switch data-highlighted:bg-surface-hover data-highlighted:text-accent-foreground grid cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-60 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n];\n\n// The check draws itself in. `pathLength` restates the path as 1 unit\n// long, so the dash values are fractions of the stroke and survive a HugeIcons\n// reshape. Deriving the icon array is the only way to reach the path.\nconst checkIcon = CheckIcon.map(([tag, attrs]) => [\n  tag,\n  { ...attrs, pathLength: 1 },\n]) as typeof CheckIcon;\n\nconst checkmarkClasses =\n  \"[&_path]:ease-out-expo [&_path]:transition-[stroke-dashoffset] [&_path]:duration-150 [&_path]:[stroke-dasharray:1] in-data-checked:[&_path]:[stroke-dashoffset:0] in-data-unchecked:[&_path]:[stroke-dashoffset:1] motion-reduce:[&_path]:transition-none\" +\n  // Fade the unchecked mark out to hide the round-cap dot an undrawn dash leaves.\n  \" transition-opacity duration-150 ease-out in-data-unchecked:opacity-0 motion-reduce:transition-none\";\n\nfunction ContextMenu({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Root>) {\n  return <BaseContextMenu.Root data-slot=\"context-menu\" {...props} />;\n}\n\nfunction ContextMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Trigger>) {\n  return (\n    <BaseContextMenu.Trigger data-slot=\"context-menu-trigger\" {...props} />\n  );\n}\n\nfunction ContextMenuGroup({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Group>) {\n  return <BaseContextMenu.Group data-slot=\"context-menu-group\" {...props} />;\n}\n\nfunction ContextMenuPortal({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Portal>) {\n  return <BaseContextMenu.Portal data-slot=\"context-menu-portal\" {...props} />;\n}\n\nfunction ContextMenuPositioner({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Positioner>) {\n  return (\n    <BaseContextMenu.Positioner\n      data-slot=\"context-menu-positioner\"\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.RadioGroup>) {\n  return (\n    <BaseContextMenu.RadioGroup\n      data-slot=\"context-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuContent({\n  className,\n  positionerClassName,\n  sideOffset = 4,\n  align = \"start\",\n  level = 3,\n  shadowLevel = 3,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Popup> & {\n  /** Classes for the positioner (the element Base UI moves into place), merged last. */\n  positionerClassName?: string;\n  align?: BaseContextMenu.Positioner.Props[\"align\"];\n  sideOffset?: BaseContextMenu.Positioner.Props[\"sideOffset\"];\n  /** Surface elevation level for the popup bg (1-8). Bump when nesting inside a Dialog or other elevated container. Defaults to 3. */\n  level?: SurfaceLevel;\n  /** Shadow weight (1-8). Pinned to 3 by default so the menu reads the same regardless of nesting depth. */\n  shadowLevel?: SurfaceLevel;\n}) {\n  return (\n    <ContextMenuPortal>\n      <ContextMenuPositioner\n        className={cn(\n          \"max-h-(--available-height) max-w-(--available-width)\",\n          positionerClassName,\n        )}\n        align={align}\n        sideOffset={sideOffset}\n      >\n        <BaseContextMenu.Popup\n          data-slot=\"context-menu-content\"\n          data-level={level}\n          className={cn(\n            // No Menu.Viewport here (ContextMenu has no trigger-swap morph to run\n            // one for), so the popup is its own scroll container: capped at\n            // --available-height, clipping on x for the rounded corners and\n            // scrolling on y. Without the cap a tall menu runs off screen.\n            \"text-popover-foreground relative z-50 flex max-h-(--available-height) max-w-(--available-width) min-w-[12rem] origin-(--transform-origin) flex-col overflow-x-hidden overflow-y-auto rounded-xl p-1 outline-none\",\n            // Modern enter/exit — scale + fade from the transform origin, matching\n            // popover/dropdown-menu (replaces the legacy animate-in/zoom/slide classes)\n            \"ease-out-expo transition-[scale,opacity] duration-100\",\n            \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n            \"data-ending-style:scale-95 data-ending-style:opacity-0\",\n            // No data-instant guard, deliberately: 'click' is inferred from\n            // `event.detail === 0`, which every right-click matches, so it would\n            // kill the enter animation on every open. 'dismiss' must animate or\n            // there is no exit, and 'group' / 'trigger-change' cannot fire here.\n            \"motion-reduce:transition-none\",\n            solidSurface(level, shadowLevel),\n            className,\n          )}\n          {...props}\n        />\n      </ContextMenuPositioner>\n    </ContextMenuPortal>\n  );\n}\n\nfunction ContextMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <BaseContextMenu.Item\n      data-slot=\"context-menu-item\"\n      data-inset={inset || undefined}\n      data-variant={variant}\n      className={cn(\n        POPUP_ITEM,\n        \"data-highlighted:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:bg-destructive/20 data-[variant=destructive]:data-highlighted:text-destructive-foreground data-[variant=destructive]:*:[svg]:text-destructive! data-highlighted:data-[variant=destructive]:*:[svg]:text-destructive-foreground! [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:bg-surface-hover relative flex cursor-default items-center gap-2 outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-60 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuLinkItem({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.LinkItem> & {\n  inset?: boolean;\n}) {\n  return (\n    <BaseContextMenu.LinkItem\n      data-slot=\"context-menu-link-item\"\n      data-inset={inset || undefined}\n      className={cn(\n        POPUP_ITEM,\n        \"data-highlighted:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground data-highlighted:bg-surface-hover relative flex cursor-default items-center gap-2 no-underline outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-60 data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  inset,\n  indicator = \"check\",\n  switchColor = \"primary\",\n  switchShape = \"circle\",\n  switchSize = \"xs\",\n  switchMotion = \"default\",\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.CheckboxItem> & {\n  inset?: boolean;\n  /** Which indicator sits in the right-hand column. `\"switch\"` is visual only — the item keeps the `menuitemcheckbox` role. */\n  indicator?: \"check\" | \"switch\";\n  /** Checked track colour, when `indicator=\"switch\"`. */\n  switchColor?: SwitchVisualProps[\"color\"];\n  /** Thumb silhouette, when `indicator=\"switch\"`. */\n  switchShape?: SwitchVisualProps[\"shape\"];\n  /** Thumb size, when `indicator=\"switch\"`. Defaults to `xs`, which matches the row's text. */\n  switchSize?: SwitchVisualProps[\"size\"];\n  /** How the thumb travels, when `indicator=\"switch\"`. */\n  switchMotion?: SwitchVisualProps[\"motion\"];\n}) {\n  return (\n    <BaseContextMenu.CheckboxItem\n      data-slot=\"context-menu-checkbox-item\"\n      data-inset={inset || undefined}\n      data-indicator={indicator}\n      className={cn(\n        toggleItemClasses,\n        indicator === \"switch\"\n          ? \"grid-cols-[1fr_auto] gap-3\"\n          : \"grid-cols-[1fr_1rem] gap-2\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"col-start-1 flex min-w-0 items-center gap-2\">\n        {children}\n      </span>\n      {indicator === \"switch\" ? (\n        // Visual only: the row carries the role and the click target, so a real\n        // Switch here would nest a focusable control inside a menuitemcheckbox.\n        // Base UI's indicator supplies the aria-hidden.\n        <SwitchVisual\n          color={switchColor}\n          shape={switchShape}\n          size={switchSize}\n          motion={switchMotion}\n          className=\"col-start-2\"\n          render={<BaseContextMenu.CheckboxItemIndicator keepMounted />}\n        />\n      ) : (\n        <BaseContextMenu.CheckboxItemIndicator\n          keepMounted\n          className=\"col-start-2 flex items-center justify-center\"\n        >\n          <HugeiconsIcon\n            icon={checkIcon}\n            strokeWidth={2.5}\n            className={cn(\"size-4\", checkmarkClasses)}\n          />\n        </BaseContextMenu.CheckboxItemIndicator>\n      )}\n    </BaseContextMenu.CheckboxItem>\n  );\n}\n\nfunction ContextMenuRadioItem({\n  className,\n  children,\n  inset,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.RadioItem> & {\n  inset?: boolean;\n}) {\n  return (\n    <BaseContextMenu.RadioItem\n      data-slot=\"context-menu-radio-item\"\n      data-inset={inset || undefined}\n      className={cn(toggleItemClasses, \"grid-cols-[1fr_1rem] gap-2\", className)}\n      {...props}\n    >\n      <span className=\"col-start-1 flex min-w-0 items-center gap-2\">\n        {children}\n      </span>\n      <BaseContextMenu.RadioItemIndicator\n        keepMounted\n        className=\"col-start-2 flex items-center justify-center\"\n      >\n        <HugeiconsIcon\n          icon={checkIcon}\n          strokeWidth={2.5}\n          className={cn(\"size-4\", checkmarkClasses)}\n        />\n      </BaseContextMenu.RadioItemIndicator>\n    </BaseContextMenu.RadioItem>\n  );\n}\n\nfunction ContextMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<\"div\"> & {\n  inset?: boolean;\n}) {\n  return (\n    <div\n      data-slot=\"context-menu-label\"\n      data-inset={inset || undefined}\n      className={cn(\n        \"px-2.5 py-1.5 text-xs font-medium data-inset:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuGroupLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.GroupLabel> & {\n  inset?: boolean;\n}) {\n  return (\n    <BaseContextMenu.GroupLabel\n      data-slot=\"context-menu-group-label\"\n      data-inset={inset || undefined}\n      className={cn(\n        \"px-2.5 py-1.5 text-xs font-medium data-inset:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Separator>) {\n  return (\n    <BaseContextMenu.Separator\n      data-slot=\"context-menu-separator\"\n      // Inset to the item label, not the popup edge: mx-2.5 clears the popup's\n      // p-1 plus the item's px-2.5, so the rule starts where the text does\n      // instead of running into the popup's rounded corners.\n      className={cn(\n        \"bg-border mx-2.5 my-1 h-px\",\n        // Drop rules orphaned when filtering hides an emptied group.\n        \"[&:not(:has(~:not([hidden]):not([role=separator])))]:hidden [[hidden]+&]:hidden\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"context-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuSub({\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.SubmenuRoot>) {\n  return (\n    <BaseContextMenu.SubmenuRoot data-slot=\"context-menu-sub\" {...props} />\n  );\n}\n\nfunction ContextMenuSubTrigger({\n  className,\n  inset,\n  children,\n  delay = 0,\n  closeDelay = 0,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.SubmenuTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <BaseContextMenu.SubmenuTrigger\n      data-slot=\"context-menu-sub-trigger\"\n      data-inset={inset || undefined}\n      delay={delay}\n      closeDelay={closeDelay}\n      className={cn(\n        POPUP_ITEM,\n        \"data-highlighted:text-accent-foreground data-popup-open:text-accent-foreground data-highlighted:bg-surface-hover data-popup-open:bg-surface-hover flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-60 data-inset:pl-8\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <HugeiconsIcon\n        icon={ChevronRightIcon}\n        className=\"ml-auto size-4\"\n        strokeWidth={2}\n      />\n    </BaseContextMenu.SubmenuTrigger>\n  );\n}\n\nfunction ContextMenuSubContent({\n  className,\n  positionerClassName,\n  // 8px, not 0: the positioner anchors to the sub-trigger, which sits 4px\n  // inside the parent popup because of its p-1. A 0 offset therefore overlaps\n  // the parent's edge by 4px; 8 leaves a 4px gap, matching Menubar.\n  sideOffset = 8,\n  align = \"start\",\n  level = 5,\n  shadowLevel = 3,\n  ...props\n}: React.ComponentProps<typeof BaseContextMenu.Popup> & {\n  /** Classes for the positioner (the element Base UI moves into place), merged last. */\n  positionerClassName?: string;\n  align?: BaseContextMenu.Positioner.Props[\"align\"];\n  sideOffset?: BaseContextMenu.Positioner.Props[\"sideOffset\"];\n  /** Surface elevation level for the submenu bg (1-8). Defaults to 5 — one tier above the parent menu's default of 3. Bump higher when nesting inside a Dialog. */\n  level?: SurfaceLevel;\n  /** Shadow weight (1-8). Pinned to 3 by default so the submenu reads the same dropdown weight as its parent. */\n  shadowLevel?: SurfaceLevel;\n}) {\n  return (\n    <ContextMenuPortal>\n      <ContextMenuPositioner\n        className={cn(\n          \"max-h-(--available-height) max-w-(--available-width)\",\n          positionerClassName,\n        )}\n        sideOffset={sideOffset}\n        align={align}\n      >\n        <BaseContextMenu.Popup\n          data-slot=\"context-menu-sub-content\"\n          data-level={level}\n          className={cn(\n            // Its own scroll container, like ContextMenuContent above.\n            \"text-popover-foreground relative z-50 flex max-h-(--available-height) max-w-(--available-width) min-w-[12rem] origin-(--transform-origin) flex-col overflow-x-hidden overflow-y-auto rounded-xl p-1 outline-none\",\n            // Modern enter/exit — scale + fade from the transform origin, matching\n            // popover/dropdown-menu (replaces the legacy animate-in/zoom/slide classes)\n            \"ease-out-expo transition-[scale,opacity] duration-100\",\n            \"data-starting-style:scale-95 data-starting-style:opacity-0\",\n            \"data-ending-style:scale-95 data-ending-style:opacity-0\",\n            // See ContextMenuContent for why there is no data-instant guard.\n            \"motion-reduce:transition-none\",\n            solidSurface(level, shadowLevel),\n            className,\n          )}\n          {...props}\n        />\n      </ContextMenuPositioner>\n    </ContextMenuPortal>\n  );\n}\n\n// Filtering (Base UI preview API). ContextMenu renders a Menu root inside, so\n// Menu's filter parts work here: wrap `ContextMenu` or `ContextMenuSub` in the\n// provider, then put a `ContextMenuInput` above a `ContextMenuList`.\nfunction ContextMenuFilter({\n  autoHighlight = true,\n  ...props\n}: BaseMenu.FilterProvider.Props) {\n  // Highlight the top match while typing, so Enter runs it.\n  return <BaseMenu.FilterProvider autoHighlight={autoHighlight} {...props} />;\n}\n\nfunction ContextMenuInput({\n  className,\n  inputClassName,\n  showClear = true,\n  clearLabel,\n  \"aria-label\": ariaLabel = \"Search\",\n  ...props\n}: BaseMenu.Input.Props & {\n  /** Class applied to the inner `<input>`. `className` styles the field. */\n  inputClassName?: string;\n  /** Show a clear button while the query has text. */\n  showClear?: boolean;\n  /** Accessible name of the clear button. Defaults to \"Clear search\". */\n  clearLabel?: string;\n}) {\n  return (\n    <div\n      data-slot=\"context-menu-input-group\"\n      // Outside the list's scroll, as in Base UI's filter demo. An item's height\n      // (32px); the list's own padding forms the gap below it.\n      className={cn(POPUP_SEARCH_FIELD, POPUP_SEARCH_MENU_FIELD, className)}\n    >\n      <HugeiconsIcon\n        icon={Search01Icon}\n        strokeWidth={2}\n        className={POPUP_SEARCH_ICON}\n      />\n      <BaseMenu.Input\n        data-slot=\"context-menu-input\"\n        className={cn(POPUP_SEARCH_INPUT, inputClassName)}\n        // The placeholder isn't a name; pass a specific label when you can.\n        aria-label={ariaLabel}\n        {...props}\n      />\n      {showClear && <ContextMenuClear aria-label={clearLabel} />}\n    </div>\n  );\n}\n\nfunction ContextMenuClear({\n  className,\n  children,\n  \"aria-label\": ariaLabel = \"Clear search\",\n  ...props\n}: BaseMenu.Clear.Props) {\n  return (\n    <BaseMenu.Clear\n      data-slot=\"context-menu-clear\"\n      aria-label={ariaLabel}\n      className={cn(POPUP_SEARCH_CLEAR, className)}\n      {...props}\n    >\n      {children ?? (\n        <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} className=\"size-4\" />\n      )}\n    </BaseMenu.Clear>\n  );\n}\n\nfunction ContextMenuList({\n  className,\n  ...props\n}: Omit<BaseMenu.List.Props, \"className\"> & {\n  /** Styles the scroll area around the list, e.g. to change its height cap. */\n  className?: string;\n}) {\n  return (\n    // Scrolls on its own so the field stays put. Padding sits inside the scroll so\n    // rows reach the popup's edges. Capped at 20rem; shrinks as a flex item to\n    // fit a short screen, as Combobox's and Select's lists do.\n    <ScrollArea fadeEdges=\"y\" className={cn(\"max-h-80\", className)}>\n      <BaseMenu.List data-slot=\"context-menu-list\" className=\"p-1\" {...props} />\n    </ScrollArea>\n  );\n}\n\nfunction ContextMenuEmpty({ className, ...props }: BaseMenu.Empty.Props) {\n  return (\n    <BaseMenu.Empty\n      data-slot=\"context-menu-empty\"\n      // Lines up with the item labels. The list's 8px padding still renders below,\n      // so it sits 10px from the field and the bottom.\n      className={cn(\n        \"text-muted-foreground shrink-0 px-3.5 pt-2.5 pb-0.5 text-sm\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nconst useContextMenuFilter = BaseMenu.useFilter;\n\nexport {\n  ContextMenu,\n  ContextMenuContent,\n  ContextMenuItem,\n  ContextMenuLinkItem,\n  ContextMenuShortcut,\n  ContextMenuTrigger,\n  ContextMenuSeparator,\n  ContextMenuGroup,\n  ContextMenuLabel,\n  ContextMenuGroupLabel,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioGroup,\n  ContextMenuPortal,\n  ContextMenuPositioner,\n  ContextMenuRadioItem,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n  ContextMenuFilter,\n  ContextMenuInput,\n  ContextMenuClear,\n  ContextMenuList,\n  ContextMenuEmpty,\n  useContextMenuFilter,\n};\n",
      "type": "registry:ui",
      "target": "components/ui/cubby-ui/context-menu.tsx"
    }
  ],
  "type": "registry:ui"
}