Context Menu Unfold
ns-ui/context-menu-unfoldFreeComponent
A context menu that unfolds like a pocket knife — items swing out from a hinged spine as slim blades, staggered from folded to open, with a hairline edge highlight while swinging; submenus unfold as a second, smaller knife from a blade's tip.
Live preview
live · rendered by the registry
Rendered by ns-ui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://design.helpmarq.com/r/context-menu-unfold.jsonSource
1"use client";23import {4 useCallback,5 useEffect,6 useId,7 useMemo,8 useRef,9 useState,10 type KeyboardEvent as ReactKeyboardEvent,11 type MouseEvent as ReactMouseEvent,12 type ReactNode,13} from "react";1415// ---------------------------------------------------------------------------16// JackKnife — a context menu that unfolds like a pocket knife. A spine17// anchors at the trigger point (the pointer for a right-click, or the18// trigger button's own position for the accessible trigger); menu items are19// slim "blades" hinged at the spine edge, each swinging in with a staggered20// rotation from folded (~80deg, tucked flat against the spine) to open21// (0deg). Every swing is a one-shot ref-driven CSS transition (transform +22// a border-color pulse for the hairline edge highlight) — timed with a23// per-item delay for the stagger — not a continuous rAF loop. A submenu is24// the same component recursively, anchored at the parent blade's tip.25// Distinct from menu-nested-trays (telescoping trays) and dropdown-drape (cloth26// dropdown): this is a radial-fold context-menu primitive.27// ---------------------------------------------------------------------------2829export interface JackKnifeItem {30 id: string;31 label: string;32 shortcut?: string;33 disabled?: boolean;34 submenu?: JackKnifeItem[];35}3637export interface JackKnifeProps {38 items: JackKnifeItem[];39 onSelect?: (id: string) => void;40 /** region that opens the menu on right-click; the trigger button always works too */41 children?: ReactNode;42 /** accessible name for the trigger button and the menu itself */43 label?: string;44 className?: string;45}4647type Anchor = { x: number; y: number; side: "left" | "right" };4849const OPEN_MS = 260;50const OPEN_STAGGER_MS = 38;51const CLOSE_MS = 150;52const CLOSE_STAGGER_MS = 22;53const HAIRLINE_HOLD_MS = 120;54const TYPEAHEAD_RESET_MS = 700;55const ITEM_W = 200;56const EDGE_MARGIN = 10;5758function computeAnchor(x: number, y: number): Anchor {59 const side: Anchor["side"] = x + ITEM_W + EDGE_MARGIN > window.innerWidth ? "left" : "right";60 return { x, y, side };61}6263function clampTop(y: number, height: number) {64 return Math.min(Math.max(EDGE_MARGIN, y), Math.max(EDGE_MARGIN, window.innerHeight - height - EDGE_MARGIN));65}6667function foldItem(68 el: HTMLElement | null,69 index: number,70 opening: boolean,71 side: "left" | "right",72 reduced: boolean73) {74 if (!el) return;75 const sign = side === "right" ? -1 : 1;76 if (reduced) {77 el.style.transition = "none";78// … truncated
Files it writes
More from ns-ui
All 228 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Accordion Latchaccordion-latch | ns-ui | Components | Free | no deps | |
| Approval Inline Diffapproval-inline-diff | ns-ui | Components | Free | no deps | |
| ASCII Engraving Contourascii-engraving-contour | ns-ui | Components | Free | no deps | |
| ASCII Globe Spinascii-globe-spin | ns-ui | Components | Free | no deps | |
| ASCII Torus Donutascii-torus-donut | ns-ui | Components | Free | no deps | |
| Autosave Ratchetautosave-ratchet | ns-ui | Components | Free | no deps | |
| Avatar Stack Flockavatar-stack-flock | ns-ui | Components | Free | no deps | |
| Background ASCII Ditherbackground-ascii-dither | ns-ui | Components | Free | no deps |
