{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"context-menu","type":"registry:ui","title":"ContextMenu","description":"A menu that opens at the pointer on right-click, sharing DropdownMenu's item, submenu and keyboard behaviour but anchored to a virtual point instead of a trigger element.","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["navigation"],"registryDependencies":["https://fancy-ui.rama.app/r/dropdown-menu.json","https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/internals-motion.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/context-menu/ContextMenu.tsx","content":"\"use client\";\n\nimport { useCallback, useMemo, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport type { Side, Align } from \"../../internals/anchor-position\";\nimport { Portal } from \"../../internals/Portal\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { CONTEXT_MENU_KEY } from \"./types\";\nimport type { ContextMenuRootContext } from \"./types\";\nimport type { MenuCloseOptions } from \"../dropdown-menu/types\";\n\nexport interface ContextMenuProps {\n\t/**\n\t * Whether the menu is open.\n\t *\n\t * The source declares this `$bindable(false)`; React has no two-way\n\t * channel, so the component keeps its own copy either way — seeded from\n\t * this prop and re-synced whenever the caller changes it. `onOpenChange`\n\t * fires with the same value at the same moment in every call shape.\n\t */\n\topen?: boolean;\n\t/** Called with the new value whenever the menu opens or closes, however the change happened. */\n\tonOpenChange?: (open: boolean) => void;\n\t/** Side of the pointer to place the menu on. */\n\tside?: Side;\n\t/** Alignment along the pointer's cross axis. */\n\talign?: Align;\n\t/** Gap in pixels between the pointer and the menu. */\n\toffset?: number;\n\t/** Whether arrow-key navigation wraps at the ends. */\n\tloop?: boolean;\n\t/** The `ContextMenuTrigger` and `ContextMenuContent`. */\n\tchildren?: ReactNode;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/**\n * The root of the context-menu compound. Publishes the one context its\n * trigger and content read, and renders the zero-size virtual anchor the\n * panel positions itself against — exactly the two things the source does.\n *\n * No `forwardRef`: the source declares no `ref` binding here, and the Svelte\n * API surface is the contract, per-component.\n */\nexport function ContextMenu({\n\topen: openProp,\n\tonOpenChange,\n\tside = \"bottom\",\n\talign = \"start\",\n\toffset = 2,\n\tloop = true,\n\tchildren,\n\tsound = false,\n}: ContextMenuProps) {\n\t// `useFancyId()`, the counterpart of `$props.id()`. Never `uid()`, which\n\t// throws on the server by design.\n\tconst contentId = useFancyId();\n\n\t// A plain `let` in the source — deliberately NOT reactive state there, and\n\t// therefore a plain ref here. Nothing renders off it: the content reads it\n\t// lazily, from an anchor getter that runs at position time.\n\tconst anchorRef = useRef<HTMLElement | null>(null);\n\n\tconst [point, setPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 });\n\n\t// Captured the moment the menu opens, exactly the reasoning\n\t// `internals/focus-trap.ts`'s own `previouslyFocused` uses for a modal\n\t// surface — except this component isn't modal and can't reuse that\n\t// module, so it borrows the pattern locally: remember what had focus,\n\t// restore it on close if it's still around.\n\tconst previouslyFocused = useRef<HTMLElement | null>(null);\n\n\t// The React shape of the source's `open = $bindable(false)`: an internal\n\t// copy seeded from the prop, re-synced during render whenever the CALLER\n\t// changes the prop, and free to move on its own in between. That is what\n\t// makes all three documented call shapes work off one implementation — a\n\t// caller driving `open` from its own state, a caller who passes only\n\t// `onOpenChange`, and a caller who passes neither and lets the region run\n\t// the whole thing. Re-synced in the render path, not an effect: an effect\n\t// would paint one frame of the stale value first.\n\tconst [open, setOpenState] = useState(openProp ?? false);\n\tconst [lastOpenProp, setLastOpenProp] = useState(openProp);\n\tif (lastOpenProp !== openProp) {\n\t\tsetLastOpenProp(openProp);\n\t\tsetOpenState(openProp ?? false);\n\t}\n\n\tconst openRef = useLiveRef(open);\n\tconst handleOpenChange = useEventCallback(onOpenChange);\n\t// The `sound &&` guard moves into the hook, so `silent` keeps its exact\n\t// meaning below and a cue is decided audible inside `sound.play()` at call\n\t// time rather than at render time.\n\tconst playCue = useSoundCue(sound);\n\n\t// The one place `open` changes, in either direction — a plain function,\n\t// not an effect, so it never reads and writes `open` in the same pass and\n\t// never fights a controlled caller's own write.\n\tconst setOpen = useCallback(\n\t\t(next: boolean, options: MenuCloseOptions = {}): void => {\n\t\t\tif (openRef.current === next) return;\n\t\t\tsetOpenState(next);\n\t\t\thandleOpenChange(next);\n\t\t\t// `silent` is what keeps one activation to exactly one cue: an\n\t\t\t// item's own `select` closes the whole menu, and this `close` would\n\t\t\t// otherwise sound on top of it.\n\t\t\tif (!options.silent) playCue(next ? \"open\" : \"close\");\n\t\t\tif (!next && (options.returnFocus ?? true) && previouslyFocused.current?.isConnected) {\n\t\t\t\tpreviouslyFocused.current.focus();\n\t\t\t}\n\t\t},\n\t\t[openRef, handleOpenChange, playCue]\n\t);\n\n\tconst openAt = useCallback(\n\t\t(x: number, y: number): void => {\n\t\t\t// Opening a second context menu while this one is already open\n\t\t\t// replaces it rather than stacking: there is only ever one\n\t\t\t// `ContextMenuContent` per root, so moving `point` while `open`\n\t\t\t// stays true just repositions the same panel instead of mounting a\n\t\t\t// second one. Only remember the pre-open focus target the first\n\t\t\t// time — re-remembering it on a reposition would capture the menu's\n\t\t\t// own currently-focused item instead of what was focused before it\n\t\t\t// opened at all.\n\t\t\tif (!openRef.current) {\n\t\t\t\tpreviouslyFocused.current = document.activeElement as HTMLElement | null;\n\t\t\t}\n\t\t\tsetPoint({ x, y });\n\t\t\tsetOpen(true);\n\t\t},\n\t\t[openRef, setOpen]\n\t);\n\n\tconst close = useCallback(\n\t\t(options: MenuCloseOptions = {}): void => {\n\t\t\tsetOpen(false, options);\n\t\t},\n\t\t[setOpen]\n\t);\n\n\t// Identity-stable, so the anchor's ref callback never detaches and\n\t// reattaches just because the root re-rendered.\n\tconst setAnchorRef = useCallback((el: HTMLElement | null): void => {\n\t\tanchorRef.current = el;\n\t}, []);\n\n\t// A plain object rebuilt when its scalar inputs change — the rebuild is\n\t// what makes the content re-render. The non-reactive field stays a getter\n\t// over its ref, which is precisely what it is on the source side.\n\tconst context = useMemo<ContextMenuRootContext>(\n\t\t() => ({\n\t\t\tcontentId,\n\t\t\tside,\n\t\t\talign,\n\t\t\toffset,\n\t\t\tloop,\n\t\t\topen,\n\t\t\tpoint,\n\t\t\tsound,\n\t\t\tget anchorRef() {\n\t\t\t\treturn anchorRef.current;\n\t\t\t},\n\t\t\tsetAnchorRef,\n\t\t\topenAt,\n\t\t\tclose,\n\t\t}),\n\t\t[contentId, side, align, offset, loop, open, point, sound, setAnchorRef, openAt, close]\n\t);\n\n\treturn (\n\t\t<CONTEXT_MENU_KEY.Provider value={context}>\n\t\t\t{children}\n\t\t\t{/*\n\t\t\t\tThe virtual anchor `useAnchorPosition` positions\n\t\t\t\t`ContextMenuContent` against: a zero-size, `position: fixed`\n\t\t\t\tpoint at the last-opened pointer coordinates. The anchor option\n\t\t\t\tneeds a real element to call `getBoundingClientRect()` on —\n\t\t\t\tthere is no DOM node at a right-click, so this one stands in for\n\t\t\t\tit. Kept mounted for this component's whole lifetime (not just\n\t\t\t\twhile open) so it's already in place, positioned, and ready the\n\t\t\t\tinstant `ContextMenuContent` asks for its rect.\n\n\t\t\t\tPortalled, same as `ContextMenuContent` itself: `position: fixed`\n\t\t\t\tresolves its containing block against the nearest ancestor that\n\t\t\t\testablishes one — not just `position`/`transform` on that\n\t\t\t\tancestor, but `filter`, `perspective`, `will-change: transform`\n\t\t\t\tand `contain` too, all of which this library ships components\n\t\t\t\tbuilt on. Left un-portalled, a `<ContextMenu>` nested inside any\n\t\t\t\tof those would measure this span's `getBoundingClientRect()`\n\t\t\t\trelative to that ancestor instead of the viewport —\n\t\t\t\tself-consistent, since the positioning core reads whatever rect\n\t\t\t\tthis span actually reports, but silently wrong: the panel opens\n\t\t\t\taway from the pointer coordinates `clientX`/`clientY` actually\n\t\t\t\treported. Portalling to `document.body` guarantees the same\n\t\t\t\tcontaining block `clientX`/`clientY` are already relative to,\n\t\t\t\tregardless of what the consumer's own tree does around the\n\t\t\t\ttrigger.\n\t\t\t*/}\n\t\t\t<Portal>\n\t\t\t\t<span\n\t\t\t\t\tref={setAnchorRef}\n\t\t\t\t\tclassName=\"ft-context-menu-anchor\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"fixed\",\n\t\t\t\t\t\tleft: `${point.x}px`,\n\t\t\t\t\t\ttop: `${point.y}px`,\n\t\t\t\t\t\twidth: 0,\n\t\t\t\t\t\theight: 0,\n\t\t\t\t\t\tpointerEvents: \"none\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t</Portal>\n\t\t</CONTEXT_MENU_KEY.Provider>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/context-menu/ContextMenu.tsx"},{"path":"react/src/components/context-menu/ContextMenuContent.tsx","content":"\"use client\";\n\nimport { forwardRef, useCallback, useEffect, useMemo } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { Portal } from \"../../internals/Portal\";\nimport { useDismissable } from \"../../internals/dismissable\";\nimport { useMenuFocus } from \"../../internals/menu\";\nimport { useAnchorPosition } from \"../../internals/use-anchor-position\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useConstant } from \"../../internals/dom/ssr\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { anchored, originFor } from \"../../internals/motion/anchored\";\nimport { usePresence } from \"../../internals/motion/presence\";\nimport { createOpenSubRegistry, handleMenuContentKeydown } from \"../dropdown-menu/menu-shared\";\nimport { MENU_KEY } from \"../dropdown-menu/types\";\nimport type { MenuCloseOptions, MenuContext } from \"../dropdown-menu/types\";\nimport { useContextMenuRoot } from \"./types\";\n\nexport interface ContextMenuContentProps {\n\t/** The `ContextMenuItem`/`ContextMenuSeparator`/`ContextMenuLabel`/`ContextMenuSub` children. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes, merged onto the panel. */\n\tclassName?: string;\n}\n\n/**\n * The portalled, anchored panel, and the level of menu-focus wiring its own\n * direct items register with.\n *\n * Self-gated on the root's `open`, same reasoning as `DropdownMenuContent`.\n * Not modal: no focus trap, no scroll lock. Positioned against a *virtual*\n * anchor — `ContextMenu`'s own zero-size anchor span, moved to the last\n * right-click's coordinates — instead of a real trigger element, but the\n * positioning core's flip/clamp behaviour needs nothing different for that: a\n * zero-size `DOMRect` at the pointer flips and clamps at the viewport edges\n * exactly the same way a real element's rect does — and the growth origin\n * follows that same resolved side, so a menu that flipped to sit *above* a\n * click near the bottom of the viewport grows out of its own bottom edge, the\n * one still touching the pointer, instead of its top. The exit collapses back\n * into that same corner.\n *\n * The source declares `ref = $bindable(null)`, so the panel element arrives\n * through the ref channel rather than a prop.\n */\nexport const ContextMenuContent = forwardRef<HTMLDivElement, ContextMenuContentProps>(\n\tfunction ContextMenuContent({ children, className }, forwardedRef) {\n\t\tconst root = useContextMenuRoot();\n\n\t\t// The NODE, not a ref (convention C-1): the panel is created by\n\t\t// `presence.mounted`, so a `useRef` read inside a `[]`-deps effect\n\t\t// would still be null when the position and dismiss effects fire.\n\t\tconst [panel, setPanelNode] = useElementRef<HTMLDivElement>();\n\n\t\t// `loop` as a PLAIN VALUE: the hook hands the core a getter over a live\n\t\t// ref, so `move()` reads `root.loop` fresh on every call exactly as the\n\t\t// source's `get loop()` did.\n\t\tconst focus = useMenuFocus({ loop: root.loop });\n\n\t\tconst registry = useConstant(createOpenSubRegistry);\n\t\tconst { registerOpenSub, closeSiblingSubs } = registry;\n\n\t\tconst presence = usePresence(root.open);\n\n\t\t// The placement as ACTUALLY resolved — the requested side and align\n\t\t// until the positioning core flips or clamps it away from a viewport\n\t\t// edge. Seeded with the *requested* values by the hook itself, so an\n\t\t// un-flipped open never depends on a placement callback having fired\n\t\t// first. This is the panel where a flip is routine rather than\n\t\t// exceptional: the anchor is a point at the pointer, and a right-click\n\t\t// anywhere in the lower or right band of the viewport flips it. Growing\n\t\t// from the corner nearest that point is what keeps the menu feeling\n\t\t// attached to the click instead of erupting from its own middle. The\n\t\t// cross-axis alignment differs from the requested one whenever clamping\n\t\t// slid the panel along that axis — near a viewport edge the requested\n\t\t// corner is no longer the one touching the anchor, and an entrance grown\n\t\t// from it would expand from the far corner instead.\n\t\t//\n\t\t// `recomputeKey` carries the pointer coordinates. The anchor is one span\n\t\t// kept mounted for the root's whole life, so a second right-click while\n\t\t// the menu is open — the normal path, since the pointerdown dismisses\n\t\t// and the `contextmenu` event then reopens the still-mounted panel\n\t\t// mid-exit — moves that span without changing which ELEMENT the\n\t\t// positioner is pointed at, and every geometry option stays put too.\n\t\t// Without the key the panel would sit at the previous click's `left`/\n\t\t// `top` until an unrelated scroll or resize fired. React commits the\n\t\t// span's new inline `left`/`top` before layout effects run, so the rect\n\t\t// this recompute reads is already the new one. The source makes\n\t\t// `root.point` a tracked dependency of its own action options for the\n\t\t// same effect.\n\t\tconst { side: resolvedSide, align: resolvedAlign } = useAnchorPosition(panel, {\n\t\t\tanchor: () => root.anchorRef,\n\t\t\tside: root.side,\n\t\t\talign: root.align,\n\t\t\toffset: root.offset,\n\t\t\trecomputeKey: `${root.point.x},${root.point.y}`,\n\t\t});\n\n\t\t// `active: root.open` — a plain boolean where the source needed a\n\t\t// getter. The layer stays ON the stack for its whole exit and stops\n\t\t// being TOP of it the instant `open` flips, so a second Escape during\n\t\t// the fade is neither answered again nor swallowed on its way to\n\t\t// whatever sits underneath. No `exclude`: unlike a dropdown's button,\n\t\t// this family's trigger is a whole region and the source excludes\n\t\t// nothing from the outside-click test.\n\t\tuseDismissable(panel, {\n\t\t\tonDismiss: () => root.close(),\n\t\t\tactive: root.open,\n\t\t});\n\n\t\t// Identity-stable for the life of the root, so this level's own context\n\t\t// is not rebuilt just because the root re-rendered.\n\t\tconst closeRoot = root.close;\n\t\tconst closeAll = useCallback(\n\t\t\t(options?: MenuCloseOptions) => {\n\t\t\t\t// Closing the root unmounts this whole subtree — every nested\n\t\t\t\t// `ContextMenuSub`/`ContextMenuSubContent` goes with it, so\n\t\t\t\t// there is nothing more for this level to do to close a\n\t\t\t\t// deeply-nested submenu's own state.\n\t\t\t\tcloseRoot(options);\n\t\t\t},\n\t\t\t[closeRoot]\n\t\t);\n\n\t\t// text-[12px]: this family's own density, distinct from DropdownMenu's\n\t\t// 13px — the mockup specifies both explicitly. Every shared item leaf\n\t\t// inherits this from the panel below via normal CSS (they carry no\n\t\t// font-size of their own); a nested `ContextMenuSubContent` gets it\n\t\t// forwarded through `menuContext.itemTextClass` instead, since a\n\t\t// portalled submenu is a DOM sibling of this panel once open, not a\n\t\t// descendant, and can't inherit it directly.\n\t\tconst menuContext = useMemo<MenuContext>(\n\t\t\t() => ({\n\t\t\t\tfocus,\n\t\t\t\titemTextClass: \"text-[12px]\",\n\t\t\t\trootOpen: root.open,\n\t\t\t\tsound: root.sound,\n\t\t\t\tcloseAll,\n\t\t\t\tregisterOpenSub,\n\t\t\t\tcloseSiblingSubs,\n\t\t\t}),\n\t\t\t[focus, root.open, root.sound, closeAll, registerOpenSub, closeSiblingSubs]\n\t\t);\n\n\t\t// The source waits a `tick()` here because its items register from\n\t\t// their own mount effect, which runs AFTER this component's. React runs\n\t\t// child effects before parent effects and registration happens in each\n\t\t// item's ref callback — earlier still — so the items are already\n\t\t// registered by the time this runs.\n\t\t//\n\t\t// Keyed on `presence.mounted` as well as `open`: `mounted` flips in\n\t\t// `usePresence`'s layout effect, one render after `open` does, and it\n\t\t// is that later commit which actually creates the items.\n\t\tconst mounted = presence.mounted;\n\t\tconst open = root.open;\n\t\tuseEffect(() => {\n\t\t\tif (!open || !mounted) return;\n\t\t\tfocus.moveToEdge(\"first\");\n\t\t}, [open, mounted, focus]);\n\n\t\tfunction handleKeydown(event: KeyboardEvent<HTMLDivElement>): void {\n\t\t\t// Only keys pressed inside THIS panel. A `ContextMenuSubContent` is\n\t\t\t// portalled to `document.body` — a DOM sibling of this panel,\n\t\t\t// exactly as in the source — but it stays a React-tree DESCENDANT\n\t\t\t// of this element, and React propagates synthetic events through a\n\t\t\t// portal. Without this guard every ArrowDown/ArrowUp/Home/End and\n\t\t\t// every typeahead character pressed inside an open submenu would be\n\t\t\t// handled twice. In the source the two panels are native DOM\n\t\t\t// siblings and the second dispatch simply does not exist.\n\t\t\tif (!event.currentTarget.contains(event.target as Node)) return;\n\t\t\thandleMenuContentKeydown(event, menuContext, {\n\t\t\t\tonTab: () => root.close({ returnFocus: false }),\n\t\t\t});\n\t\t}\n\n\t\t// Convention C-2: composed ABOVE the conditional below. Calling this\n\t\t// inside the JSX branch would be a conditional hook and would throw the\n\t\t// first time `mounted` flips.\n\t\t//\n\t\t// ONE bidirectional transition, never a split in/out pair: the\n\t\t// in-flight counterpart's current position is passed into the fresh\n\t\t// leg, so a menu reopened mid-exit continues from where it is instead\n\t\t// of snapping to invisible first. `entering` is what tells it which way\n\t\t// it is going, and the params are read at the instant each leg starts.\n\t\tconst panelRef = useComposedRefs(\n\t\t\tsetPanelNode,\n\t\t\tforwardedRef,\n\t\t\tpresence.register(anchored, (entering) => ({ side: resolvedSide, entering }))\n\t\t);\n\n\t\tconst classes = cn(\n\t\t\t\"ft-context-menu-content flex w-max min-w-[180px] flex-col gap-[1px] rounded-[10px] border border-border bg-popover p-[5px] text-[12px] text-popover-foreground shadow-lg outline-none\",\n\t\t\tclassName\n\t\t);\n\n\t\t// The `Portal` stays ABOVE the mounted gate, and the gate wraps its\n\t\t// CHILDREN. `usePortalTarget` resolves its container in a layout\n\t\t// effect, so a `Portal` that first mounts in the same commit as the\n\t\t// panel would render null on that pass — `usePresence` would then find\n\t\t// no registered leg and the entrance would be silently skipped.\n\t\t//\n\t\t// Focus needs nothing here: `ContextMenu`'s own `setOpen` returns focus\n\t\t// to whatever was focused before the right-click, from a plain function\n\t\t// outside this gate, so the return still happens at the dismiss instant\n\t\t// rather than waiting out the fade. `data-state` carries\n\t\t// `surfaceState`'s TWO values — never `\"opening\"` (convention C-5) —\n\t\t// and `inert` is not written by hand either: `usePresence` sets it on\n\t\t// every registered node for the whole exit, which is what keeps a menu\n\t\t// on its way out from answering a click.\n\t\treturn (\n\t\t\t<Portal>\n\t\t\t\t<MENU_KEY.Provider value={menuContext}>\n\t\t\t\t\t{presence.mounted ? (\n\t\t\t\t\t\t<div\n\t\t\t\t\t\t\tref={panelRef}\n\t\t\t\t\t\t\tid={root.contentId}\n\t\t\t\t\t\t\trole=\"menu\"\n\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\tclassName={classes}\n\t\t\t\t\t\t\tdata-state={presence.surfaceState}\n\t\t\t\t\t\t\tdata-side={resolvedSide}\n\t\t\t\t\t\t\tdata-align={root.align}\n\t\t\t\t\t\t\tstyle={{ transformOrigin: originFor(resolvedSide, resolvedAlign) }}\n\t\t\t\t\t\t\tonKeyDown={handleKeydown}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t{children}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t) : null}\n\t\t\t\t</MENU_KEY.Provider>\n\t\t\t</Portal>\n\t\t);\n\t}\n);\n\nContextMenuContent.displayName = \"ContextMenuContent\";\n","type":"registry:ui","target":"@ui/fancy/components/context-menu/ContextMenuContent.tsx"},{"path":"react/src/components/context-menu/ContextMenuTrigger.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef } from \"react\";\nimport type { MouseEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useContextMenuRoot } from \"./types\";\n\nexport interface ContextMenuTriggerProps {\n\t/** Disables the region: `contextmenu` is left alone and the browser's native menu shows instead. */\n\tdisabled?: boolean;\n\t/** The wrapped region's content. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes, merged onto the wrapping element. */\n\tclassName?: string;\n}\n\n/**\n * The region a right-click opens the menu over.\n *\n * The source declares `ref = $bindable(null)`, so the wrapping element\n * arrives through the ref channel rather than a prop.\n *\n * No `role`, and no `aria-haspopup`/`aria-expanded`/`aria-controls` either,\n * unlike `DropdownMenuTrigger`: `contextmenu` is a pointer gesture with an\n * OS-level keyboard equivalent (the Menu key, Shift+F10), not a role-driven\n * interaction an AT user \"activates\" via Enter/Space the way a button or\n * link is. There is no ARIA role that describes \"right-clickable region\",\n * and inventing one (or borrowing `role=\"button\"`) would claim a keyboard/AT\n * interaction model this element doesn't actually offer; the three `aria-*`\n * attributes describe a control a user *activates* to open something, and\n * this wrapping element isn't one.\n */\nexport const ContextMenuTrigger = forwardRef<HTMLDivElement, ContextMenuTriggerProps>(\n\tfunction ContextMenuTrigger({ disabled = false, children, className }, forwardedRef) {\n\t\t// `ContextMenu` only ever mounts this alongside `ContextMenuContent`\n\t\t// under its own context, so there is no standalone-usage fallback to\n\t\t// design for.\n\t\tconst ctx = useContextMenuRoot();\n\n\t\t// A plain ref, not `useElementRef` (the exception convention C-1\n\t\t// carves out): this element is rendered unconditionally and the node\n\t\t// is only ever read inside an event handler, never by an effect keyed\n\t\t// on its arrival.\n\t\tconst node = useRef<HTMLDivElement | null>(null);\n\t\tconst ref = useComposedRefs(node, forwardedRef);\n\n\t\t// How the keyboard path (the Menu key, Shift+F10) is told apart from a\n\t\t// real right-click: both dispatch the same `contextmenu` event — there\n\t\t// is no separate keyboard event to listen for — but `event.button`\n\t\t// reports which mouse button actually fired it, and a\n\t\t// keyboard-synthesized `contextmenu` reports `0` (the same value a\n\t\t// synthetic/keyboard-sourced event always carries), never `2` (the\n\t\t// right mouse button). That is not an inference about where the\n\t\t// pointer probably wasn't — it is what the event states about its own\n\t\t// origin — so it holds even for a genuine right-click at the literal\n\t\t// viewport corner, unlike a coordinate-based guess. The keyboard path\n\t\t// still has no real pointer position to open at, so it falls back to\n\t\t// this region's own rect, same as before.\n\t\tfunction handleContextMenu(event: MouseEvent<HTMLDivElement>): void {\n\t\t\tif (disabled) return;\n\t\t\tevent.preventDefault();\n\t\t\tconst isKeyboardInvoked = event.button !== 2;\n\t\t\tif (isKeyboardInvoked && node.current) {\n\t\t\t\tconst rect = node.current.getBoundingClientRect();\n\t\t\t\tctx.openAt(rect.left, rect.top);\n\t\t\t} else {\n\t\t\t\tctx.openAt(event.clientX, event.clientY);\n\t\t\t}\n\t\t}\n\n\t\treturn (\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={cn(\"ft-context-menu-trigger\", className)}\n\t\t\t\tonContextMenu={handleContextMenu}\n\t\t\t>\n\t\t\t\t{children}\n\t\t\t</div>\n\t\t);\n\t}\n);\n\nContextMenuTrigger.displayName = \"ContextMenuTrigger\";\n","type":"registry:ui","target":"@ui/fancy/components/context-menu/ContextMenuTrigger.tsx"},{"path":"react/src/components/context-menu/index.ts","content":"\"use client\";\n\nexport { ContextMenu } from \"./ContextMenu\";\nexport type { ContextMenuProps } from \"./ContextMenu\";\nexport { ContextMenuTrigger } from \"./ContextMenuTrigger\";\nexport type { ContextMenuTriggerProps } from \"./ContextMenuTrigger\";\nexport { ContextMenuContent } from \"./ContextMenuContent\";\nexport type { ContextMenuContentProps } from \"./ContextMenuContent\";\n\n// `Item`, `Separator`, `Label`, `Sub`, `SubTrigger` and `SubContent` are\n// identical between the two menu families once a panel is open — same markup,\n// same menu-focus wiring, same submenu intent-delay behaviour. Rather than\n// keep a second copy in sync with the first, this family re-exports\n// `dropdown-menu`'s implementations under its own names. What genuinely\n// differs is the root, the trigger, and how the content anchors itself.\nexport { DropdownMenuItem as ContextMenuItem } from \"../dropdown-menu/DropdownMenuItem\";\nexport type { DropdownMenuItemProps as ContextMenuItemProps } from \"../dropdown-menu/DropdownMenuItem\";\nexport { DropdownMenuSeparator as ContextMenuSeparator } from \"../dropdown-menu/DropdownMenuSeparator\";\nexport type { DropdownMenuSeparatorProps as ContextMenuSeparatorProps } from \"../dropdown-menu/DropdownMenuSeparator\";\nexport { DropdownMenuLabel as ContextMenuLabel } from \"../dropdown-menu/DropdownMenuLabel\";\nexport type { DropdownMenuLabelProps as ContextMenuLabelProps } from \"../dropdown-menu/DropdownMenuLabel\";\nexport { DropdownMenuSub as ContextMenuSub } from \"../dropdown-menu/DropdownMenuSub\";\nexport type { DropdownMenuSubProps as ContextMenuSubProps } from \"../dropdown-menu/DropdownMenuSub\";\nexport { DropdownMenuSubTrigger as ContextMenuSubTrigger } from \"../dropdown-menu/DropdownMenuSubTrigger\";\nexport type { DropdownMenuSubTriggerProps as ContextMenuSubTriggerProps } from \"../dropdown-menu/DropdownMenuSubTrigger\";\nexport { DropdownMenuSubContent as ContextMenuSubContent } from \"../dropdown-menu/DropdownMenuSubContent\";\nexport type { DropdownMenuSubContentProps as ContextMenuSubContentProps } from \"../dropdown-menu/DropdownMenuSubContent\";\nexport { CONTEXT_MENU_KEY, type ContextMenuRootContext } from \"./types\";\nexport { MENU_KEY, type MenuContext, SUB_KEY, type SubContext } from \"../dropdown-menu/types\";\n","type":"registry:ui","target":"@ui/fancy/components/context-menu/index.ts"},{"path":"react/src/components/context-menu/types.ts","content":"\"use client\";\n\n/**\n * The one contract this family publishes of its own, and the React context\n * that carries it.\n *\n * `MenuContext`/`MENU_KEY`/`SubContext`/`SUB_KEY` are NOT redefined here —\n * they are the exact same contract `DropdownMenuContent` and\n * `DropdownMenuSubContent` already implement, re-exported from\n * `dropdown-menu/types.ts` so both families' item-level components\n * (`*Item`, `*Separator`, `*Label`, `*Sub`, `*SubTrigger`, `*SubContent`)\n * share one implementation instead of two.\n *\n * The Svelte source publishes its own key as a `unique symbol`; React's\n * context object plays that role here, so the exported name is kept and the\n * value is a `React.Context` rather than a symbol — the same substitution\n * `dropdown-menu/types.ts` makes for `DROPDOWN_MENU_KEY`.\n */\n\nimport { createContext, useContext } from \"react\";\nimport type { Side, Align } from \"../../internals/anchor-position\";\nimport type { MenuCloseOptions } from \"../dropdown-menu/types\";\n\nexport { MENU_KEY, SUB_KEY } from \"../dropdown-menu/types\";\nexport type { MenuContext, MenuCloseOptions, SubContext } from \"../dropdown-menu/types\";\n\n/** The contract between `ContextMenu` and its own `ContextMenuTrigger`/`ContextMenuContent`. */\nexport interface ContextMenuRootContext {\n\treadonly contentId: string;\n\treadonly side: Side;\n\treadonly align: Align;\n\treadonly offset: number;\n\treadonly loop: boolean;\n\treadonly open: boolean;\n\t/** The pointer coordinates the panel is anchored at — viewport-relative, matching `MouseEvent.clientX/clientY`. */\n\treadonly point: { x: number; y: number };\n\t/** The zero-size virtual anchor element `ContextMenuContent` positions itself against. */\n\treadonly anchorRef: HTMLElement | null;\n\t/** Whether this menu plays sound cues — mirrors `ContextMenuProps.sound`. */\n\treadonly sound: boolean;\n\tsetAnchorRef(el: HTMLElement | null): void;\n\t/** Opens (or, if already open, repositions) the menu at `(x, y)`. */\n\topenAt(x: number, y: number): void;\n\t/** Closes the menu. Returns focus to whatever held it before the menu opened, by default. */\n\tclose(options?: MenuCloseOptions): void;\n}\n\nexport const CONTEXT_MENU_KEY = createContext<ContextMenuRootContext | undefined>(undefined);\nCONTEXT_MENU_KEY.displayName = \"ContextMenuRootContext\";\n\n/**\n * Reads the enclosing `ContextMenu`'s context.\n *\n * Both consumers below are only ever mounted by this folder's own root, so\n * there is no standalone-usage fallback to design for — the same assumption\n * each source sub-component makes. Internal; not exported from `index.ts`.\n */\nexport function useContextMenuRoot(): ContextMenuRootContext {\n\tconst value = useContext(CONTEXT_MENU_KEY);\n\tif (value === undefined) {\n\t\tthrow new Error(\n\t\t\t\"ContextMenuRootContext is missing: this component must be rendered inside its provider.\"\n\t\t);\n\t}\n\treturn value;\n}\n","type":"registry:ui","target":"@ui/fancy/components/context-menu/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/context-menu?framework=react"}}