{"$schema":"https:\/\/ui.particle.academy\/schema\/registry-item.json","name":"context-menu","type":"registry:ui","title":"ContextMenu","description":"ContextMenu from react-fancy","package":"react-fancy","dependencies":[],"registryDependencies":["portal"],"files":[{"path":"components\/fancy\/context-menu\/ContextMenu.context.ts","content":"import { createContext, useContext } from \"react\";\r\nimport type { ContextMenuContextValue } from \".\/ContextMenu.types\";\r\n\r\nexport const ContextMenuContext =\r\n  createContext<ContextMenuContextValue | null>(null);\r\n\r\nexport function useContextMenu(): ContextMenuContextValue {\r\n  const ctx = useContext(ContextMenuContext);\r\n  if (!ctx) {\r\n    throw new Error(\r\n      \"ContextMenu compound components must be used within <ContextMenu>\",\r\n    );\r\n  }\r\n  return ctx;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenu.context.ts"},{"path":"components\/fancy\/context-menu\/ContextMenu.tsx","content":"import { useMemo, useRef, useState } from \"react\";\r\nimport { ContextMenuContext } from \".\/ContextMenu.context\";\r\nimport { ContextMenuTrigger } from \".\/ContextMenuTrigger\";\r\nimport { ContextMenuContent } from \".\/ContextMenuContent\";\r\nimport { ContextMenuItem } from \".\/ContextMenuItem\";\r\nimport { ContextMenuSeparator } from \".\/ContextMenuSeparator\";\r\nimport { ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent } from \".\/ContextMenuSub\";\r\nimport type { ContextMenuProps } from \".\/ContextMenu.types\";\r\n\r\nfunction ContextMenuRoot({ children }: ContextMenuProps) {\r\n  const [open, setOpen] = useState(false);\r\n  const positionRef = useRef({ x: 0, y: 0 });\r\n\r\n  const ctx = useMemo(\r\n    () => ({ open, setOpen, position: positionRef.current }),\r\n    [open],\r\n  );\r\n\r\n  return (\r\n    <ContextMenuContext.Provider value={ctx}>\r\n      {children}\r\n    <\/ContextMenuContext.Provider>\r\n  );\r\n}\r\n\r\nexport const ContextMenu = Object.assign(ContextMenuRoot, {\r\n  Trigger: ContextMenuTrigger,\r\n  Content: ContextMenuContent,\r\n  Item: ContextMenuItem,\r\n  Separator: ContextMenuSeparator,\r\n  Sub: ContextMenuSub,\r\n  SubTrigger: ContextMenuSubTrigger,\r\n  SubContent: ContextMenuSubContent,\r\n});\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenu.tsx"},{"path":"components\/fancy\/context-menu\/ContextMenu.types.ts","content":"import type { ReactNode } from \"react\";\r\n\r\nexport interface ContextMenuContextValue {\r\n  open: boolean;\r\n  setOpen: (open: boolean) => void;\r\n  position: { x: number; y: number };\r\n}\r\n\r\nexport interface ContextMenuProps {\r\n  children: ReactNode;\r\n}\r\n\r\nexport interface ContextMenuTriggerProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface ContextMenuContentProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface ContextMenuItemProps {\r\n  children: ReactNode;\r\n  onClick?: () => void;\r\n  disabled?: boolean;\r\n  danger?: boolean;\r\n  className?: string;\r\n}\r\n\r\nexport interface ContextMenuSeparatorProps {\r\n  className?: string;\r\n}\r\n\r\nexport interface ContextMenuSubProps {\r\n  children: ReactNode;\r\n}\r\n\r\nexport interface ContextMenuSubTriggerProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n\r\nexport interface ContextMenuSubContentProps {\r\n  children: ReactNode;\r\n  className?: string;\r\n}\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenu.types.ts"},{"path":"components\/fancy\/context-menu\/ContextMenuContent.tsx","content":"import { useCallback, useEffect, useRef, useState } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { Portal } from \"..\/Portal\";\r\nimport { useContextMenu } from \".\/ContextMenu.context\";\r\nimport { useOutsideClick } from \"..\/..\/hooks\/use-outside-click\";\r\nimport { useEscapeKey } from \"..\/..\/hooks\/use-escape-key\";\r\nimport { useAnimation } from \"..\/..\/hooks\/use-animation\";\r\nimport { MenuGroupProvider } from \".\/ContextMenuSub\";\r\nimport type { ContextMenuContentProps } from \".\/ContextMenu.types\";\r\n\r\nexport function ContextMenuContent({\r\n  children,\r\n  className,\r\n}: ContextMenuContentProps) {\r\n  const { open, setOpen, position } = useContextMenu();\r\n  const menuRef = useRef<HTMLDivElement>(null);\r\n  const [adjusted, setAdjusted] = useState<{ x: number; y: number }>(position);\r\n\r\n  const close = useCallback(() => setOpen(false), [setOpen]);\r\n  useOutsideClick(menuRef, close, open);\r\n  useEscapeKey(close, open);\r\n\r\n  \/\/ Clamp position to viewport after mount\r\n  useEffect(() => {\r\n    const node = menuRef.current;\r\n    if (!open || !node) {\r\n      setAdjusted(position);\r\n      return;\r\n    }\r\n    requestAnimationFrame(() => {\r\n      const rect = node.getBoundingClientRect();\r\n      const pad = 8;\r\n      let x = position.x;\r\n      let y = position.y;\r\n      if (x + rect.width > window.innerWidth - pad) x = window.innerWidth - rect.width - pad;\r\n      if (y + rect.height > window.innerHeight - pad) y = window.innerHeight - rect.height - pad;\r\n      if (x < pad) x = pad;\r\n      if (y < pad) y = pad;\r\n      setAdjusted({ x, y });\r\n    });\r\n  }, [open, position]);\r\n\r\n  const { mounted, className: animClass, ref: animRef } = useAnimation({\r\n    open,\r\n    enterClass: \"fancy-scale-in\",\r\n    exitClass: \"fancy-fade-out\",\r\n  });\r\n\r\n  if (!mounted) return null;\r\n\r\n  return (\r\n    <Portal>\r\n      <div\r\n        ref={(node) => {\r\n          menuRef.current = node;\r\n          (animRef as React.MutableRefObject<HTMLElement | null>).current = node;\r\n        }}\r\n        data-react-fancy-context-menu=\"\"\r\n        role=\"menu\"\r\n        className={cn(\r\n          \"fixed z-50 min-w-[8rem] rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900\",\r\n          animClass,\r\n          className,\r\n        )}\r\n        style={{ left: adjusted.x, top: adjusted.y }}\r\n      >\r\n        <MenuGroupProvider>\r\n          {children}\r\n        <\/MenuGroupProvider>\r\n      <\/div>\r\n    <\/Portal>\r\n  );\r\n}\r\n\r\nContextMenuContent.displayName = \"ContextMenuContent\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenuContent.tsx"},{"path":"components\/fancy\/context-menu\/ContextMenuItem.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport { useContextMenu } from \".\/ContextMenu.context\";\r\nimport type { ContextMenuItemProps } from \".\/ContextMenu.types\";\r\n\r\nexport function ContextMenuItem({\r\n  children,\r\n  onClick,\r\n  disabled = false,\r\n  danger = false,\r\n  className,\r\n}: ContextMenuItemProps) {\r\n  const { setOpen } = useContextMenu();\r\n\r\n  return (\r\n    <button\r\n      type=\"button\"\r\n      data-react-fancy-context-menu-item=\"\"\r\n      role=\"menuitem\"\r\n      disabled={disabled}\r\n      aria-disabled={disabled}\r\n      className={cn(\r\n        \"flex w-full items-center rounded-lg px-3 py-2 text-left text-sm transition-colors\",\r\n        danger\r\n          ? \"text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950\"\r\n          : \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\",\r\n        disabled && \"cursor-not-allowed opacity-50\",\r\n        className,\r\n      )}\r\n      onClick={() => {\r\n        if (disabled) return;\r\n        onClick?.();\r\n        setOpen(false);\r\n      }}\r\n    >\r\n      {children}\r\n    <\/button>\r\n  );\r\n}\r\n\r\nContextMenuItem.displayName = \"ContextMenuItem\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenuItem.tsx"},{"path":"components\/fancy\/context-menu\/ContextMenuSeparator.tsx","content":"import { cn } from \"..\/..\/utils\/cn\";\r\nimport type { ContextMenuSeparatorProps } from \".\/ContextMenu.types\";\r\n\r\nexport function ContextMenuSeparator({ className }: ContextMenuSeparatorProps) {\r\n  return (\r\n    <div\r\n      data-react-fancy-context-menu-separator=\"\"\r\n      role=\"separator\"\r\n      className={cn(\"my-1 h-px bg-zinc-200 dark:bg-zinc-700\", className)}\r\n    \/>\r\n  );\r\n}\r\n\r\nContextMenuSeparator.displayName = \"ContextMenuSeparator\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenuSeparator.tsx"},{"path":"components\/fancy\/context-menu\/ContextMenuSub.tsx","content":"import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport type { ContextMenuSubProps, ContextMenuSubTriggerProps, ContextMenuSubContentProps } from \".\/ContextMenu.types\";\r\n\r\n\/\/ ---------------------------------------------------------------------------\r\n\/\/ Sibling coordination: parent tracks which sub is active so opening one\r\n\/\/ immediately closes any previously-open sibling.\r\n\/\/ ---------------------------------------------------------------------------\r\n\r\ninterface MenuGroupContextValue {\r\n  activeSubId: string | null;\r\n  setActiveSub: (id: string | null) => void;\r\n}\r\n\r\nconst MenuGroupContext = createContext<MenuGroupContextValue>({\r\n  activeSubId: null,\r\n  setActiveSub: () => {},\r\n});\r\n\r\n\/** Wrap sibling Subs so only one is open at a time. Placed inside Content\/SubContent. *\/\r\nexport function MenuGroupProvider({ children }: { children: React.ReactNode }) {\r\n  const [activeSubId, setActiveSub] = useState<string | null>(null);\r\n  return (\r\n    <MenuGroupContext.Provider value={{ activeSubId, setActiveSub }}>\r\n      {children}\r\n    <\/MenuGroupContext.Provider>\r\n  );\r\n}\r\n\r\n\/\/ ---------------------------------------------------------------------------\r\n\/\/ Sub components\r\n\/\/ ---------------------------------------------------------------------------\r\n\r\ninterface SubContextValue {\r\n  open: boolean;\r\n  setOpen: (open: boolean) => void;\r\n  cancelClose: () => void;\r\n}\r\n\r\nconst ContextMenuSubContext = createContext<SubContextValue>({\r\n  open: false,\r\n  setOpen: () => {},\r\n  cancelClose: () => {},\r\n});\r\n\r\nexport function ContextMenuSub({ children }: ContextMenuSubProps) {\r\n  const id = useId();\r\n  const { activeSubId, setActiveSub } = useContext(MenuGroupContext);\r\n  const open = activeSubId === id;\r\n  const closeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);\r\n  const activeRef = useRef(activeSubId);\r\n  activeRef.current = activeSubId;\r\n\r\n  const setOpen = useCallback((v: boolean) => {\r\n    if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }\r\n    setActiveSub(v ? id : null);\r\n  }, [id, setActiveSub]);\r\n\r\n  const scheduleClose = useCallback(() => {\r\n    if (closeTimer.current) clearTimeout(closeTimer.current);\r\n    closeTimer.current = setTimeout(() => {\r\n      if (activeRef.current === id) setActiveSub(null);\r\n    }, 150);\r\n  }, [id, setActiveSub]);\r\n\r\n  const cancelClose = useCallback(() => {\r\n    if (closeTimer.current) { clearTimeout(closeTimer.current); closeTimer.current = null; }\r\n  }, []);\r\n\r\n  useEffect(() => () => {\r\n    if (closeTimer.current) clearTimeout(closeTimer.current);\r\n  }, []);\r\n\r\n  return (\r\n    <div\r\n      className=\"relative\"\r\n      onMouseEnter={cancelClose}\r\n      onMouseLeave={scheduleClose}\r\n      data-react-fancy-context-menu-sub=\"\"\r\n    >\r\n      <ContextMenuSubContext.Provider value={{ open, setOpen, cancelClose }}>\r\n        {children}\r\n      <\/ContextMenuSubContext.Provider>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nContextMenuSub.displayName = \"ContextMenuSub\";\r\n\r\nexport function ContextMenuSubTrigger({ children, className }: ContextMenuSubTriggerProps) {\r\n  const { open, setOpen, cancelClose } = useContext(ContextMenuSubContext);\r\n\r\n  return (\r\n    <button\r\n      type=\"button\"\r\n      data-react-fancy-context-menu-sub-trigger=\"\"\r\n      role=\"menuitem\"\r\n      aria-haspopup=\"menu\"\r\n      aria-expanded={open}\r\n      className={cn(\r\n        \"flex w-full items-center justify-between rounded-lg px-3 py-2 text-left text-sm transition-colors\",\r\n        \"text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800\",\r\n        open && \"bg-zinc-100 dark:bg-zinc-800\",\r\n        className,\r\n      )}\r\n      onMouseEnter={() => {\r\n        cancelClose();\r\n        setOpen(true);\r\n      }}\r\n      onClick={() => setOpen(!open)}\r\n    >\r\n      <span>{children}<\/span>\r\n      <svg width=\"12\" height=\"12\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\" className=\"ml-2 opacity-50\">\r\n        <polyline points=\"9 18 15 12 9 6\" \/>\r\n      <\/svg>\r\n    <\/button>\r\n  );\r\n}\r\n\r\nContextMenuSubTrigger.displayName = \"ContextMenuSubTrigger\";\r\n\r\nexport function ContextMenuSubContent({ children, className }: ContextMenuSubContentProps) {\r\n  const { open } = useContext(ContextMenuSubContext);\r\n  const ref = useRef<HTMLDivElement>(null);\r\n  const [flipLeft, setFlipLeft] = useState(false);\r\n  const [topOffset, setTopOffset] = useState(0);\r\n  const [maxH, setMaxH] = useState<number | undefined>(undefined);\r\n\r\n  useEffect(() => {\r\n    if (!open || !ref.current) return;\r\n    const pad = 8;\r\n    const rect = ref.current.getBoundingClientRect();\r\n\r\n    \/\/ Horizontal: flip left if overflowing right\r\n    setFlipLeft(rect.right > window.innerWidth - pad);\r\n\r\n    \/\/ Vertical: shift up if overflowing bottom, cap height if still too tall\r\n    const overflow = rect.bottom - (window.innerHeight - pad);\r\n    if (overflow > 0) {\r\n      const shifted = Math.min(overflow, rect.top - pad);\r\n      setTopOffset(-shifted);\r\n      if (overflow > shifted) {\r\n        setMaxH(window.innerHeight - pad * 2);\r\n      } else {\r\n        setMaxH(undefined);\r\n      }\r\n    } else {\r\n      setTopOffset(0);\r\n      setMaxH(undefined);\r\n    }\r\n  }, [open]);\r\n\r\n  if (!open) return null;\r\n\r\n  return (\r\n    <div\r\n      ref={ref}\r\n      data-react-fancy-context-menu-sub-content=\"\"\r\n      role=\"menu\"\r\n      className={cn(\r\n        \"absolute z-50 min-w-[8rem] rounded-xl border border-zinc-200 bg-white p-1 shadow-lg dark:border-zinc-700 dark:bg-zinc-900\",\r\n        flipLeft ? \"right-full mr-1\" : \"left-full ml-1\",\r\n        maxH && \"overflow-y-auto\",\r\n        className,\r\n      )}\r\n      style={{ top: topOffset, maxHeight: maxH }}\r\n    >\r\n      <MenuGroupProvider>\r\n        {children}\r\n      <\/MenuGroupProvider>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nContextMenuSubContent.displayName = \"ContextMenuSubContent\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenuSub.tsx"},{"path":"components\/fancy\/context-menu\/ContextMenuTrigger.tsx","content":"import { useCallback, type MouseEvent } from \"react\";\r\nimport { cn } from \"..\/..\/utils\/cn\";\r\nimport { useContextMenu } from \".\/ContextMenu.context\";\r\nimport type { ContextMenuTriggerProps } from \".\/ContextMenu.types\";\r\n\r\nexport function ContextMenuTrigger({\r\n  children,\r\n  className,\r\n}: ContextMenuTriggerProps) {\r\n  const ctx = useContextMenu();\r\n\r\n  const handleContextMenu = useCallback(\r\n    (e: MouseEvent) => {\r\n      e.preventDefault();\r\n      (ctx as { position: { x: number; y: number } }).position.x = e.clientX;\r\n      (ctx as { position: { x: number; y: number } }).position.y = e.clientY;\r\n      ctx.setOpen(true);\r\n    },\r\n    [ctx],\r\n  );\r\n\r\n  return (\r\n    <div data-react-fancy-context-menu-trigger=\"\" onContextMenu={handleContextMenu} className={cn(className)}>\r\n      {children}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nContextMenuTrigger.displayName = \"ContextMenuTrigger\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/ContextMenuTrigger.tsx"},{"path":"components\/fancy\/context-menu\/index.ts","content":"export { ContextMenu } from \".\/ContextMenu\";\r\nexport { useContextMenu } from \".\/ContextMenu.context\";\r\nexport type {\r\n  ContextMenuProps,\r\n  ContextMenuTriggerProps,\r\n  ContextMenuContentProps,\r\n  ContextMenuItemProps,\r\n  ContextMenuSeparatorProps,\r\n  ContextMenuSubProps,\r\n  ContextMenuSubTriggerProps,\r\n  ContextMenuSubContentProps,\r\n  ContextMenuContextValue,\r\n} from \".\/ContextMenu.types\";\r\n","type":"registry:ui","target":"components\/fancy\/context-menu\/index.ts"}]}