{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu-motion",
  "title": "Menu-motion context",
  "description": "Styled context: the Menu-motion set. 5 decorative context variations (PopContextMenu, FadeContextMenu, SlideContextMenu, SpringContextMenu, ScaleContextMenu) 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"
  ],
  "files": [
    {
      "path": "registry/ai2/styled/context-menu-motion.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\"\n\n/* Motion context menu family: 5 opening motions. 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 ENTER/EXIT motion: pop (an overshooting\n   scale), fade (opacity only), slide (sliding down from above), spring (springy),\n   scale (growing vertically). No transform under reduced motion, only a fade.\n   NO radix or portal. Color comes ONLY from tokens, via alpha color-mix. */\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\nconst menuBase =\n  \"fixed z-50 origin-top-left overflow-hidden rounded-xl border border-border bg-popover p-1.5 text-sm text-popover-foreground shadow-lg 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-accent hover:text-accent-foreground focus-visible:bg-accent focus-visible:text-accent-foreground 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\ntype MenuMotion = {\n  initial: Record<string, number>\n  animate: Record<string, number>\n  exit: Record<string, number>\n  transition: Record<string, unknown>\n}\n\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. menuMotion carries the variant's enter/exit motion; it falls back to a fade under reduced motion. */\nfunction MotionContextMenuShell({\n  className,\n  size = \"md\",\n  children,\n  render,\n  menuMotion,\n}: {\n  className?: string\n  size?: StyledSize\n  children?: React.ReactNode\n  render: (close: () => void) => React.ReactNode\n  menuMotion: MenuMotion\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 fade: MenuMotion = {\n    initial: { opacity: 0 },\n    animate: { opacity: 1 },\n    exit: { opacity: 0 },\n    transition: { duration: 0.12, ease: \"easeOut\" },\n  }\n  const anim = reduce ? fade : menuMotion\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, menuSize[size])}\n            initial={anim.initial}\n            animate={anim.animate}\n            exit={anim.exit}\n            transition={anim.transition}\n          >\n            {render(close)}\n          </motion.div>\n        ) : null}\n      </AnimatePresence>\n    </div>\n  )\n}\n\nfunction MotionMenu({\n  className,\n  size,\n  children,\n  items,\n  menuMotion,\n}: ContextMenuProps & { menuMotion: MenuMotion }) {\n  const list = items ?? defaultItems\n  return (\n    <MotionContextMenuShell\n      className={className}\n      size={size}\n      menuMotion={menuMotion}\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    </MotionContextMenuShell>\n  )\n}\n\n/* Pop: kucukten hizli firlayan yayli scale. */\nexport function PopContextMenu(props: ContextMenuProps) {\n  return (\n    <MotionMenu\n      {...props}\n      menuMotion={{\n        initial: { opacity: 0, scale: 0.8 },\n        animate: { opacity: 1, scale: 1 },\n        exit: { opacity: 0, scale: 0.8 },\n        transition: { type: \"spring\" as const, stiffness: 520, damping: 26 },\n      }}\n    />\n  )\n}\n\n/* Fade: opacity only, no transform. */\nexport function FadeContextMenu(props: ContextMenuProps) {\n  return (\n    <MotionMenu\n      {...props}\n      menuMotion={{\n        initial: { opacity: 0 },\n        animate: { opacity: 1 },\n        exit: { opacity: 0 },\n        transition: { duration: 0.18, ease: \"easeOut\" },\n      }}\n    />\n  )\n}\n\n/* Slide: yukaridan asagi kayarak girer. */\nexport function SlideContextMenu(props: ContextMenuProps) {\n  return (\n    <MotionMenu\n      {...props}\n      menuMotion={{\n        initial: { opacity: 0, y: -8 },\n        animate: { opacity: 1, y: 0 },\n        exit: { opacity: 0, y: -8 },\n        transition: { duration: 0.16, ease: \"easeOut\" },\n      }}\n    />\n  )\n}\n\n/* Spring: yumusak, salinimli yay ile hem scale hem kayma. */\nexport function SpringContextMenu(props: ContextMenuProps) {\n  return (\n    <MotionMenu\n      {...props}\n      menuMotion={{\n        initial: { opacity: 0, scale: 0.92, y: -6 },\n        animate: { opacity: 1, scale: 1, y: 0 },\n        exit: { opacity: 0, scale: 0.92, y: -6 },\n        transition: { type: \"spring\" as const, stiffness: 320, damping: 18 },\n      }}\n    />\n  )\n}\n\n/* Scale: origin-top-left'ten dikey buyume. */\nexport function ScaleContextMenu(props: ContextMenuProps) {\n  return (\n    <MotionMenu\n      {...props}\n      menuMotion={{\n        initial: { opacity: 0, scaleY: 0.6 },\n        animate: { opacity: 1, scaleY: 1 },\n        exit: { opacity: 0, scaleY: 0.6 },\n        transition: { duration: 0.18, ease: \"easeOut\" },\n      }}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ui/context-menu-motion.tsx"
    }
  ],
  "categories": [
    "styled",
    "context"
  ],
  "type": "registry:component"
}