Animated Context Menu
beui/context-menuFreeComponent
Composable context-menu primitives with a pointer-origin clip morph, a gliding active row, checkbox and radio choices, keyboard navigation, typeahead, and long-press support.
Install it
npx shadcn@latest add https://beui.dev/r/context-menu.jsonSource
1"use client";2// beui.dev/components/motion/context-menu34import { Check } from "lucide-react";5import {6 AnimatePresence,7 motion,8 useReducedMotion,9} from "motion/react";10import {11 cloneElement,12 createContext,13 isValidElement,14 type ReactElement,15 type KeyboardEvent as ReactKeyboardEvent,16 type MouseEvent as ReactMouseEvent,17 type ReactNode,18 type PointerEvent as ReactPointerEvent,19 type Ref,20 useCallback,21 useContext,22 useEffect,23 useId,24 useLayoutEffect,25 useMemo,26 useRef,27 useState,28} from "react";29import { createPortal } from "react-dom";30import { EASE_OUT, SPRING_LAYOUT, SPRING_PANEL } from "@/lib/ease";31import { cn } from "@/lib/utils";3233type OpenModality = "pointer" | "keyboard" | "touch";34type MenuPoint = { x: number; y: number };3536const VIEWPORT_PADDING = 8;37const LONG_PRESS_DELAY = 520;38const LONG_PRESS_TOLERANCE = 10;39const MORPH_DURATION = 0.3;4041type TriggerElementProps = React.HTMLAttributes<HTMLElement> & {42 ref?: Ref<HTMLElement>;43};4445interface ContextMenuContextValue {46 open: boolean;47 setOpen: (open: boolean) => void;48 openAt: (point: MenuPoint, modality: OpenModality) => void;49 point: MenuPoint;50 modality: OpenModality;51 invocation: number;52 menuId: string;53 triggerRef: React.MutableRefObject<HTMLElement | null>;54 contentRef: React.MutableRefObject<HTMLDivElement | null>;55 activeId: string | null;56 setActiveId: (id: string | null) => void;57 reduce: boolean;58}5960const ContextMenuContext = createContext<ContextMenuContextValue | null>(null);6162function useContextMenuContext(component: string) {63 const context = useContext(ContextMenuContext);64 if (!context) {65 throw new Error(`${component} must be used within <ContextMenu>`);66 }67 return context;68}6970function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {71 if (typeof ref === "function") {72 ref(value);73 } else if (ref) {74 ref.current = value;75 }76}7778function getEnabledItems(container: HTMLElement | null) {79 if (!container) return [];80 return Array.from(81 container.querySelectorAll<HTMLElement>(82 '[data-context-menu-item="true"]:not([data-disabled="true"])',83 ),84 );85}8687function clamp(value: number, min: number, max: number) {88 return Math.min(Math.max(value, min), max);89}9091function collapsedClip(92 origin: MenuPoint,93 size: { width: number; height: number },94) {95 const half = 8;96 const top = clamp(origin.y - half, 0, size.height);97 const right = clamp(size.width - origin.x - half, 0, size.width);98// … truncated
What it pulls in
npm packages
Files it writes
More from beui
All 104 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Action Swap Bluraction-swap-blur | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Cascadeaction-swap-cascade | beui | Components | Free | 3 deps · 4 files | |
| Action Swap Rollaction-swap-roll | beui | Components | Free | 3 deps · 4 files | |
| Agent Activityagent-activity | beui | Components | Free | 4 deps · 9 files | |
| Agent Loading States Agent Progressagent-progress | beui | Components | Free | 3 deps · 3 files | |
| AI Sidebarai-sidebar | beui | Components | Free | 4 deps · 5 files | |
| Animated Badgeanimated-badge | beui | Components | Free | 4 deps · 3 files | |
| Number Animation Animated Numberanimated-number | beui | Components | Free | 3 deps · 3 files |
