{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "menu",
  "title": "Menu",
  "description": "Base UI menu with submenus, checkbox/radio items, and a spring-driven popup.",
  "dependencies": [
    "@base-ui/react",
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "springs",
    "use-proximity-hover"
  ],
  "files": [
    {
      "path": "src/components/ui/menu.tsx",
      "content": "\"use client\";\n\nimport {\n  Children,\n  cloneElement,\n  createContext,\n  isValidElement,\n  useContext,\n  useEffect,\n  useMemo,\n  useRef,\n  type ComponentProps,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport { Menu as MenuPrimitive } from \"@base-ui/react/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\n// ─── Proximity hover ─────────────────────────────────────────────────────────\n// Every MenuContent/MenuSubContent popup owns one useProximityHover instance\n// — the same measured-rect hover wash Accordion/Tabs use, scoped to that\n// popup's own items so a submenu's pill never reaches into its parent's.\n// Base UI's own `data-highlighted` (keyboard nav and pointer hover both set\n// it) still drives each row's text color, same split Accordion uses between\n// its pill-owns-background and item-owns-text-color.\n\ninterface MenuProximityContextValue {\n  registerItem: (index: number, element: HTMLElement | null) => void;\n}\n\nconst MenuProximityContext = createContext<MenuProximityContextValue | null>(null);\n\n/** Position for proximity hover — auto-assigned by MenuContent's child walk; only present on items it recognized as interactive rows. */\ntype MenuIndexProp = { _index?: number };\n\nfunction useMenuItemRegistration(ref: React.RefObject<HTMLElement | null>, index?: number) {\n  const ctx = useContext(MenuProximityContext);\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\n/**\n * Auto-indexes MenuContent's children so callers never hand-thread an index\n * just for proximity hover — mirrors Accordion's indexedChildren, but as a\n * recursive walk (not a flat Children.map) since indexable rows can sit one\n * level down inside a MenuGroup/MenuRadioGroup. MenuSub's trigger is indexed\n * as a row in *this* popup, but Base UI requires it nested one level inside\n * MenuSub (alongside the portaled SubContent), so that level is unwrapped\n * here rather than recursed into — SubContent is a separate popup with its\n * own independent index space and must be left alone.\n */\nfunction indexMenuChildren(children: ReactNode, counter: { current: number }): ReactNode {\n  return Children.map(children, (child) => {\n    if (!isValidElement(child)) return child;\n    if (child.type === MenuGroup || child.type === MenuRadioGroup) {\n      const groupProps = child.props as { children?: ReactNode };\n      return cloneElement(child as ReactElement<{ children?: ReactNode }>, {\n        children: indexMenuChildren(groupProps.children, counter),\n      });\n    }\n    if (child.type === MenuSub) {\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 === MenuSubTrigger) {\n            return cloneElement(subChild as ReactElement<MenuIndexProp>, {\n              _index: counter.current++,\n            });\n          }\n          return subChild;\n        }),\n      });\n    }\n    if (\n      child.type === MenuItem ||\n      child.type === MenuCheckboxItem ||\n      child.type === MenuRadioItem ||\n      child.type === MenuSubTrigger\n    ) {\n      return cloneElement(child as ReactElement<MenuIndexProp>, { _index: counter.current++ });\n    }\n    return child;\n  });\n}\n\n// ─── Menu ────────────────────────────────────────────────────────────────────\n\nfunction Menu({ ...props }: MenuPrimitive.Root.Props) {\n  return <MenuPrimitive.Root data-slot=\"menu\" {...props} />;\n}\n\nfunction MenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) {\n  return <MenuPrimitive.Trigger data-slot=\"menu-trigger\" {...props} />;\n}\n\nfunction MenuPortal({ ...props }: MenuPrimitive.Portal.Props) {\n  return <MenuPrimitive.Portal data-slot=\"menu-portal\" {...props} />;\n}\n\n/**\n * Anchored floating list — same elevation step as Popover, entering from a\n * small trigger-facing offset on `spring.moderate`. Sized to its content\n * (`min-w-40`).\n */\nfunction MenuContent({\n  align = \"start\",\n  alignOffset = 0,\n  side = \"bottom\",\n  sideOffset = 4,\n  className,\n  children,\n  ...props\n}: MenuPrimitive.Popup.Props &\n  Pick<MenuPrimitive.Positioner.Props, \"align\" | \"alignOffset\" | \"side\" | \"sideOffset\">) {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const reduceMotion = useReducedMotion();\n  const { activeIndex, itemRects, handlers, registerItem, measureItems } = useProximityHover(\n    containerRef,\n    { axis: \"y\" },\n  );\n\n  useEffect(() => {\n    measureItems();\n  }, [measureItems, children]);\n\n  const activeRect = activeIndex !== null ? itemRects[activeIndex] : null;\n  const indexedChildren = indexMenuChildren(children, { current: 0 });\n  const triggerOffset = reduceMotion\n    ? { x: 0, y: 0 }\n    : {\n        x: side === \"right\" ? -4 : side === \"left\" ? 4 : 0,\n        y: side === \"bottom\" ? -4 : side === \"top\" ? 4 : 0,\n      };\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    <MenuPortal>\n      <MenuPrimitive.Positioner\n        data-slot=\"menu-positioner\"\n        align={align}\n        alignOffset={alignOffset}\n        side={side}\n        sideOffset={sideOffset}\n        className=\"z-50 outline-none\"\n      >\n        <MenuPrimitive.Popup\n          ref={containerRef}\n          data-slot=\"menu-content\"\n          render={(popupProps, state) => {\n            const exiting = state.transitionStatus === \"ending\";\n            return (\n              <motion.div\n                {...(popupProps as Record<string, unknown>)}\n                {...(props as Record<string, unknown>)}\n                onMouseMove={handlers.onMouseMove}\n                onMouseEnter={handlers.onMouseEnter}\n                onMouseLeave={handlers.onMouseLeave}\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={{ opacity: 0, ...triggerOffset }}\n                animate={{\n                  opacity: exiting ? 0 : 1,\n                  x: exiting ? triggerOffset.x : 0,\n                  y: exiting ? triggerOffset.y : 0,\n                }}\n                transition={exiting ? spring.moderate.exit : spring.moderate.enter}\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                <MenuProximityContext.Provider value={proximityContextValue}>\n                  {indexedChildren}\n                </MenuProximityContext.Provider>\n              </motion.div>\n            );\n          }}\n        />\n      </MenuPrimitive.Positioner>\n    </MenuPortal>\n  );\n}\n\nfunction MenuGroup({ ...props }: MenuPrimitive.Group.Props) {\n  return <MenuPrimitive.Group data-slot=\"menu-group\" {...props} />;\n}\n\nfunction MenuLabel({\n  className,\n  inset,\n  ...props\n}: MenuPrimitive.GroupLabel.Props & {\n  inset?: boolean;\n}) {\n  return (\n    <MenuPrimitive.GroupLabel\n      data-slot=\"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 MenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  _index,\n  ...props\n}: MenuPrimitive.Item.Props &\n  MenuIndexProp & {\n    inset?: boolean;\n    variant?: \"default\" | \"destructive\";\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useMenuItemRegistration(ref, _index);\n\n  return (\n    <MenuPrimitive.Item\n      ref={ref}\n      data-slot=\"menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"group/menu-item relative z-10 flex cursor-pointer 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:cursor-not-allowed 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 MenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) {\n  return <MenuPrimitive.SubmenuRoot data-slot=\"menu-sub\" {...props} />;\n}\n\nfunction MenuSubTrigger({\n  className,\n  inset,\n  children,\n  _index,\n  ...props\n}: MenuPrimitive.SubmenuTrigger.Props &\n  MenuIndexProp & {\n    inset?: boolean;\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useMenuItemRegistration(ref, _index);\n\n  return (\n    <MenuPrimitive.SubmenuTrigger\n      ref={ref}\n      data-slot=\"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-pointer 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    </MenuPrimitive.SubmenuTrigger>\n  );\n}\n\n/**\n * Reuses MenuContent (same popup styling/motion/proximity hover) with\n * defaults suited to a submenu's anchor: it opens off its trigger's right\n * edge rather than below it. Nests one popup inside another, both already\n * at the `--popover` step — there's no elevation level past that on the\n * documented ladder yet, so this stays at the same step rather than\n * skipping ahead of it.\n */\nfunction MenuSubContent({\n  align = \"start\",\n  alignOffset = -4,\n  side = \"right\",\n  sideOffset = 2,\n  className,\n  ...props\n}: ComponentProps<typeof MenuContent>) {\n  return (\n    <MenuContent\n      data-slot=\"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 MenuCheckboxItem({\n  className,\n  children,\n  checked,\n  inset,\n  _index,\n  ...props\n}: MenuPrimitive.CheckboxItem.Props &\n  MenuIndexProp & {\n    inset?: boolean;\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useMenuItemRegistration(ref, _index);\n\n  return (\n    <MenuPrimitive.CheckboxItem\n      ref={ref}\n      data-slot=\"menu-checkbox-item\"\n      data-inset={inset}\n      className={cn(\n        \"group/menu-item relative z-10 flex cursor-pointer 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:cursor-not-allowed 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=\"menu-checkbox-item-indicator\"\n      >\n        {/* Selection indicators are `spring.fast` — the same tier the motion\n            table names for that exact role. `keepMounted` lets framer play\n            the pop-out on uncheck instead of Base UI unmounting it first. */}\n        <MenuPrimitive.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    </MenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction MenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) {\n  return <MenuPrimitive.RadioGroup data-slot=\"menu-radio-group\" {...props} />;\n}\n\nfunction MenuRadioItem({\n  className,\n  children,\n  inset,\n  indicator = \"dot\",\n  _index,\n  ...props\n}: MenuPrimitive.RadioItem.Props &\n  MenuIndexProp & {\n    inset?: boolean;\n    /** Defaults to the compact dot used by generic radio menus. */\n    indicator?: \"dot\" | \"check\";\n  }) {\n  const ref = useRef<HTMLDivElement>(null);\n  useMenuItemRegistration(ref, _index);\n\n  return (\n    <MenuPrimitive.RadioItem\n      ref={ref}\n      data-slot=\"menu-radio-item\"\n      data-inset={inset}\n      className={cn(\n        \"group/menu-item relative z-10 flex cursor-pointer 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:cursor-not-allowed 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=\"menu-radio-item-indicator\"\n      >\n        <MenuPrimitive.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={\n                  indicator === \"dot\" ? \"flex size-1.5 rounded-full bg-foreground\" : \"flex\"\n                }\n              >\n                {indicator === \"check\" ? <CheckIcon className=\"size-3.5\" /> : null}\n              </motion.span>\n            );\n          }}\n        />\n      </span>\n      {children}\n    </MenuPrimitive.RadioItem>\n  );\n}\n\nfunction MenuSeparator({ className, ...props }: MenuPrimitive.Separator.Props) {\n  return (\n    <MenuPrimitive.Separator\n      data-slot=\"menu-separator\"\n      className={cn(\"-mx-1 my-1 h-px bg-border\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction MenuShortcut({ className, ...props }: ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"menu-shortcut\"\n      className={cn(\n        \"ml-auto text-meta text-muted-foreground group-data-highlighted/menu-item:text-foreground\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  Menu,\n  MenuPortal,\n  MenuTrigger,\n  MenuContent,\n  MenuGroup,\n  MenuLabel,\n  MenuItem,\n  MenuCheckboxItem,\n  MenuRadioGroup,\n  MenuRadioItem,\n  MenuSeparator,\n  MenuShortcut,\n  MenuSub,\n  MenuSubTrigger,\n  MenuSubContent,\n};\n",
      "type": "registry:ui"
    }
  ],
  "type": "registry:ui"
}