"use client" import * as React from "react" import { cn } from "@/lib/utils" export type RightClickMenuItem = { label: React.ReactNode icon?: React.ReactNode shortcut?: React.ReactNode onSelect?: () => void disabled?: boolean variant?: "default" | "destructive" } export type RightClickMenuProps = React.ComponentProps<"div"> & { items: RightClickMenuItem[] label?: string } function getClampedPoint(x: number, y: number, width = 224, height: number) { if (typeof window === "undefined") { return { x, y } } const padding = 8 return { x: Math.min(Math.max(padding, x), Math.max(padding, window.innerWidth - width - padding)), y: Math.min(Math.max(padding, y), Math.max(padding, window.innerHeight - height - padding)), } } function RightClickMenu({ items, children, className, label = "Context menu", ...props }: RightClickMenuProps) { const [open, setOpen] = React.useState(false) const [point, setPoint] = React.useState({ x: 0, y: 0 }) const [activeIndex, setActiveIndex] = React.useState(0) const rootRef = React.useRef(null) const itemRefs = React.useRef>([]) const enabledItems = React.useMemo( () => items.map((item, index) => ({ item, index })).filter(({ item }) => !item.disabled), [items] ) const openAt = React.useCallback( (x: number, y: number) => { const nextPoint = getClampedPoint(x, y, 224, Math.max(44, items.length * 42 + 12)) setPoint(nextPoint) setActiveIndex(enabledItems[0]?.index ?? 0) setOpen(true) }, [enabledItems, items.length] ) React.useEffect(() => { if (!open) return const close = () => setOpen(false) const onKeyDown = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false) } } window.addEventListener("pointerdown", close) window.addEventListener("resize", close) window.addEventListener("scroll", close, true) window.addEventListener("keydown", onKeyDown) return () => { window.removeEventListener("pointerdown", close) window.removeEventListener("resize", close) window.removeEventListener("scroll", close, true) window.removeEventListener("keydown", onKeyDown) } }, [open]) React.useEffect(() => { if (!open) return itemRefs.current[activeIndex]?.focus() }, [activeIndex, open]) const focusNext = (direction: 1 | -1) => { if (enabledItems.length === 0) return const currentEnabledIndex = enabledItems.findIndex(({ index }) => index === activeIndex) const nextEnabled = enabledItems[(currentEnabledIndex + direction + enabledItems.length) % enabledItems.length] setActiveIndex(nextEnabled.index) } return (
{ event.preventDefault() openAt(event.clientX, event.clientY) }} onKeyDown={(event) => { if (event.key === "ContextMenu" || (event.shiftKey && event.key === "F10")) { event.preventDefault() const rect = rootRef.current?.getBoundingClientRect() openAt(rect ? rect.left + 12 : 12, rect ? rect.top + 12 : 12) } }} {...props} > {children} {open ? (
event.stopPropagation()} onKeyDown={(event) => { if (event.key === "ArrowDown") { event.preventDefault() focusNext(1) } if (event.key === "ArrowUp") { event.preventDefault() focusNext(-1) } if (event.key === "Home") { event.preventDefault() setActiveIndex(enabledItems[0]?.index ?? activeIndex) } if (event.key === "End") { event.preventDefault() setActiveIndex(enabledItems.at(-1)?.index ?? activeIndex) } }} > {items.map((item, index) => ( ))}
) : null}
) } export { RightClickMenu }