{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "Context Menu",
  "author": "AlgorizeTech",
  "description": "A right-click context menu that opens at the pointer, with items, separators and shortcut hints. Use it for secondary actions on a row or canvas object.",
  "dependencies": [],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/components/custom/blocks/ContextMenuBlock.tsx",
      "content": "\"use client\";\n\nimport React, {\n  createContext,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { cn } from \"@/lib/utils\";\n\n// ==========================================\n// 0. COLOR THEMES\n// ==========================================\nexport type ContextMenuColor =\n  | \"primary\"\n  | \"indigo\"\n  | \"rose\"\n  | \"emerald\"\n  | \"blue\"\n  | \"zinc\"\n  | (string & {});\n\nconst colorThemes: Record<string, { itemHover: string; triggerHover: string }> = {\n  primary: {\n    itemHover: \"hover:bg-primary/10 hover:text-primary text-foreground\",\n    triggerHover: \"hover:border-primary/50\",\n  },\n  indigo: {\n    itemHover: \"hover:bg-indigo-500/15 hover:text-indigo-600 dark:hover:text-indigo-400 text-foreground\",\n    triggerHover: \"hover:border-indigo-500/50\",\n  },\n  rose: {\n    itemHover: \"hover:bg-rose-500/15 hover:text-rose-600 dark:hover:text-rose-400 text-foreground\",\n    triggerHover: \"hover:border-rose-500/50\",\n  },\n  emerald: {\n    itemHover: \"hover:bg-emerald-500/15 hover:text-emerald-600 dark:hover:text-emerald-400 text-foreground\",\n    triggerHover: \"hover:border-emerald-500/50\",\n  },\n  blue: {\n    itemHover: \"hover:bg-blue-500/15 hover:text-blue-600 dark:hover:text-blue-400 text-foreground\",\n    triggerHover: \"hover:border-blue-500/50\",\n  },\n  zinc: {\n    itemHover: \"hover:bg-zinc-500/15 hover:text-zinc-900 dark:hover:text-zinc-100 text-foreground\",\n    triggerHover: \"hover:border-zinc-500/50\",\n  },\n};\n\nfunction getContextMenuTheme(color?: ContextMenuColor) {\n  const c = color || \"primary\";\n  const isCustomHex = c.startsWith(\"#\") || c.startsWith(\"rgb\") || c.startsWith(\"hsl\");\n\n  if (isCustomHex) {\n    return {\n      isCustomHex: true,\n      customVars: {\n        \"--ctx-color\": c,\n        \"--ctx-color-15\": c.startsWith(\"#\") && c.length === 7 ? `${c}26` : c,\n      } as React.CSSProperties,\n      itemHover: \"hover:bg-[var(--ctx-color-15)] hover:text-[var(--ctx-color)] text-foreground\",\n      triggerHover: \"hover:border-[var(--ctx-color)]\",\n    };\n  }\n\n  const preset = colorThemes[c] || colorThemes.primary;\n  return {\n    isCustomHex: false,\n    customVars: {} as React.CSSProperties,\n    itemHover: preset.itemHover,\n    triggerHover: preset.triggerHover,\n  };\n}\n\n// ==========================================\n// 1. CONTEXT\n// ==========================================\ninterface ContextMenuContextType {\n  open: boolean;\n  setOpen: React.Dispatch<React.SetStateAction<boolean>>;\n  coords: { x: number; y: number };\n  setCoords: React.Dispatch<React.SetStateAction<{ x: number; y: number }>>;\n  color: ContextMenuColor;\n}\n\nconst ContextMenuContext = createContext<ContextMenuContextType | undefined>(undefined);\n\n// ==========================================\n// 2. ROOT COMPONENT\n// ==========================================\nexport interface ContextMenuProps {\n  children: React.ReactNode;\n  color?: ContextMenuColor;\n}\n\nexport function ContextMenu({ children, color = \"primary\" }: ContextMenuProps) {\n  const [open, setOpen] = useState(false);\n  const [coords, setCoords] = useState({ x: 0, y: 0 });\n\n  return (\n    <ContextMenuContext.Provider value={{ open, setOpen, coords, setCoords, color }}>\n      {children}\n    </ContextMenuContext.Provider>\n  );\n}\n\n// ==========================================\n// 3. TRIGGER (Intercepts Right-Click)\n// ==========================================\nexport interface ContextMenuTriggerProps extends React.HTMLAttributes<HTMLDivElement> {\n  children: React.ReactNode;\n}\n\nexport function ContextMenuTrigger({ className, children, ...props }: ContextMenuTriggerProps) {\n  const context = useContext(ContextMenuContext);\n  if (!context) throw new Error(\"ContextMenuTrigger must be used within a ContextMenu\");\n\n  const { setOpen, setCoords, color } = context;\n  const theme = getContextMenuTheme(color);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  const openMenuAt = (x: number, y: number) => {\n    setCoords({ x, y });\n    setOpen(true);\n  };\n\n  const handleContextMenu = (e: React.MouseEvent<HTMLDivElement>) => {\n    e.preventDefault(); // Stop the native browser menu\n    e.stopPropagation();\n    openMenuAt(e.clientX, e.clientY);\n  };\n\n  // Long press for mobile touch screens\n  const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {\n    if (e.touches.length === 1) {\n      const touch = e.touches[0];\n      const x = touch.clientX;\n      const y = touch.clientY;\n      timerRef.current = setTimeout(() => {\n        openMenuAt(x, y);\n      }, 500); // 500ms long press\n    }\n  };\n\n  const handleTouchEnd = () => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n  };\n\n  const handleTouchMove = () => {\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n      timerRef.current = null;\n    }\n  };\n\n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    // Also trigger on left click if desired for user convenience\n    openMenuAt(e.clientX, e.clientY);\n  };\n\n  return (\n    <div\n      onContextMenu={handleContextMenu}\n      onClick={handleClick}\n      onTouchStart={handleTouchStart}\n      onTouchEnd={handleTouchEnd}\n      onTouchMove={handleTouchMove}\n      style={{ ...theme.customVars, ...props.style }}\n      className={cn(\"w-full h-full select-none cursor-pointer transition-colors\", theme.triggerHover, className)}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\n// ==========================================\n// 4. CONTENT (Portal Renderer with Collision)\n// ==========================================\nexport type ContextMenuVariant = \"default\" | \"minimal\" | \"glass\";\n\nexport interface ContextMenuContentProps extends React.HTMLAttributes<HTMLDivElement> {\n  variant?: ContextMenuVariant;\n}\n\nexport function ContextMenuContent({\n  variant = \"default\",\n  className,\n  children,\n  ...props\n}: ContextMenuContentProps) {\n  const context = useContext(ContextMenuContext);\n  if (!context) throw new Error(\"ContextMenuContent must be used within a ContextMenu\");\n\n  const { open, setOpen, coords, color } = context;\n  const theme = getContextMenuTheme(color);\n  const contentRef = useRef<HTMLDivElement>(null);\n  const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null);\n  const [adjustedCoords, setAdjustedCoords] = useState({ x: -9999, y: -9999 });\n  const [visible, setVisible] = useState(false);\n\n  // Resolves the document this menu actually lives in, which is not always the\n  // global one — previews portal components into an iframe. Resolving in a\n  // callback ref rather than reading a ref during render keeps render pure, and\n  // a null target doubles as the \"not mounted yet\" signal that guards SSR.\n  const setAnchor = useCallback((node: HTMLSpanElement | null) => {\n    setPortalTarget(node ? node.ownerDocument.body : null);\n  }, []);\n\n  // Handle Position & Screen Boundaries\n  useEffect(() => {\n    if (!open || !contentRef.current) {\n      setVisible(false);\n      return;\n    }\n\n    const adjustPosition = () => {\n      const menu = contentRef.current;\n      if (!menu) return;\n\n      // Resolve the window & viewport width the menu lives in (supports iframe rendering).\n      const win = menu.ownerDocument.defaultView ?? window;\n      const viewportWidth = win.document.documentElement.clientWidth || win.innerWidth;\n      const viewportHeight = win.document.documentElement.clientHeight || win.innerHeight;\n\n      const rect = menu.getBoundingClientRect();\n      let x = coords.x;\n      let y = coords.y;\n\n      // On small screens, if clicking near the right half or if menu extends near right edge, shift left\n      if (x + rect.width > viewportWidth - 16 || (viewportWidth < 640 && x > viewportWidth / 2)) {\n        x = Math.max(12, x - rect.width);\n        if (x + rect.width > viewportWidth - 12) {\n          x = Math.max(12, viewportWidth - rect.width - 16);\n        }\n      }\n      // Prevent opening off the left edge\n      if (x < 12) {\n        x = 12;\n      }\n\n      // Prevent opening off the bottom edge\n      if (y + rect.height > viewportHeight - 16) {\n        y = Math.max(12, viewportHeight - rect.height - 16);\n      }\n      // Prevent opening off the top edge\n      if (y < 12) {\n        y = 12;\n      }\n\n      setAdjustedCoords({ x, y });\n      setVisible(true); // Fade in after coords are fixed to prevent flicker\n    };\n\n    // Minor delay to let DOM render and measure dimensions\n    const timer = setTimeout(adjustPosition, 0);\n    return () => clearTimeout(timer);\n  }, [open, coords]);\n\n  // Handle Outside Click, Scroll, & Escape\n  useEffect(() => {\n    if (!open) return;\n\n    const handleInteractOutside = (e: MouseEvent | TouchEvent) => {\n      if (contentRef.current && !contentRef.current.contains(e.target as Node)) {\n        setOpen(false);\n      }\n    };\n\n    const handleEscape = (e: KeyboardEvent) => {\n      if (e.key === \"Escape\") setOpen(false);\n    };\n\n    const handleScroll = () => {\n      setOpen(false); // Typically scrolling dismisses a context menu\n    };\n\n    // Listen on the document/window the menu actually lives in (supports iframe rendering).\n    const doc = contentRef.current?.ownerDocument ?? document;\n    const win = doc.defaultView ?? window;\n\n    doc.addEventListener(\"mousedown\", handleInteractOutside);\n    doc.addEventListener(\"touchstart\", handleInteractOutside);\n    doc.addEventListener(\"keydown\", handleEscape);\n    win.addEventListener(\"scroll\", handleScroll, true);\n\n    return () => {\n      doc.removeEventListener(\"mousedown\", handleInteractOutside);\n      doc.removeEventListener(\"touchstart\", handleInteractOutside);\n      doc.removeEventListener(\"keydown\", handleEscape);\n      win.removeEventListener(\"scroll\", handleScroll, true);\n    };\n  }, [open, setOpen]);\n\n  const variantClasses: Record<ContextMenuVariant, string> = {\n    default: \"border-border bg-popover text-popover-foreground shadow-md\",\n    minimal: \"border-border/50 bg-popover/95 text-popover-foreground shadow-sm backdrop-blur-xs\",\n    glass: \"border-foreground/10 bg-popover/80 text-popover-foreground backdrop-blur-xl shadow-xl\",\n  };\n\n  if (!portalTarget || !open) {\n    return <span ref={setAnchor} className=\"hidden\" style={{ display: \"none\" }} aria-hidden=\"true\" />;\n  }\n\n  return (\n    <>\n      <span ref={setAnchor} className=\"hidden\" style={{ display: \"none\" }} aria-hidden=\"true\" />\n      {createPortal(\n        <div\n          ref={contentRef}\n          role=\"menu\"\n          style={{\n            top: adjustedCoords.y,\n            left: adjustedCoords.x,\n            opacity: visible ? 1 : 0,\n            ...theme.customVars,\n            ...props.style,\n          }}\n          className={cn(\n            \"fixed z-[9999] w-48 sm:w-64 max-w-[calc(100vw-24px)] max-h-[calc(100vh-24px)] overflow-y-auto rounded-md border p-1 transition-opacity duration-150 text-xs sm:text-sm\",\n            variantClasses[variant],\n            className\n          )}\n          onClick={(e) => e.stopPropagation()}\n          onContextMenu={(e) => e.preventDefault()} // Prevent nesting native menus\n          {...props}\n        >\n          {children}\n        </div>,\n        portalTarget\n      )}\n    </>\n  );\n}\n\n// ==========================================\n// 5. SUB-COMPONENTS\n// ==========================================\nexport interface ContextMenuItemProps extends React.HTMLAttributes<HTMLDivElement> {\n  inset?: boolean;\n  disabled?: boolean;\n}\n\nexport function ContextMenuItem({ className, inset, disabled, onClick, children, ...props }: ContextMenuItemProps) {\n  const context = useContext(ContextMenuContext);\n  const theme = getContextMenuTheme(context?.color);\n  \n  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (disabled) return;\n    onClick?.(e);\n    context?.setOpen(false); // Close menu after clicking an item\n  };\n\n  return (\n    <div\n      role=\"menuitem\"\n      onClick={handleClick}\n      data-disabled={disabled ? \"true\" : undefined}\n      className={cn(\n        \"group relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors\",\n        inset && \"pl-8\",\n        disabled ? \"opacity-50 pointer-events-none cursor-not-allowed\" : theme.itemHover,\n        className\n      )}\n      {...props}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function ContextMenuSeparator({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {\n  return <div className={cn(\"-mx-1 my-1 h-px bg-border/60 dark:bg-border\", className)} {...props} />;\n}\n\nexport function ContextMenuShortcut({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) {\n  return <span className={cn(\"ml-auto text-xs tracking-widest text-muted-foreground group-hover:text-primary/80 transition-colors\", className)} {...props} />;\n}\n\nexport function ContextMenuLabel({ className, inset, ...props }: React.HTMLAttributes<HTMLDivElement> & { inset?: boolean }) {\n  return <div className={cn(\"px-2 py-1.5 text-xs font-semibold text-muted-foreground\", inset && \"pl-8\", className)} {...props} />;\n}",
      "type": "registry:component",
      "target": "components/custom/blocks/ContextMenuBlock.tsx"
    }
  ],
  "docs": "Named exports: ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuShortcut. The trigger suppresses the native browser menu inside its bounds. Positioned from pointer coordinates, so it opens where the user clicked rather than anchored to an element.",
  "categories": [
    "overlays",
    "actions"
  ],
  "type": "registry:component"
}
