{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu-glass",
  "title": "Menu-glass context",
  "description": "Styled context: the Menu-glass set. 5 decorative context variations (FrostContextMenu, TintContextMenu, SmokeContextMenu, CrystalContextMenu, DepthContextMenu) on the ai2 token system, powered by framer-motion, reduced-motion aware and sized sm to xl. Part of the free styled layer.",
  "dependencies": [
    "motion@^12.42.2",
    "lucide-react@^1.23.0"
  ],
  "registryDependencies": [
    "@ai2/tokens",
    "@ai2/glass"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/context-menu-glass.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { ClipboardPaste, Copy, Scissors, Share2, Star } from \"lucide-react\"\nimport { AnimatePresence, motion, useReducedMotion } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { glassDepth } from \"@/registry/ai2/ui/glass\"\n\n/* Glass context menu family: 5 frosted panels. The shell is the same as\n   context-menu-styled - the target area opens a fixed menu at the cursor via\n   onContextMenu (preventDefault), and it closes on an outside pointerdown / Escape /\n   item selection. The only difference is the PANEL surface: each variant carries a\n   different frosted-glass technique (neutral frost, primary tint, dark smoke, sharp\n   crystal, layered depth). NO radix or portal. Color comes ONLY from tokens, via\n   alpha color-mix. The coordinates come from the contextmenu event, with no\n   randomness.\n\n   The glass surface derives from the glassDepth scale in @ai2/glass (AGENTS.md 4.5)\n   - the blur is never hand-written. Because glassDepth carries a [box-shadow]\n   signature, the shadow-lg in menuBase was REMOVED (same tailwind-merge group; had\n   it stayed, the signature would have silently dropped) - the ambient shadow is\n   part of the signature. */\n\nexport type StyledSize = \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type StyledContextMenuItem = {\n  label: React.ReactNode\n  icon?: React.ReactNode\n  onSelect?: () => void\n  shortcut?: React.ReactNode\n  group?: string\n  danger?: boolean\n}\n\ntype ContextMenuProps = {\n  className?: string\n  size?: StyledSize\n  children?: React.ReactNode\n  items?: StyledContextMenuItem[]\n}\n\nconst menuSize: Record<StyledSize, string> = {\n  sm: \"w-48\",\n  md: \"w-56\",\n  lg: \"w-64\",\n  xl: \"w-72\",\n}\n\nconst targetBox =\n  \"flex h-28 w-full select-none items-center justify-center rounded-xl border border-dashed border-border bg-[color-mix(in_oklab,var(--color-foreground)_3%,transparent)] px-6 text-center text-sm text-muted-foreground\"\n\n/* Panel body: each variant adds its own glass surface as a class. */\nconst menuBase =\n  \"fixed z-50 origin-top-left overflow-hidden rounded-xl p-1.5 text-sm text-popover-foreground outline-none\"\n\nconst itemBase =\n  \"flex w-full cursor-default items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-popover-foreground outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)] focus-visible:bg-[color-mix(in_oklab,var(--color-foreground)_8%,transparent)] focus-visible:ring-[3px] focus-visible:ring-ring/50 [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:text-base [&>i]:leading-none [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst itemDanger =\n  \"flex w-full cursor-default items-center gap-2 rounded-md px-3 py-2 text-left text-sm text-danger outline-none transition-colors hover:bg-[color-mix(in_oklab,var(--color-danger)_12%,transparent)] focus-visible:bg-[color-mix(in_oklab,var(--color-danger)_12%,transparent)] focus-visible:ring-[3px] focus-visible:ring-danger/40 [&>svg]:size-4 [&>svg]:shrink-0 [&>i]:text-base [&>i]:leading-none [&_svg]:size-4 [&_svg]:shrink-0 [&_i]:text-base [&_i]:leading-none\"\n\nconst shortcutChip =\n  \"ml-auto inline-flex h-5 items-center rounded border border-border bg-[color-mix(in_oklab,var(--color-foreground)_5%,transparent)] px-1.5 font-mono text-[11px] text-muted-foreground\"\n\nconst defaultItems: StyledContextMenuItem[] = [\n  { label: \"Copy\", icon: <Copy />, shortcut: \"Ctrl C\" },\n  { label: \"Cut\", icon: <Scissors />, shortcut: \"Ctrl X\" },\n  { label: \"Paste\", icon: <ClipboardPaste />, shortcut: \"Ctrl V\" },\n  { label: \"Share\", icon: <Share2 /> },\n  { label: \"Add to favorites\", icon: <Star /> },\n]\n\n/* One row: icon plus label plus an optional shortcut chip. onSelect runs on choice and the menu closes. data-menu-item is the marker for arrow-key navigation. */\nfunction MenuItemButton({\n  item,\n  close,\n}: {\n  item: StyledContextMenuItem\n  close: () => void\n}) {\n  return (\n    <button\n      type=\"button\"\n      role=\"menuitem\"\n      data-menu-item=\"true\"\n      onClick={() => {\n        item.onSelect?.()\n        close()\n      }}\n      className={item.danger ? itemDanger : itemBase}\n    >\n      {item.icon ? (\n        <span className=\"inline-flex shrink-0\" aria-hidden>\n          {item.icon}\n        </span>\n      ) : null}\n      <span className=\"flex-1 truncate\">{item.label}</span>\n      {item.shortcut ? <kbd className={shortcutChip}>{item.shortcut}</kbd> : null}\n    </button>\n  )\n}\n\n/* Shared shell: the target area plus a fixed AnimatePresence menu at the cursor position. surfaceClassName carries the variant's glass surface. */\nfunction GlassContextMenuShell({\n  className,\n  size = \"md\",\n  children,\n  render,\n  surfaceClassName,\n}: {\n  className?: string\n  size?: StyledSize\n  children?: React.ReactNode\n  render: (close: () => void) => React.ReactNode\n  surfaceClassName: string\n}) {\n  const [open, setOpen] = React.useState(false)\n  const [coords, setCoords] = React.useState({ x: 0, y: 0 })\n  const reduce = useReducedMotion()\n  const menuRef = React.useRef<HTMLDivElement>(null)\n  const menuId = React.useId()\n\n  const close = React.useCallback(() => setOpen(false), [])\n\n  const onContextMenu = (e: React.MouseEvent) => {\n    e.preventDefault()\n    setCoords({ x: e.clientX, y: e.clientY })\n    setOpen(true)\n  }\n\n  React.useEffect(() => {\n    if (!open) return\n    const onKey = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") {\n        setOpen(false)\n        return\n      }\n      if (e.key !== \"ArrowDown\" && e.key !== \"ArrowUp\") return\n      const node = menuRef.current\n      if (!node) return\n      const rows = Array.from(\n        node.querySelectorAll<HTMLElement>('[data-menu-item=\"true\"]')\n      )\n      if (rows.length === 0) return\n      e.preventDefault()\n      const current = rows.indexOf(document.activeElement as HTMLElement)\n      const next =\n        e.key === \"ArrowDown\"\n          ? (current + 1) % rows.length\n          : current <= 0\n            ? rows.length - 1\n            : current - 1\n      rows[next]?.focus()\n    }\n    const onPointer = (e: PointerEvent) => {\n      const node = menuRef.current\n      if (node && e.target instanceof Node && !node.contains(e.target)) {\n        setOpen(false)\n      }\n    }\n    window.addEventListener(\"keydown\", onKey)\n    window.addEventListener(\"pointerdown\", onPointer)\n    return () => {\n      window.removeEventListener(\"keydown\", onKey)\n      window.removeEventListener(\"pointerdown\", onPointer)\n    }\n  }, [open])\n\n  const anim = reduce\n    ? { initial: { opacity: 1 }, animate: { opacity: 1 }, exit: { opacity: 1 } }\n    : {\n        initial: { opacity: 0, scale: 0.96 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.96 },\n      }\n\n  return (\n    <div data-slot=\"styled-context-menu\" className=\"w-full\">\n      <div\n        data-slot=\"styled-context-menu-target\"\n        onContextMenu={onContextMenu}\n        className={cn(targetBox, className)}\n      >\n        {children ?? \"Right-click here\"}\n      </div>\n\n      <AnimatePresence>\n        {open ? (\n          <motion.div\n            ref={menuRef}\n            id={menuId}\n            role=\"menu\"\n            data-slot=\"styled-context-menu-content\"\n            style={{ left: coords.x, top: coords.y }}\n            className={cn(menuBase, surfaceClassName, menuSize[size])}\n            initial={anim.initial}\n            animate={anim.animate}\n            exit={anim.exit}\n            transition={reduce ? { duration: 0 } : { duration: 0.14, ease: \"easeOut\" }}\n          >\n            {render(close)}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nfunction GlassMenu({\n  className,\n  size,\n  children,\n  items,\n  surfaceClassName,\n}: ContextMenuProps & { surfaceClassName: string }) {\n  const list = items ?? defaultItems\n  return (\n    <GlassContextMenuShell\n      className={className}\n      size={size}\n      surfaceClassName={surfaceClassName}\n      render={(close) => (\n        <div className=\"flex flex-col\">\n          {list.map((item, i) => (\n            <MenuItemButton key={i} item={item} close={close} />\n          ))}\n        </div>\n      )}\n    >\n      {children}\n    </GlassContextMenuShell>\n  )\n}\n\n/* Frost: a neutral translucent surface with a light border. Depth md (8px): the library's default and exactly the step it describes for a \"menu\". The neutral ground is left to the library; the character is the soft border alone. */\nexport function FrostContextMenu(props: ContextMenuProps) {\n  return (\n    <GlassMenu\n      {...props}\n      surfaceClassName={cn(\n        glassDepth.md,\n        \"border border-[color-mix(in_oklab,var(--color-border)_70%,transparent)]\"\n      )}\n    />\n  )\n}\n\n/* Tint: an info-toned frosted surface plus an info border. Depth md (8px): deliberately the same step as Frost - the only difference is hue. The ground is overridden in both forms (with and without @supports), otherwise the library's bg-background/60 variant would hide the tint. */\nexport function TintContextMenu(props: ContextMenuProps) {\n  return (\n    <GlassMenu\n      {...props}\n      surfaceClassName={cn(\n        glassDepth.md,\n        \"border border-[color-mix(in_oklab,var(--color-info)_30%,transparent)]\",\n        \"bg-[color-mix(in_oklab,var(--color-info)_14%,var(--color-popover))] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-info)_12%,transparent)]\"\n      )}\n    />\n  )\n}\n\n/* Smoke: dark foreground smoke with a low-contrast border. Depth lg (16px): the smoke turns what is behind into texture - the scale step closest to the intent of the old 24px. */\nexport function SmokeContextMenu(props: ContextMenuProps) {\n  return (\n    <GlassMenu\n      {...props}\n      surfaceClassName={cn(\n        glassDepth.lg,\n        \"border border-[color-mix(in_oklab,var(--color-foreground)_12%,transparent)]\",\n        \"bg-[color-mix(in_oklab,var(--color-foreground)_10%,var(--color-popover))] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-foreground)_18%,transparent)]\"\n      )}\n    />\n  )\n}\n\n/* Crystal: clear glass, high saturation, bright edge. Depth sm (4px): it was already after a \"very light blur\" (the old value was 8px) and crystal = clarity. The old shadow-[inset...] was REMOVED: being in the same merge group as the signature, it was erasing it - the bright line on top is already the signature's own inset highlight. The edge brightness is driven by border plus saturate. */\nexport function CrystalContextMenu(props: ContextMenuProps) {\n  return (\n    <GlassMenu\n      {...props}\n      surfaceClassName={cn(\n        glassDepth.sm,\n        \"border border-[color-mix(in_oklab,var(--color-background)_60%,var(--color-border))]\",\n        \"bg-[color-mix(in_oklab,var(--color-popover)_55%,transparent)] supports-[backdrop-filter]:bg-[color-mix(in_oklab,var(--color-popover)_55%,transparent)]\",\n        \"supports-[backdrop-filter]:backdrop-saturate-150\"\n      )}\n    />\n  )\n}\n\n/* Depth: gradient glass from top to bottom, the deepest in the family. Depth xl (24px): the highest diffusion. The old shadow-xl was REMOVED (the same merge group as the signature); the depth is carried by the signature's ambient shadow plus the gradient layer, with the extra separation coming from an inset ring. */\nexport function DepthContextMenu(props: ContextMenuProps) {\n  return (\n    <GlassMenu\n      {...props}\n      surfaceClassName={cn(\n        glassDepth.xl,\n        \"border border-[color-mix(in_oklab,var(--color-border)_60%,transparent)]\",\n        \"bg-gradient-to-b from-[color-mix(in_oklab,var(--color-popover)_75%,transparent)] to-[color-mix(in_oklab,var(--color-popover)_45%,transparent)]\",\n        \"ring-1 ring-inset ring-[color-mix(in_oklab,var(--color-background)_30%,transparent)]\"\n      )}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/context-menu-glass.tsx"
    }
  ],
  "categories": [
    "styled",
    "context"
  ],
  "type": "registry:component"
}