{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "type": "registry:ui",
  "title": "Context menu",
  "description": "The Menu at the pointer: a right-click, Shift+F10 or a 500ms hold on touch. The same glass, rows, gliding highlight and solid submenus as the Menu, grown from the pointer's corner, and on a phone a submenu that replaces the list in place.",
  "dependencies": [
    "@radix-ui/react-context-menu",
    "motion"
  ],
  "registryDependencies": [
    "https://littmd.vercel.app/r/menu.json",
    "https://littmd.vercel.app/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/litt/ui/context-menu.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n// @litt/context-menu 2026.11.05. What changed: https://littmd.vercel.app/changelog.md\n\n/**\n * Context menu: the Menu at the pointer, on @radix-ui/react-context-menu (roles, focus, typeahead, submenus,\n * dismissal) with Menu's own panel and rows.\n *\n * - Look and rows: the Menu's exactly. It renders MenuPanel from menu.tsx with ContextMenu's Radix parts, so\n *   MenuItem, MenuSub, MenuCheckboxItem, MenuRadioGroup, MenuRadioItem, MenuLabel, MenuGroup and MenuSeparator\n *   work in it unchanged: glass at the surface radius with the float shadow, 44px rows, one gliding highlight,\n *   checks in a fixed 16px cell, solid submenus, focus by modality, and on a phone a submenu that replaces the\n *   list in place with a Back row first.\n * - Open. A right-click opens it at the pointer. Shift+F10 or the menu key opens it on the focused element inside\n *   the target, at that element's bottom-left corner, with the first row lit and ringed, as any keyboard open is.\n *   On touch a press and hold of 500ms (the tooltip's hold: an interaction delay, not a motion token) opens it at\n *   the finger. Lifting early, drifting past a small slop or scrolling cancels the hold, and the click a hold may\n *   leave behind is swallowed, so the target's own action does not also run. The target turns off the browser's\n *   callout, and its text selection on coarse pointers.\n * - Why the target does not take all of Radix's trigger: Radix holds for 700ms, and React bubbles events out of\n *   portals rendered from inside the target (a row's \"⋯\" Menu, a Dialog) into its handlers. So the target takes\n *   only Radix's open handler (and its state and style), and this file decides when to call it: for events whose\n *   element really sits inside the target, and for the hold and the keys by sending a contextmenu event at the\n *   point it should open from. Nested targets work: the innermost one opens.\n * - While it is open the browser's own menu is held back, so a second contextmenu event (the menu key sends one\n *   on keyup on Windows) or a right-click on the panel never stacks a native menu on top of it.\n * - Motion, responsive tier, owned by MenuPanel: it grows from the pointer's corner (Radix's transform origin),\n *   travels a quarter of the travel token from the side it opens on after collision flipping, and leaves on the\n *   exit token. The hooks zero all of it under reduced motion.\n *\n * On a phone (foundations.md section 6) it stays anchored at the finger, like the Menu, and a submenu replaces\n * the list in place. Every action in it must also be reachable another way (a \"⋯\" Menu on the row, a toolbar, a\n * shortcut): a context menu is a shortcut, and a touch or keyboard user may never find it.\n */\nimport {\n  cloneElement,\n  useEffect,\n  useRef,\n  useState,\n  type CSSProperties,\n  type KeyboardEvent,\n  type MouseEvent,\n  type PointerEvent,\n  type ReactElement,\n  type ReactNode,\n} from \"react\";\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\";\nimport { AnimatePresence } from \"motion/react\";\nimport { useDevice } from \"../../lib/litt/motion\";\nimport { MenuPanel, type MenuParts } from \"./menu\";\n\n/* Radix's asChild, spread in: shadcn's CLI rewrites a written asChild to Base UI's render prop in base-* hosts. */\nconst asChildProps = { asChild: true } as const;\n\nconst HOLD_DELAY = 500; // ms of press and hold before it opens on touch: the tooltip's hold (an interaction delay, not animation)\nconst HOLD_SLOP = 10; // px a finger may drift during the hold before it counts as a scroll or a drag\nconst CLICK_WINDOW = 500; // ms after a hold ends in which the click it may leave behind is swallowed\n\nconst CONTEXT_PARTS: MenuParts = {\n  Item: ContextMenuPrimitive.Item,\n  CheckboxItem: ContextMenuPrimitive.CheckboxItem,\n  ItemIndicator: ContextMenuPrimitive.ItemIndicator,\n  RadioGroup: ContextMenuPrimitive.RadioGroup,\n  RadioItem: ContextMenuPrimitive.RadioItem,\n  Label: ContextMenuPrimitive.Label,\n  Group: ContextMenuPrimitive.Group,\n  Separator: ContextMenuPrimitive.Separator,\n  Sub: ContextMenuPrimitive.Sub,\n  SubTrigger: ContextMenuPrimitive.SubTrigger,\n  SubContent: ContextMenuPrimitive.SubContent,\n  Portal: ContextMenuPrimitive.Portal,\n  /* It takes no side, align or sideOffset: it opens to the right of the pointer, and flips. */\n  Content: ContextMenuPrimitive.Content as MenuParts[\"Content\"],\n  origin: \"origin-(--radix-context-menu-content-transform-origin)\",\n  fit: \"max-h-(--radix-context-menu-content-available-height)\",\n};\n\n/** The handlers and looks the target element may already have. Each one still runs, before the menu's own. */\ntype OwnProps = {\n  className?: string;\n  style?: CSSProperties;\n  onContextMenu?: (e: MouseEvent<HTMLElement>) => void;\n  onKeyDown?: (e: KeyboardEvent<HTMLElement>) => void;\n  onPointerDown?: (e: PointerEvent<HTMLElement>) => void;\n  onPointerMove?: (e: PointerEvent<HTMLElement>) => void;\n  onPointerUp?: (e: PointerEvent<HTMLElement>) => void;\n  onPointerCancel?: (e: PointerEvent<HTMLElement>) => void;\n  onClickCapture?: (e: MouseEvent<HTMLElement>) => void;\n};\n\ntype TargetProps = {\n  element: ReactElement;\n  coarse: boolean;\n  /* What Radix's trigger hands its child through asChild. Its touch handlers (the 700ms hold) arrive too, and are dropped. */\n  onContextMenu?: (e: MouseEvent<HTMLElement>) => void;\n  style?: CSSProperties;\n  \"data-state\"?: string;\n};\n\n/** An event from an element that really sits inside the target, not one React bubbled out of a portal. */\nconst inside = (e: { currentTarget: EventTarget; target: EventTarget }) => (e.currentTarget as Node).contains(e.target as Node);\n\n/** Open at a point through the path a right-click takes, so Radix puts its anchor there. */\nconst openAt = (el: Element, x: number, y: number) =>\n  el.dispatchEvent(new window.MouseEvent(\"contextmenu\", { bubbles: true, cancelable: true, clientX: x, clientY: y, button: 2 }));\n\n/** The target element, with the menu's ways in added to its own handlers. */\nfunction Target({ element, coarse, onContextMenu: open, style, \"data-state\": state }: TargetProps) {\n  const own = element.props as OwnProps;\n  /* `pending` is the hold's timer and where it started. `held` is true from the moment a hold opens the menu until\n     that press ends; `swallowUntil` then covers the click the release may cause. */\n  const pending = useRef<{ timer: number; x: number; y: number } | null>(null);\n  const held = useRef(false);\n  const swallowUntil = useRef(0);\n  const cancelHold = () => {\n    if (pending.current) window.clearTimeout(pending.current.timer);\n    pending.current = null;\n  };\n  useEffect(() => cancelHold, []);\n\n  return cloneElement(element as ReactElement<OwnProps & { \"data-state\"?: string }>, {\n    \"data-state\": state,\n    style: { ...style, ...own.style },\n    className: [own.className, \"pointer-coarse:select-none pointer-coarse:[-webkit-touch-callout:none]\"].filter(Boolean).join(\" \"),\n    onContextMenu: (e: MouseEvent<HTMLElement>) => {\n      own.onContextMenu?.(e);\n      /* Handled already: by the element, by a target nested in this one, or held back while a menu is open. */\n      if (e.defaultPrevented || !inside(e)) return;\n      if (e.nativeEvent.isTrusted && pending.current) {\n        /* The browser's own long press got there first: that open stands for the hold. */\n        cancelHold();\n        held.current = true;\n      }\n      open?.(e);\n    },\n    onKeyDown: (e: KeyboardEvent<HTMLElement>) => {\n      own.onKeyDown?.(e);\n      if (e.defaultPrevented || e.repeat || !inside(e) || !open) return;\n      const menuKey = e.key === \"ContextMenu\" || (e.key === \"F10\" && e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey);\n      if (!menuKey) return;\n      /* Ours, so the browser does not open its own menu as well. */\n      e.preventDefault();\n      const el = e.target as Element;\n      const r = el.getBoundingClientRect();\n      openAt(el, r.left, r.bottom);\n    },\n    onPointerDown: (e: PointerEvent<HTMLElement>) => {\n      own.onPointerDown?.(e);\n      cancelHold();\n      held.current = false;\n      swallowUntil.current = 0;\n      if (!inside(e) || !e.isPrimary || e.button !== 0) return;\n      /* A mouse right-clicks. A finger or a pen holds, and so does a mouse while a phone is previewed. */\n      if (e.pointerType === \"mouse\" && !coarse) return;\n      const el = e.target as Element;\n      const x = e.clientX;\n      const y = e.clientY;\n      pending.current = {\n        x,\n        y,\n        timer: window.setTimeout(() => {\n          pending.current = null;\n          held.current = true;\n          openAt(el, x, y);\n        }, HOLD_DELAY),\n      };\n    },\n    onPointerMove: (e: PointerEvent<HTMLElement>) => {\n      own.onPointerMove?.(e);\n      const at = pending.current;\n      if (at && Math.hypot(e.clientX - at.x, e.clientY - at.y) > HOLD_SLOP) cancelHold();\n    },\n    onPointerUp: (e: PointerEvent<HTMLElement>) => {\n      own.onPointerUp?.(e);\n      cancelHold();\n      if (held.current) swallowUntil.current = performance.now() + CLICK_WINDOW;\n      held.current = false;\n    },\n    onPointerCancel: (e: PointerEvent<HTMLElement>) => {\n      own.onPointerCancel?.(e);\n      cancelHold();\n      held.current = false;\n    },\n    onClickCapture: (e: MouseEvent<HTMLElement>) => {\n      own.onClickCapture?.(e);\n      /* Opening the menu is not choosing the target. */\n      if (performance.now() < swallowUntil.current) {\n        swallowUntil.current = 0;\n        e.preventDefault();\n        e.stopPropagation();\n      }\n    },\n  });\n}\n\nexport type ContextMenuProps = {\n  /** The target: one element, such as a list row, a card or a file grid, or a component that passes its props on to one. A right-click on it, a press and hold, or Shift+F10 on something focused inside it opens the menu. Put something focusable inside it, so the keys can reach it. */\n  trigger: ReactElement;\n  /** The menu's accessible name, for example \"Founder landing actions\". A target has no name to lend it, so it is required. */\n  label: string;\n  /** The text of the Back row that leaves a submenu on a phone, where a submenu replaces the list instead of flying out. Translate it with the rest of your labels. */\n  backLabel?: string;\n  /** The rows, the Menu's own: MenuItem, MenuSub, MenuCheckboxItem, MenuRadioGroup, MenuLabel, MenuGroup and MenuSeparator. Put every one of these actions somewhere else too. */\n  children: ReactNode;\n};\n\n/** The Menu at the pointer. It wraps your target and the rows, and owns its open state, so there is no open prop. */\nexport function ContextMenu({ trigger, label, backLabel = \"Back\", children }: ContextMenuProps) {\n  const [open, setOpen] = useState(false);\n  const { coarse } = useDevice();\n\n  /* While it is open, the browser's own menu stays shut: no native menu stacks on top of this one. */\n  useEffect(() => {\n    if (!open) return;\n    const hold = (e: Event) => e.preventDefault();\n    document.addEventListener(\"contextmenu\", hold, true);\n    return () => document.removeEventListener(\"contextmenu\", hold, true);\n  }, [open]);\n\n  return (\n    /* Modal, unlike the Menu: opened by a press and hold, it must outlast the finger lifting, which a non-modal menu\n       would take for a press outside. */\n    <ContextMenuPrimitive.Root open={open} onOpenChange={setOpen}>\n      <ContextMenuPrimitive.Trigger {...asChildProps}>\n        <Target element={trigger} coarse={coarse} />\n      </ContextMenuPrimitive.Trigger>\n      <AnimatePresence>\n        {open && (\n          <ContextMenuPrimitive.Portal forceMount key=\"menu\">\n            <MenuPanel parts={CONTEXT_PARTS} label={label} backLabel={backLabel}>\n              {children}\n            </MenuPanel>\n          </ContextMenuPrimitive.Portal>\n        )}\n      </AnimatePresence>\n    </ContextMenuPrimitive.Root>\n  );\n}\n",
      "target": "components/litt/context-menu.tsx"
    }
  ],
  "docs": "Import ContextMenu from components/litt/context-menu and fill it with the Menu's rows from components/litt/menu. Pass the target element as trigger and a label. Every action in it must also be reachable another way, such as a Menu on the row.",
  "meta": {
    "version": "2026.11.05",
    "released": "2026-11-05",
    "changelog": "https://littmd.vercel.app/changelog.md"
  }
}
