{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "context-menu",
  "title": "Context menu",
  "description": "The same list of words, opened where you pressed and spreading from that point like ink.",
  "dependencies": [
    "@radix-ui/react-context-menu"
  ],
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/dropdown-menu.json",
    "https://thedirectors.agency/ui/r/popover.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/context-menu.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport * as MenuPrimitive from \"@radix-ui/react-context-menu\"\n\nimport { useComposedRefs } from \"@/lib/0nlytype/refs\"\nimport { cn } from \"@/lib/utils\"\nimport { DropdownMenuShortcut, MenuLook, useSubmenuPlace } from \"@/components/ui/dropdown-menu\"\n\n// Radix reads direction from its own provider, not the page. Without a dir, the menu takes the\n// direction of the page around its trigger, read as the trigger mounts (as the dropdown menu\n// does). The trigger also keeps where you pressed, for the content to place itself from.\ntype Point = { x: number; y: number }\ntype Around = { dir?: \"ltr\" | \"rtl\"; setAround: (dir: \"rtl\") => void; atRef: React.RefObject<Point> }\nconst Around = React.createContext<Around>({ setAround: () => {}, atRef: { current: { x: 0, y: 0 } } })\n\nfunction ContextMenu({ dir, ...props }: React.ComponentProps<typeof MenuPrimitive.Root>) {\n  const [around, setAround] = React.useState<\"rtl\">()\n  const atRef = React.useRef<Point>({ x: 0, y: 0 })\n  const value = React.useMemo(() => ({ dir: dir ?? around, setAround, atRef }), [dir, around])\n  return (\n    <Around.Provider value={value}>\n      <MenuPrimitive.Root data-slot=\"context-menu\" dir={dir ?? around} {...props} />\n    </Around.Provider>\n  )\n}\n\nfunction ContextMenuTrigger({ ref, onContextMenu, onPointerDown, ...props }: React.ComponentProps<typeof MenuPrimitive.Trigger>) {\n  const { setAround, atRef } = React.useContext(Around)\n  const composedRef = useComposedRefs(React.useCallback((node: HTMLSpanElement | null) => {\n    if (node && getComputedStyle(node).direction === \"rtl\") setAround(\"rtl\")\n  }, [setAround]), ref)\n  return (\n    <MenuPrimitive.Trigger\n      ref={composedRef}\n      data-slot=\"context-menu-trigger\"\n      onContextMenu={(event) => {\n        atRef.current = { x: event.clientX, y: event.clientY }\n        onContextMenu?.(event)\n      }}\n      onPointerDown={(event) => {\n        atRef.current = { x: event.clientX, y: event.clientY } // a long press opens from here\n        onPointerDown?.(event)\n      }}\n      {...props}\n    />\n  )\n}\n\nfunction ContextMenuGroup(props: React.ComponentProps<typeof MenuPrimitive.Group>) {\n  return <MenuPrimitive.Group data-slot=\"context-menu-group\" {...props} />\n}\n\nfunction ContextMenuRadioGroup(props: React.ComponentProps<typeof MenuPrimitive.RadioGroup>) {\n  return <MenuPrimitive.RadioGroup data-slot=\"context-menu-radio-group\" {...props} />\n}\n\nfunction ContextMenuSub(props: React.ComponentProps<typeof MenuPrimitive.Sub>) {\n  return <MenuPrimitive.Sub data-slot=\"context-menu-sub\" {...props} />\n}\n\n/** Where a menu opens from the point along one axis: forward (toward the line's end, or\n * down) when it fits, else back from the point, else slid in to fit. Returns its start edge. */\nfunction reach(at: number, size: number, room: number, pad: number, forward: boolean) {\n  const fore = at + 2, back = at - 2 - size\n  const fitsFore = fore + size <= room - pad, fitsBack = back >= pad\n  if (forward ? fitsFore : fitsBack) return forward ? fore : back\n  if (forward ? fitsBack : fitsFore) return forward ? back : fore\n  return Math.max(pad, Math.min(fore, room - pad - size))\n}\n\n/**\n * The same list of words, opened where you pressed (right-click, the ContextMenu key or\n * Shift+F10) and spreading from that point like ink. It has no leader line: nothing\n * hangs it from a trigger. It opens toward the line's end and down from the point, as the\n * system's own menus do: to the left on a right-to-left page. Without room it opens back\n * from the point, and on a phone, with room neither way, it slides in to fit, over the\n * point. Radix only opens it to the right and never slides it across, so it is placed here:\n * Radix sets it at the point (no collisions) and it is carried to its place. A cross, the\n * register mark of a Swiss sheet, pins the point the ink spread from.\n *\n * `variant`: `list` (the highlighter), `leaders` (the dropdown menu's contents page) or\n * `orbit`, after WOVE: the words stand on an arc bowed away from the point, a dot on the arc\n * for each, and the one you point at inks with the accent dot.\n */\nfunction ContextMenuContent({\n  className,\n  collisionPadding = 20,\n  variant = \"list\",\n  ref,\n  style,\n  children,\n  ...props\n}: React.ComponentProps<typeof MenuPrimitive.Content> & { variant?: \"list\" | \"leaders\" | \"orbit\" }) {\n  const { dir, atRef } = React.useContext(Around)\n  const [shift, setShift] = React.useState<Point>({ x: 0, y: 0 })\n  const pad = typeof collisionPadding === \"number\" ? collisionPadding : Math.max(...Object.values(collisionPadding), 0)\n  const composedRef = useComposedRefs(React.useCallback((node: HTMLDivElement | null) => {\n    if (node) {\n      const { x, y } = atRef.current, html = document.documentElement\n      const next = { x: reach(x, node.offsetWidth, html.clientWidth, pad, dir !== \"rtl\") - (x + 2), y: reach(y - 2, node.offsetHeight, html.clientHeight, pad, true) - y }\n      setShift((was) => (was.x === next.x && was.y === next.y ? was : next))\n    }\n  }, [atRef, dir, pad]), ref)\n  return (\n    <MenuPrimitive.Portal>\n      <MenuPrimitive.Content\n        ref={composedRef}\n        data-slot=\"context-menu-content\"\n        data-at=\"point\"\n        data-variant={variant}\n        avoidCollisions={false}\n        collisionPadding={collisionPadding}\n        style={{ translate: `${shift.x}px ${shift.y}px`, \"--ot-at-x\": `${-shift.x}px`, \"--ot-at-y\": `${-shift.y}px`, ...style } as React.CSSProperties}\n        className={cn(\"ot-pop ot-menu\", className)}\n        {...props}\n      >\n        <MenuLook.Provider value={variant}>{children}</MenuLook.Provider>\n      </MenuPrimitive.Content>\n    </MenuPrimitive.Portal>\n  )\n}\n\n/** A destructive item says so in its words (\"Delete Halden for good\"); it is never red. */\nfunction ContextMenuItem({\n  className,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof MenuPrimitive.Item> & { variant?: \"default\" | \"destructive\" }) {\n  return <MenuPrimitive.Item data-slot=\"context-menu-item\" data-variant={variant} className={cn(\"ot-menu-item\", className)} {...props} />\n}\n\n/** Checked, the sentence turns to the expression italic. Screen readers get aria-checked. */\nfunction ContextMenuCheckboxItem({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.CheckboxItem>) {\n  return <MenuPrimitive.CheckboxItem data-slot=\"context-menu-checkbox-item\" className={cn(\"ot-menu-item\", className)} {...props} />\n}\n\nfunction ContextMenuRadioItem({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.RadioItem>) {\n  return <MenuPrimitive.RadioItem data-slot=\"context-menu-radio-item\" className={cn(\"ot-menu-item\", className)} {...props} />\n}\n\nfunction ContextMenuLabel({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Label>) {\n  return <MenuPrimitive.Label data-slot=\"context-menu-label\" className={cn(\"ot-menu-label\", className)} {...props} />\n}\n\nfunction ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.Separator>) {\n  return <MenuPrimitive.Separator data-slot=\"context-menu-separator\" className={cn(\"ot-menu-sep\", className)} {...props} />\n}\n\n/** Shortcut keys, each a base `ot-kbd` cap; it is the dropdown menu's, under this slot. */\nfunction ContextMenuShortcut(props: React.ComponentProps<typeof DropdownMenuShortcut>) {\n  return <DropdownMenuShortcut data-slot=\"context-menu-shortcut\" {...props} />\n}\n\n/** Opens something, so it carries the one → glyph. */\nfunction ContextMenuSubTrigger({ className, ...props }: React.ComponentProps<typeof MenuPrimitive.SubTrigger>) {\n  return <MenuPrimitive.SubTrigger data-slot=\"context-menu-sub-trigger\" className={cn(\"ot-menu-item ot-menu-sub\", className)} {...props} />\n}\n\n/** Beside its item, or on a narrow screen dropped open under it (the dropdown menu's useSubmenuPlace). */\nfunction ContextMenuSubContent({ className, ref, sideOffset, alignOffset, collisionPadding, style, ...props }: React.ComponentProps<typeof MenuPrimitive.SubContent>) {\n  const place = useSubmenuPlace({ ref, sideOffset, alignOffset, collisionPadding, style })\n  return (\n    <MenuPrimitive.Portal>\n      <MenuPrimitive.SubContent data-slot=\"context-menu-sub-content\" className={cn(\"ot-pop ot-menu\", className)} {...props} {...place} />\n    </MenuPrimitive.Portal>\n  )\n}\n\nexport {\n  ContextMenu,\n  ContextMenuTrigger,\n  ContextMenuContent,\n  ContextMenuGroup,\n  ContextMenuLabel,\n  ContextMenuItem,\n  ContextMenuCheckboxItem,\n  ContextMenuRadioGroup,\n  ContextMenuRadioItem,\n  ContextMenuSeparator,\n  ContextMenuShortcut,\n  ContextMenuSub,\n  ContextMenuSubTrigger,\n  ContextMenuSubContent,\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/context-menu.css",
      "content": "@layer components {\n  /* ── ot-menu[data-at=\"point\"] ── a context menu opens where you pressed and spreads from\n     that point like ink. Nothing hangs it from a trigger, so no leader; a cross, the register\n     mark of \"the uncreative\", pins the point instead, over the menu's corner. The list itself\n     (ot-menu) is dropdown-menu.css. context-menu.tsx places it from the point and sets\n     --ot-at-x and --ot-at-y, the point in the menu's own box. */\n  .ot-pop.ot-menu[data-at=\"point\"]::after { display: none; }\n  .ot-pop.ot-menu[data-at=\"point\"]::before {\n    content: \"\"; position: absolute; pointer-events: none; left: calc(var(--ot-at-x, 0px) - 6px); top: calc(var(--ot-at-y, 0px) - 6px); width: 13px; height: 13px;\n    background: linear-gradient(var(--ot-ink), var(--ot-ink)) 50% 0 / var(--ot-hairline) 100% no-repeat, linear-gradient(var(--ot-ink), var(--ot-ink)) 0 50% / 100% var(--ot-hairline) no-repeat;\n    animation: none;\n  }\n  .ot-pop.ot-menu[data-at=\"point\"][data-state=\"open\"] { animation: ot-spread var(--ot-andante) var(--ot-exhale); }\n  @keyframes ot-spread { from { clip-path: circle(0 at var(--ot-at-x, 0px) var(--ot-at-y, 0px)); } to { clip-path: circle(150% at var(--ot-at-x, 0px) var(--ot-at-y, 0px)); } }\n\n  /* ── orbit ── after WOVE: the words stand on an arc bowed away from the point, one dot on\n     the arc for each, and the one you point at inks, its dot landing as the accent. The arc is\n     the frame, so the panel's hairline stands down. Every row is one step high, so the arc is\n     drawn by the rows themselves: each one's offset and its piece of the ring come from its\n     place in the list (sibling-index). Put items and separators straight in the list; a group\n     counts as one row. The paper ends at the arc too: each row lays it only outside the ring,\n     so the arc is the panel's edge. The words swing out to the arc as they arrive. */\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] {\n    --ot-orbit-row: calc(var(--ot-p) * 2.3); --ot-orbit-dot: var(--ot-space-3);\n    box-shadow: none; background: none; padding-block: 0; padding-inline-end: calc(var(--ot-space-5) + var(--ot-orbit-row));\n  }\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > * {\n    --ot-orbit-r: calc(sibling-count() * 0.7);\n    --ot-orbit-t: calc(sibling-index() - (sibling-count() + 1) / 2);\n    --ot-orbit-s: sqrt(var(--ot-orbit-r) * var(--ot-orbit-r) - var(--ot-orbit-t) * var(--ot-orbit-t));\n    --ot-orbit-e: sqrt(var(--ot-orbit-r) * var(--ot-orbit-r) - (sibling-count() - 1) * (sibling-count() - 1) / 4);\n    --ot-orbit-ring: calc(var(--ot-orbit-r) * var(--ot-orbit-row));\n    position: relative; box-sizing: border-box; display: flex; align-items: center; block-size: var(--ot-orbit-row); margin: 0; padding-block: 0; white-space: nowrap;\n    translate: calc(var(--ot-orbit-row) * (var(--ot-orbit-s) - var(--ot-orbit-e))) 0;\n    color: var(--ot-graphite);\n    background: radial-gradient(circle var(--ot-orbit-ring) at calc(var(--ot-orbit-dot) - var(--ot-orbit-row) * var(--ot-orbit-s)) calc((0.5 - var(--ot-orbit-t)) * var(--ot-orbit-row)),\n      transparent calc(var(--ot-orbit-ring) - 0.6px), var(--ot-rule-strong) calc(var(--ot-orbit-ring) - 0.5px), var(--ot-rule-strong) calc(var(--ot-orbit-ring) + 0.5px), var(--ot-paper) calc(var(--ot-orbit-ring) + 0.6px));\n    transition: color var(--ot-allegro);\n  }\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) > * {\n    translate: calc(var(--ot-orbit-row) * (var(--ot-orbit-e) - var(--ot-orbit-s))) 0;\n    background-image: radial-gradient(circle var(--ot-orbit-ring) at calc(100% - var(--ot-orbit-dot) + var(--ot-orbit-row) * var(--ot-orbit-s)) calc((0.5 - var(--ot-orbit-t)) * var(--ot-orbit-row)),\n      transparent calc(var(--ot-orbit-ring) - 0.6px), var(--ot-rule-strong) calc(var(--ot-orbit-ring) - 0.5px), var(--ot-rule-strong) calc(var(--ot-orbit-ring) + 0.5px), var(--ot-paper) calc(var(--ot-orbit-ring) + 0.6px));\n  }\n  /* A dot on the arc for each item; the one you point at lands as the accent. */\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item::before {\n    content: \"\"; position: absolute; inset-inline-start: calc(var(--ot-orbit-dot) - var(--ot-dot) / 2); top: calc(50% - var(--ot-dot) / 2); width: var(--ot-dot); height: var(--ot-dot); border-radius: 50%;\n    background: var(--ot-rule-strong); scale: 0.43; transition: scale var(--ot-moderato) var(--ot-spiccato), background-color 0s;\n  }\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item:is([data-highlighted], [data-force~=\"hover\"], .ot-menu-sub[data-state=\"open\"]) { color: var(--ot-ink); }\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item:is([data-highlighted], [data-force~=\"hover\"], .ot-menu-sub[data-state=\"open\"])::before { background: var(--ot-accent); scale: 1; }\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item[data-disabled]::before { opacity: 0.55; }\n  /* A separator is a step of silence on the arc, with a short rule in it. */\n  .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-sep::after { content: \"\"; position: absolute; inset-inline-start: var(--ot-space-5); top: 50%; width: 2em; height: var(--ot-hairline); background: var(--ot-rule); }\n\n  @media (forced-colors: active) {\n    .ot-pop.ot-menu[data-at=\"point\"]::before { forced-color-adjust: none; background: linear-gradient(CanvasText, CanvasText) 50% 0 / 1px 100% no-repeat, linear-gradient(CanvasText, CanvasText) 0 50% / 100% 1px no-repeat; }\n    .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] { border: 1px solid CanvasText; background: Canvas; } /* the arc and its paper are gradients, which forced colours drops */\n    .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item::before { forced-color-adjust: none; background: GrayText; }\n    .ot-pop.ot-menu[data-at=\"point\"][data-variant=\"orbit\"] > .ot-menu-item:is([data-highlighted], [data-force~=\"hover\"])::before { background: Highlight; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/context-menu.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/context-menu.css\"": {}
  },
  "meta": {
    "movement": "IX",
    "underneath": "radix",
    "contract": "ot-menu"
  },
  "type": "registry:ui"
}
