"use client" import * as React from "react" import { createPortal } from "react-dom" import { Check, ChevronRight, Circle } from "lucide-react" import { cn } from "@/lib/utils" const DropdownMenuContext = React.createContext<{ open: boolean setOpen: React.Dispatch> triggerRef: React.RefObject }>({ open: false, setOpen: () => {}, triggerRef: { current: null }, }) export function DropdownMenu({ children, open: controlledOpen, onOpenChange, }: { children: React.ReactNode open?: boolean onOpenChange?: (open: boolean) => void }) { const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false) const triggerRef = React.useRef(null) const isControlled = controlledOpen !== undefined const open = isControlled ? controlledOpen : uncontrolledOpen const setOpen = React.useCallback( (value: boolean | ((prev: boolean) => boolean)) => { if (isControlled) { const nextValue = typeof value === "function" ? value(controlledOpen) : value onOpenChange?.(nextValue) } else { setUncontrolledOpen(value) } }, [isControlled, controlledOpen, onOpenChange], ) return (
{children}
) } export function DropdownMenuTrigger({ children, asChild, className, ...props }: { children: React.ReactNode asChild?: boolean className?: string } & React.HTMLAttributes) { const { open, setOpen, triggerRef } = React.useContext(DropdownMenuContext) const handleClick = (e: React.MouseEvent) => { e.preventDefault() setOpen(!open) props.onClick?.(e) } if (asChild && React.isValidElement(children)) { return React.cloneElement(children, { ref: triggerRef, onClick: handleClick, "aria-expanded": open, "aria-haspopup": true, ...props, }) } return ( ) } export function DropdownMenuContent({ children, className, align = "start", sideOffset = 4, alignOffset = 0, ...props }: { children: React.ReactNode className?: string align?: "start" | "center" | "end" sideOffset?: number alignOffset?: number } & React.HTMLAttributes) { const { open, setOpen, triggerRef } = React.useContext(DropdownMenuContext) const contentRef = React.useRef(null) const [mounted, setMounted] = React.useState(false) const [position, setPosition] = React.useState({ top: 0, left: 0, transformOrigin: "top center", }) React.useEffect(() => { setMounted(true) return () => setMounted(false) }, []) React.useEffect(() => { if (open && triggerRef.current && mounted) { const triggerRect = triggerRef.current.getBoundingClientRect() const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight // Calculate width of content (estimate if not mounted yet) const contentWidth = contentRef.current?.offsetWidth || 200 const contentHeight = contentRef.current?.offsetHeight || 200 // Calculate left position based on alignment let left = 0 if (align === "start") { left = triggerRect.left + alignOffset } else if (align === "center") { left = triggerRect.left + triggerRect.width / 2 - contentWidth / 2 + alignOffset } else if (align === "end") { left = triggerRect.right - contentWidth + alignOffset } // Ensure menu stays within viewport horizontally const rightOverflow = left + contentWidth - viewportWidth if (rightOverflow > 0) { left = Math.max(8, left - rightOverflow - 8) // 8px safety margin } if (left < 8) { left = 8 // 8px minimum margin from left edge } // Calculate top position let top = triggerRect.bottom + sideOffset let transformOrigin = "top center" // Check if menu would overflow bottom of viewport if (top + contentHeight > viewportHeight - 8) { // Position above trigger if not enough space below top = Math.max(8, triggerRect.top - contentHeight - sideOffset) transformOrigin = "bottom center" } setPosition({ top, left, transformOrigin }) } }, [open, align, sideOffset, alignOffset, mounted]) React.useEffect(() => { if (!mounted) return const handleClickOutside = (event: MouseEvent) => { if ( contentRef.current && !contentRef.current.contains(event.target as Node) && triggerRef.current && !triggerRef.current.contains(event.target as Node) ) { setOpen(false) } } const handleEscape = (event: KeyboardEvent) => { if (event.key === "Escape") { setOpen(false) } } if (open) { document.addEventListener("mousedown", handleClickOutside) document.addEventListener("keydown", handleEscape) } return () => { document.removeEventListener("mousedown", handleClickOutside) document.removeEventListener("keydown", handleEscape) } }, [open, setOpen, mounted]) if (!mounted || !open) return null const content = (
{children}
) return createPortal(content, document.body) } export function DropdownMenuItem({ children, className, disabled, onSelect, ...props }: { children: React.ReactNode className?: string disabled?: boolean onSelect?: () => void } & React.HTMLAttributes) { const { setOpen } = React.useContext(DropdownMenuContext) const handleClick = (e: React.MouseEvent) => { if (disabled) return onSelect?.() setOpen(false) props.onClick?.(e) } return (
{children}
) } export function DropdownMenuCheckboxItem({ children, className, checked, onCheckedChange, disabled, ...props }: { children: React.ReactNode className?: string checked?: boolean onCheckedChange?: (checked: boolean) => void disabled?: boolean } & React.HTMLAttributes) { const handleSelect = () => { if (disabled) return onCheckedChange?.(!checked) } return ( {checked && } {children} ) } export function DropdownMenuRadioItem({ children, className, checked, onSelect, disabled, ...props }: { children: React.ReactNode className?: string checked?: boolean onSelect?: () => void disabled?: boolean } & React.HTMLAttributes) { return ( {checked && } {children} ) } export function DropdownMenuLabel({ children, className, inset, ...props }: { children: React.ReactNode className?: string inset?: boolean } & React.HTMLAttributes) { return (
{children}
) } export function DropdownMenuSeparator({ className, ...props }: React.HTMLAttributes) { return
} export function DropdownMenuShortcut({ className, ...props }: React.HTMLAttributes) { return } export function DropdownMenuGroup({ children, className, ...props }: React.HTMLAttributes) { return (
{children}
) } export function DropdownMenuSub({ children, ...props }: { children: React.ReactNode }) { return <>{children} } export function DropdownMenuSubTrigger({ children, className, ...props }: { children: React.ReactNode className?: string } & React.HTMLAttributes) { return (
{children}
) } export function DropdownMenuSubContent({ children, className, ...props }: { children: React.ReactNode className?: string } & React.HTMLAttributes) { return (
{children}
) } export function DropdownMenuRadioGroup({ children, className, ...props }: React.HTMLAttributes) { return (
{children}
) }