{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "Context Menu",
  "description": "Right-click menu — Base UI's Menu popup anchored at the pointer.",
  "dependencies": [
    "@base-ui/react",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "springs",
    "use-proximity-hover"
  ],
  "files": [
    {
      "path": "src/components/ui/context-menu.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  useState,\n  type ComponentProps,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { ContextMenu as ContextMenuPrimitive } from \"@base-ui/react/context-menu\";\nimport { motion, AnimatePresence, useReducedMotion } from \"motion/react\";\nimport { CheckIcon, ChevronRightIcon } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { spring } from \"@/lib/springs\";\nimport { useProximityHover, proximityHoverWashClassName } from \"@/hooks/use-proximity-hover\";\n\ninterface ContextMenuProximityContextValue {\n  registerItem: (index: number, element: HTMLElement | null) => void;\n}\n\nconst ContextMenuProximityContext = createContext<ContextMenuProximityContextValue | null>(null);\n\ntype ContextMenuIndexProp = { _index?: number };\n\nfunction useContextMenuItemRegistration(ref: React.RefObject<HTMLElement | null>, index?: number) {\n  const ctx = useContext(ContextMenuProximityContext);\n  useEffect(() => {\n    if (index === undefined || !ctx) return;\n    ctx.registerItem(index, ref.current);\n    return () => ctx.registerItem(index, null);\n    // eslint-disable-next-line react-hooks/exhaustive-deps\n  }, [index, ctx]);\n}\n\nfunction indexContextMenuChildren(children: ReactNode, counter: { current: number }): ReactNode {\n  return Children.map(children, (child) => {\n    if (!isValidElement(child)) return child;\n    if (child.type === ContextMenuGroup || child.type === ContextMenuRadioGroup) {\n      const groupProps = child.props as { children?: ReactNode };\n      return cloneElement(child as ReactElement<{ children?: ReactNode }>, {\n        children: indexContextMenuChildren(groupProps.children, counter),\n      });\n    }\n    // ContextMenuSub's trigger is indexed as a row in *this* popup, but Base\n    // UI requires it nested one level inside ContextMenuSub (alongside the\n    // portaled SubContent), so that level is unwrapped here rather than\n    // recursed into — SubContent is a separate popup with its own\n    // independent index space and must be left alone.\n    if (child.type === ContextMenuSub) {\n      const subProps = child.props as { children?: ReactNode };\n      return cloneElement(child as ReactElement<{ children?: ReactNode }>, {\n        children: Children.map(subProps.children, (subChild) => {\n          if (isValidElement(subChild) && subChild.type === ContextMenuSubTrigger) {\n            return cloneElement(subChild as ReactElement<ContextMenuIndexProp>, {\n              _index: counter.current++,\n            });\n          }\n          return subChild;\n        }),\n      });\n    }\n    if (\n      child.type === ContextMenuItem ||\n      child.type === ContextMenuCheckboxItem ||\n      child.type === ContextMenuRadioItem ||\n      child.type === ContextMenuSubTrigger\n    ) {\n      return cloneElement(child as ReactElement<ContextMenuIndexProp>, {\n        _index: counter.current++,\n      });\n    }\n    return child;\n  });\n}\n\n// ─── Context Menu ────────────────────────────────────────────────────────────\n\nfunction ContextMenu({ ...props }: ContextMenuPrimitive.Root.Props) {\n  return <ContextMenuPrimitive.Root data-slot=\"context-menu\" {...props} />;\n}\n\nfunction ContextMenuTrigger({ className, ...props }: ContextMenuPrimitive.Trigger.Props) {\n  return (\n    <ContextMenuPrimitive.Trigger\n      data-slot=\"context-menu-trigger\"\n      className={cn(\"select-none\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuPortal({ ...props }: ContextMenuPrimitive.Portal.Props) {\n  return <ContextMenuPrimitive.Portal data-slot=\"context-menu-portal\" {...props} />;\n}\n\nfunction ContextMenuMotionSurface({\n  popupProps,\n  contentProps,\n  transitionStatus,\n  pointerTravel,\n  reduceMotion,\n  handlers,\n  className,\n  children,\n}: {\n  popupProps: Record<string, unknown>;\n  contentProps: Record<string, unknown>;\n  transitionStatus: string | undefined;\n  pointerTravel: { x: number; y: number };\n  reduceMotion: boolean | null;\n  handlers: Pick<\n    React.DOMAttributes<HTMLDivElement>,\n    \"onMouseMove\" | \"onMouseEnter\" | \"onMouseLeave\"\n  >;\n  className?: string;\n  children: ReactNode;\n}) {\n  // Base UI clears its initial \"starting\" state in the same frame that this\n  // popup mounts. Hold the first visual frame ourselves so Motion has a\n  // painted origin before it begins the pointer-side travel.\n  const [entered, setEntered] = useState(Boolean(reduceMotion));\n  useEffect(() => {\n    if (!reduceMotion) setEntered(true);\n  }, [reduceMotion]);\n\n  const exiting = transitionStatus === \"ending\";\n  const hidden = exiting || !entered;\n\n  return (\n    <motion.div\n      {...popupProps}\n      {...contentProps}\n      {...handlers}\n      className={cn(\n        \"relative z-50 max-h-(--available-height) min-w-40 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover p-1 text-popover-foreground shadow-popover outline-none\",\n        className,\n      )}\n      initial={false}\n      animate={hidden ? { opacity: 0, ...pointerTravel } : { opacity: 1, x: 0, y: 0 }}\n      transition={exiting ? spring.moderate.exit : spring.moderate.enter}\n    >\n      {children}\n    </motion.div>\n  );\n}\n\n/** Same popup styling/motion/proximity hover as MenuContent — see that\n * component's docstring in menu.tsx. `side=\"right\"` default rather than\n * `\"bottom\"`: a context menu opens from the click point outward, not below\n * an anchor element. */\nfunction ContextMenuContent({\n  align = \"start\",\n  alignOffset = 4,\n  side = \"right\",\n  sideOffset = 2,\n  className,\n  children,\n  ...props\n}: ContextMenuPrimitive.Popup.Props &\n  Pick<ContextMenuPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const { activeIndex, itemRects, handlers, registerItem, measureItems } = useProximityHover(\n    containerRef,\n    { axis: \"y\" },\n  );\n  const reduceMotion = useReducedMotion();\n  const pointerTravel = reduceMotion\n    ? { x: 0, y: 0 }\n    : {\n        x: side === \"right\" ? -6 : side === \"left\" ? 6 : 0,\n        y: side === \"bottom\" ? -6 : side === \"top\" ? 6 : 0,\n      };\n\n  useEffect(() => {\n    measureItems();\n  }, [measureItems, children]);\n\n  const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;\n  const indexedChildren = indexContextMenuChildren(children, { current: 0 });\n  // Without this, `{ registerItem }` is a fresh object every render, so\n  // every item's registration effect (keyed on this context value) re-fires\n  // every render, bumps useProximityHover's registerTick, and re-renders\n  // this popup — an infinite loop caught as \"Maximum update depth\n  // exceeded.\" `registerItem` itself is already a stable useCallback.\n  const proximityContextValue = useMemo(() => ({ registerItem }), [registerItem]);\n\n  return (\n    <ContextMenuPortal>\n      <ContextMenuPrimitive.Positioner\n        data-slot=\"context-menu-positioner\"\n        align={align}\n        alignOffset={alignOffset}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"z-50 outline-none\"\n      >\n        <ContextMenuPrimitive.Popup\n          ref={containerRef}\n          data-slot=\"context-menu-content\"\n          render={(popupProps, state) => {\n            return (\n              <ContextMenuMotionSurface\n                popupProps={popupProps as Record<string, unknown>}\n                contentProps={props as Record<string, unknown>}\n                transitionStatus={state.transitionStatus}\n                pointerTravel={pointerTravel}\n                reduceMotion={reduceMotion}\n                handlers={handlers}\n                className={typeof className === \"function\" ? className(state) : className}\n              >\n                <AnimatePresence>\n                  {activeRect && (\n                    <motion.div\n                      className={cn(\n                        \"pointer-events-none absolute rounded-md\",\n                        proximityHoverWashClassName,\n                      )}\n                      initial={{\n                        top: activeRect.top,\n                        left: activeRect.left,\n                        width: activeRect.width,\n                        height: activeRect.height,\n                      }}\n                      animate={{\n                        top: activeRect.top,\n                        left: activeRect.left,\n                        width: activeRect.width,\n                        height: activeRect.height,\n                      }}\n                      transition={spring.fast.enter}\n                    />\n                  )}\n                </AnimatePresence>\n                <ContextMenuProximityContext.Provider value={proximityContextValue}>\n                  {indexedChildren}\n                </ContextMenuProximityContext.Provider>\n              </ContextMenuMotionSurface>\n            );\n          }}\n        />\n      </ContextMenuPrimitive.Positioner>\n    </ContextMenuPortal>\n  );\n}\n\nfunction ContextMenuGroup({ ...props }: ContextMenuPrimitive.Group.Props) {\n  return <ContextMenuPrimitive.Group data-slot=\"context-menu-group\" {...props} />;\n}\n\nfunction ContextMenuLabel({\n  className,\n  inset,\n  ...props\n}: ContextMenuPrimitive.GroupLabel.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <ContextMenuPrimitive.GroupLabel\n      data-slot=\"context-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-label text-muted-foreground uppercase data-inset:pl-7\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  _index,\n  ...props\n}: ContextMenuPrimitive.Item.Props &\n  ContextMenuIndexProp & {\n    inset?: boolean;\n    variant?: \"default\" | \"destructive\";\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useContextMenuItemRegistration(ref, _index);\n\n  return (\n    <ContextMenuPrimitive.Item\n      ref={ref}\n      data-slot=\"context-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"group/context-menu-item relative z-10 flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-control text-muted-foreground outline-none transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:text-foreground data-inset:pl-7 data-[variant=destructive]:text-destructive data-[variant=destructive]:data-highlighted:text-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg]:text-muted-foreground data-highlighted:[&_svg]:text-foreground data-[variant=destructive]:[&_svg]:text-destructive\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuSub({ ...props }: ContextMenuPrimitive.SubmenuRoot.Props) {\n  return <ContextMenuPrimitive.SubmenuRoot data-slot=\"context-menu-sub\" {...props} />;\n}\n\nfunction ContextMenuSubTrigger({\n  className,\n  inset,\n  children,\n  _index,\n  ...props\n}: ContextMenuPrimitive.SubmenuTrigger.Props &\n  ContextMenuIndexProp & {\n    inset?: boolean;\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useContextMenuItemRegistration(ref, _index);\n\n  return (\n    <ContextMenuPrimitive.SubmenuTrigger\n      ref={ref}\n      data-slot=\"context-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        // Persistent \"submenu open\" tint, not the transient hover wash (that's\n        // data-highlighted, painted separately by the proximity pill below).\n        // bg-accent would sit ~0.03 L off this popup's bg-popover in dark\n        // mode — see --active's definition in globals.css.\n        \"relative z-10 flex cursor-default items-center gap-2 rounded-md px-2 py-1.5 text-control text-muted-foreground outline-none transition-colors select-none data-inset:pl-7 data-highlighted:text-foreground data-popup-open:bg-active data-popup-open:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg]:text-muted-foreground\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto\" />\n    </ContextMenuPrimitive.SubmenuTrigger>\n  );\n}\n\n/** Nests one popup inside another, both already at the `--popover`\n * elevation step — same call menu.tsx's MenuSubContent makes, see its\n * docstring. */\nfunction ContextMenuSubContent({\n  align = \"start\",\n  alignOffset = -4,\n  side = \"right\",\n  sideOffset = 2,\n  className,\n  ...props\n}: ComponentProps<typeof ContextMenuContent>) {\n  return (\n    <ContextMenuContent\n      data-slot=\"context-menu-sub-content\"\n      align={align}\n      alignOffset={alignOffset}\n      side={side}\n      sideOffset={sideOffset}\n      className={cn(\"min-w-32\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  inset,\n  _index,\n  ...props\n}: ContextMenuPrimitive.CheckboxItem.Props &\n  ContextMenuIndexProp & {\n    inset?: boolean;\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useContextMenuItemRegistration(ref, _index);\n\n  return (\n    <ContextMenuPrimitive.CheckboxItem\n      ref={ref}\n      data-slot=\"context-menu-checkbox-item\"\n      data-inset={inset}\n      className={cn(\n        \"group/context-menu-item relative z-10 flex cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-control text-muted-foreground outline-none transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:text-foreground data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span\n        className=\"pointer-events-none absolute right-2 flex items-center justify-center\"\n        data-slot=\"context-menu-checkbox-item-indicator\"\n      >\n        <ContextMenuPrimitive.CheckboxItemIndicator\n          keepMounted\n          render={(indicatorProps, state) => {\n            const visible = state.checked && state.transitionStatus !== \"ending\";\n            return (\n              <motion.span\n                {...(indicatorProps as Record<string, unknown>)}\n                initial={false}\n                animate={{ opacity: visible ? 1 : 0, scale: visible ? 1 : 0.5 }}\n                transition={visible ? spring.fast.enter : spring.fast.exit}\n              >\n                <CheckIcon className=\"size-3.5\" />\n              </motion.span>\n            );\n          }}\n        />\n      </span>\n      {children}\n    </ContextMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction ContextMenuRadioGroup({ ...props }: ContextMenuPrimitive.RadioGroup.Props) {\n  return <ContextMenuPrimitive.RadioGroup data-slot=\"context-menu-radio-group\" {...props} />;\n}\n\nfunction ContextMenuRadioItem({\n  className,\n  children,\n  inset,\n  _index,\n  ...props\n}: ContextMenuPrimitive.RadioItem.Props &\n  ContextMenuIndexProp & {\n    inset?: boolean;\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useContextMenuItemRegistration(ref, _index);\n\n  return (\n    <ContextMenuPrimitive.RadioItem\n      ref={ref}\n      data-slot=\"context-menu-radio-item\"\n      data-inset={inset}\n      className={cn(\n        \"group/context-menu-item relative z-10 flex cursor-default items-center gap-2 rounded-md py-1.5 pr-8 pl-2 text-control text-muted-foreground outline-none transition-colors select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:text-foreground data-inset:pl-7 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span\n        className=\"pointer-events-none absolute right-2 flex items-center justify-center\"\n        data-slot=\"context-menu-radio-item-indicator\"\n      >\n        <ContextMenuPrimitive.RadioItemIndicator\n          keepMounted\n          render={(indicatorProps, state) => {\n            const visible = state.checked && state.transitionStatus !== \"ending\";\n            return (\n              <motion.span\n                {...(indicatorProps as Record<string, unknown>)}\n                initial={false}\n                animate={{ opacity: visible ? 1 : 0, scale: visible ? 1 : 0.5 }}\n                transition={visible ? spring.fast.enter : spring.fast.exit}\n                className=\"flex size-1.5 rounded-full bg-foreground\"\n              />\n            );\n          }}\n        />\n      </span>\n      {children}\n    </ContextMenuPrimitive.RadioItem>\n  );\n}\n\nfunction ContextMenuSeparator({ className, ...props }: ContextMenuPrimitive.Separator.Props) {\n  return (\n    <ContextMenuPrimitive.Separator\n      data-slot=\"context-menu-separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction ContextMenuShortcut({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"context-menu-shortcut\"\n      className={cn(\n        \"ml-auto text-meta text-muted-foreground group-data-highlighted/context-menu-item:text-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  ContextMenu,\n  ContextMenuPortal,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuGroup,\n  ContextMenuLabel,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioGroup,\n  ContextMenuRadioItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}