"use client" import * as React from "react" import { createPortal } from "react-dom" import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react" import { cn } from "@/lib/utils" interface DropdownMenuContextValue { open: boolean setOpen: (open: boolean) => void triggerRef: React.RefObject } const DropdownMenuContext = React.createContext(null) function useDropdownMenu() { const context = React.useContext(DropdownMenuContext) if (!context) { throw new Error("useDropdownMenu must be used within a DropdownMenu") } return context } interface DropdownMenuProps { children: React.ReactNode open?: boolean defaultOpen?: boolean onOpenChange?: (open: boolean) => void } function DropdownMenu({ children, open: controlledOpen, defaultOpen = false, onOpenChange, }: DropdownMenuProps) { const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen) const triggerRef = React.useRef(null) const isControlled = controlledOpen !== undefined const open = isControlled ? controlledOpen : uncontrolledOpen const setOpen = React.useCallback((value: boolean) => { if (!isControlled) { setUncontrolledOpen(value) } onOpenChange?.(value) }, [isControlled, onOpenChange]) return ( {children} ) } function DropdownMenuTrigger({ className, children, asChild, ...props }: React.ButtonHTMLAttributes & { asChild?: boolean }) { const { open, setOpen, triggerRef } = useDropdownMenu() const handleClick = (e: React.MouseEvent) => { e.preventDefault() setOpen(!open) props.onClick?.(e) } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " " || e.key === "ArrowDown") { e.preventDefault() setOpen(true) } props.onKeyDown?.(e) } if (asChild && React.isValidElement(children)) { return React.cloneElement(children as React.ReactElement, { ref: triggerRef, onClick: handleClick, onKeyDown: handleKeyDown, "aria-expanded": open, "aria-haspopup": "menu", "data-state": open ? "open" : "closed", }) } return ( ) } function DropdownMenuPortal({ children }: { children: React.ReactNode }) { return <>{children} } interface DropdownMenuContentProps extends React.HTMLAttributes { sideOffset?: number align?: "start" | "center" | "end" side?: "top" | "right" | "bottom" | "left" } function DropdownMenuContent({ className, sideOffset = 4, align = "start", side = "bottom", children, ...props }: DropdownMenuContentProps) { const { open, setOpen, triggerRef } = useDropdownMenu() const contentRef = React.useRef(null) const [position, setPosition] = React.useState({ top: 0, left: 0 }) const [mounted, setMounted] = React.useState(false) React.useEffect(() => { setMounted(true) }, []) React.useLayoutEffect(() => { if (!open || !triggerRef.current || !contentRef.current) return const trigger = triggerRef.current.getBoundingClientRect() const content = contentRef.current.getBoundingClientRect() let top = 0 let left = 0 // Calculate position based on side switch (side) { case "top": top = trigger.top - content.height - sideOffset break case "bottom": top = trigger.bottom + sideOffset break case "left": left = trigger.left - content.width - sideOffset top = trigger.top break case "right": left = trigger.right + sideOffset top = trigger.top break } // Calculate alignment for top/bottom if (side === "top" || side === "bottom") { switch (align) { case "start": left = trigger.left break case "center": left = trigger.left + (trigger.width - content.width) / 2 break case "end": left = trigger.right - content.width break } } // Calculate alignment for left/right if (side === "left" || side === "right") { switch (align) { case "start": top = trigger.top break case "center": top = trigger.top + (trigger.height - content.height) / 2 break case "end": top = trigger.bottom - content.height break } } setPosition({ top, left }) }, [open, side, align, sideOffset, triggerRef]) React.useEffect(() => { if (!open) return const handleClickOutside = (event: MouseEvent) => { const target = event.target as Node if ( contentRef.current && !contentRef.current.contains(target) && triggerRef.current && !triggerRef.current.contains(target) ) { setOpen(false) } } const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false) triggerRef.current?.focus() } } document.addEventListener("mousedown", handleClickOutside) document.addEventListener("keydown", handleEscape) return () => { document.removeEventListener("mousedown", handleClickOutside) document.removeEventListener("keydown", handleEscape) } }, [open, setOpen, triggerRef]) if (!open || !mounted) return null const slideClasses = { top: "slide-in-from-bottom-2", bottom: "slide-in-from-top-2", left: "slide-in-from-right-2", right: "slide-in-from-left-2", } return createPortal(
{children}
, document.body ) } function DropdownMenuGroup({ className, ...props }: React.HTMLAttributes) { return (
) } interface DropdownMenuItemProps extends React.ButtonHTMLAttributes { inset?: boolean variant?: "default" | "destructive" asChild?: boolean } function DropdownMenuItem({ className, inset, variant = "default", children, asChild, ...props }: DropdownMenuItemProps) { const { setOpen } = useDropdownMenu() const handleClick = (e: React.MouseEvent) => { props.onClick?.(e) if (!e.defaultPrevented) { setOpen(false) } } const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault() handleClick(e as unknown as React.MouseEvent) } props.onKeyDown?.(e) } const itemClasses = cn( "focus:bg-accent focus:text-accent-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none", "data-[disabled]:pointer-events-none data-[disabled]:opacity-50", "[&_svg:not([class*='text-'])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", variant === "destructive" && "text-destructive focus:bg-destructive/10 focus:text-destructive dark:focus:bg-destructive/20", inset && "pl-8", className ) if (asChild && React.isValidElement(children)) { return React.cloneElement(children as React.ReactElement, { className: itemClasses, role: "menuitem", tabIndex: -1, onClick: handleClick, onKeyDown: handleKeyDown, }) } return ( ) } interface DropdownMenuCheckboxItemProps extends Omit, "checked"> { checked?: boolean onCheckedChange?: (checked: boolean) => void } function DropdownMenuCheckboxItem({ className, children, checked, onCheckedChange, ...props }: DropdownMenuCheckboxItemProps) { const handleClick = (e: React.MouseEvent) => { e.preventDefault() onCheckedChange?.(!checked) props.onClick?.(e) } return ( ) } interface DropdownMenuRadioGroupProps extends React.HTMLAttributes { value?: string onValueChange?: (value: string) => void } const RadioGroupContext = React.createContext<{ value?: string onValueChange?: (value: string) => void } | null>(null) function DropdownMenuRadioGroup({ value, onValueChange, children, ...props }: DropdownMenuRadioGroupProps) { return (
{children}
) } interface DropdownMenuRadioItemProps extends React.ButtonHTMLAttributes { value: string } function DropdownMenuRadioItem({ className, children, value, ...props }: DropdownMenuRadioItemProps) { const context = React.useContext(RadioGroupContext) const checked = context?.value === value const handleClick = (e: React.MouseEvent) => { e.preventDefault() context?.onValueChange?.(value) props.onClick?.(e) } return ( ) } interface DropdownMenuLabelProps extends React.HTMLAttributes { inset?: boolean } function DropdownMenuLabel({ className, inset, ...props }: DropdownMenuLabelProps) { return (
) } function DropdownMenuSeparator({ className, ...props }: React.HTMLAttributes) { return (
) } function DropdownMenuShortcut({ className, ...props }: React.HTMLAttributes) { return ( ) } interface SubMenuContextValue { open: boolean setOpen: (open: boolean) => void } const SubMenuContext = React.createContext(null) function DropdownMenuSub({ children }: { children: React.ReactNode }) { const [open, setOpen] = React.useState(false) return (
{children}
) } interface DropdownMenuSubTriggerProps extends React.ButtonHTMLAttributes { inset?: boolean } function DropdownMenuSubTrigger({ className, inset, children, ...props }: DropdownMenuSubTriggerProps) { const context = React.useContext(SubMenuContext) const handleMouseEnter = () => { context?.setOpen(true) } const handleMouseLeave = () => { context?.setOpen(false) } return ( ) } function DropdownMenuSubContent({ className, ...props }: React.HTMLAttributes) { const context = React.useContext(SubMenuContext) if (!context?.open) return null return (
context.setOpen(true)} onMouseLeave={() => context.setOpen(false)} {...props} /> ) } export { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, }