{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "Context Menu",
  "description": "A right-click menu that springs from the cursor point and flips to stay in the viewport, with shortcuts and destructive actions.",
  "dependencies": ["framer-motion"],
  "registryDependencies": ["@godui/godui-theme"],
  "files": [
    {
      "path": "packages/components/src/context-menu/context-menu.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useReducedMotion } from \"framer-motion\";\nimport * as React from \"react\";\n\nexport type ContextMenuItem =\n  | { type: \"separator\" }\n  | { type: \"label\"; label: React.ReactNode }\n  | {\n      type?: \"item\";\n      label: React.ReactNode;\n      icon?: React.ReactNode;\n      shortcut?: string;\n      disabled?: boolean;\n      destructive?: boolean;\n      onSelect?: () => void;\n    };\n\nexport type ContextMenuProps = React.HTMLAttributes<HTMLDivElement> & {\n  items: ContextMenuItem[];\n  children: React.ReactNode;\n};\n\ntype Coords = { x: number; y: number; flipX: boolean; flipY: boolean };\n\nconst ContextMenu = React.forwardRef<HTMLDivElement, ContextMenuProps>(\n  ({ items, children, className, ...props }, ref) => {\n    const reduceMotion = useReducedMotion();\n    const [coords, setCoords] = React.useState<Coords | null>(null);\n    const menuRef = React.useRef<HTMLDivElement>(null);\n\n    React.useEffect(() => {\n      if (!coords) return;\n      const onDown = (e: MouseEvent) => {\n        if (menuRef.current && !menuRef.current.contains(e.target as Node)) {\n          setCoords(null);\n        }\n      };\n      const onScroll = () => setCoords(null);\n      const onKey = (e: KeyboardEvent) => {\n        if (e.key === \"Escape\") setCoords(null);\n      };\n      document.addEventListener(\"mousedown\", onDown);\n      window.addEventListener(\"scroll\", onScroll, true);\n      document.addEventListener(\"keydown\", onKey);\n      return () => {\n        document.removeEventListener(\"mousedown\", onDown);\n        window.removeEventListener(\"scroll\", onScroll, true);\n        document.removeEventListener(\"keydown\", onKey);\n      };\n    }, [coords]);\n\n    React.useEffect(() => {\n      if (coords) {\n        menuRef.current\n          ?.querySelector<HTMLElement>(\"[data-menu-item]\")\n          ?.focus();\n      }\n    }, [coords]);\n\n    const openAt = (clientX: number, clientY: number) => {\n      const menuW = 220;\n      const menuH = Math.min(items.length * 40 + 12, 360);\n      const flipX = clientX + menuW > window.innerWidth;\n      const flipY = clientY + menuH > window.innerHeight;\n      setCoords({ x: clientX, y: clientY, flipX, flipY });\n    };\n\n    const moveFocus = (dir: 1 | -1) => {\n      const nodes = Array.from(\n        menuRef.current?.querySelectorAll<HTMLElement>(\"[data-menu-item]\") ??\n          [],\n      );\n      if (!nodes.length) return;\n      const idx = nodes.indexOf(document.activeElement as HTMLElement);\n      nodes[(idx + dir + nodes.length) % nodes.length]?.focus();\n    };\n\n    const spring = reduceMotion\n      ? { duration: 0 }\n      : ({ type: \"spring\", stiffness: 520, damping: 32 } as const);\n\n    return (\n      <div ref={ref} className={className} {...props}>\n        {/* biome-ignore lint/a11y/noStaticElementInteractions: right-click capture region */}\n        <div\n          onContextMenu={(e) => {\n            e.preventDefault();\n            openAt(e.clientX, e.clientY);\n          }}\n          className=\"h-full w-full\"\n        >\n          {children}\n        </div>\n\n        <AnimatePresence>\n          {coords && (\n            <motion.div\n              ref={menuRef}\n              role=\"menu\"\n              initial={\n                reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9 }\n              }\n              animate={{ opacity: 1, scale: 1 }}\n              exit={reduceMotion ? { opacity: 0 } : { opacity: 0, scale: 0.9 }}\n              transition={spring}\n              onKeyDown={(e) => {\n                if (e.key === \"ArrowDown\") {\n                  e.preventDefault();\n                  moveFocus(1);\n                } else if (e.key === \"ArrowUp\") {\n                  e.preventDefault();\n                  moveFocus(-1);\n                }\n              }}\n              style={{\n                position: \"fixed\",\n                left: coords.flipX ? undefined : coords.x,\n                right: coords.flipX ? window.innerWidth - coords.x : undefined,\n                top: coords.flipY ? undefined : coords.y,\n                bottom: coords.flipY\n                  ? window.innerHeight - coords.y\n                  : undefined,\n                transformOrigin: `${coords.flipX ? \"right\" : \"left\"} ${\n                  coords.flipY ? \"bottom\" : \"top\"\n                }`,\n              }}\n              className=\"z-popover min-w-52 rounded-xl border border-border bg-background p-1 shadow-2xl\"\n            >\n              {items.map((item, index) => {\n                if (item.type === \"separator\") {\n                  return (\n                    <hr\n                      // biome-ignore lint/suspicious/noArrayIndexKey: positional menu structure\n                      key={`sep-${index}`}\n                      className=\"my-1 border-border border-t-0 border-b\"\n                    />\n                  );\n                }\n                if (item.type === \"label\") {\n                  return (\n                    <div\n                      // biome-ignore lint/suspicious/noArrayIndexKey: positional menu structure\n                      key={`label-${index}`}\n                      className=\"px-3 pt-2 pb-1 font-medium text-muted-foreground text-xs\"\n                    >\n                      {item.label}\n                    </div>\n                  );\n                }\n                return (\n                  <button\n                    // biome-ignore lint/suspicious/noArrayIndexKey: positional menu structure\n                    key={`item-${index}`}\n                    type=\"button\"\n                    role=\"menuitem\"\n                    data-menu-item\n                    disabled={item.disabled}\n                    onClick={() => {\n                      item.onSelect?.();\n                      setCoords(null);\n                    }}\n                    className={`group flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-sm outline-none [transition:background-color_120ms_ease] focus:bg-accent hover:bg-accent disabled:pointer-events-none disabled:opacity-40 ${\n                      item.destructive\n                        ? \"text-destructive focus:bg-destructive/10 hover:bg-destructive/10\"\n                        : \"text-foreground\"\n                    }`}\n                  >\n                    {item.icon && (\n                      <span className=\"shrink-0 opacity-70\">{item.icon}</span>\n                    )}\n                    <span className=\"flex-1 truncate\">{item.label}</span>\n                    {item.shortcut && (\n                      <span className=\"ml-auto text-muted-foreground text-xs tracking-widest\">\n                        {item.shortcut}\n                      </span>\n                    )}\n                  </button>\n                );\n              })}\n            </motion.div>\n          )}\n        </AnimatePresence>\n      </div>\n    );\n  },\n);\nContextMenu.displayName = \"ContextMenu\";\n\nexport { ContextMenu };\n",
      "type": "registry:ui",
      "target": "components/godui/context-menu.tsx"
    }
  ],
  "type": "registry:ui"
}
