//@ts-nocheck "use client"; import * as React from "react"; import { createPortal } from "react-dom"; import { motion, AnimatePresence } from "framer-motion"; import type { HTMLMotionProps } from "framer-motion"; import { cn } from "@/utils/cn"; import { useDropdownHover } from "@/components/hooks/use-dropdown-hover"; interface MenuContextValue { open: boolean; setOpen: (open: boolean) => void; triggerRef: React.RefObject; } const MenuContext = React.createContext( undefined, ); function useMenu() { const context = React.useContext(MenuContext); if (!context) { throw new Error("useMenu must be used within a Menu"); } return context; } /* ------------------------------------------------------------------------------------------------- * Menu * -----------------------------------------------------------------------------------------------*/ interface MenuProps { open?: boolean; onOpenChange?: (open: boolean) => void; children: React.ReactNode; } export function Menu({ open: controlledOpen, onOpenChange, children, }: MenuProps) { const [uncontrolledOpen, setUncontrolledOpen] = React.useState(false); const triggerRef = React.useRef(null); const open = controlledOpen !== undefined ? controlledOpen : uncontrolledOpen; const setOpen = React.useCallback( (value: boolean) => { if (controlledOpen === undefined) { setUncontrolledOpen(value); } onOpenChange?.(value); }, [controlledOpen, onOpenChange], ); return ( {children} ); } /* ------------------------------------------------------------------------------------------------- * MenuTrigger * -----------------------------------------------------------------------------------------------*/ interface MenuTriggerProps extends React.ButtonHTMLAttributes { asChild?: boolean; } export const MenuTrigger = React.forwardRef< HTMLButtonElement, MenuTriggerProps >(({ onClick, asChild, children, ...props }, forwardedRef) => { const { open, setOpen, triggerRef } = useMenu(); const handleClick = (e: React.MouseEvent) => { onClick?.(e); if (!e.defaultPrevented) { setOpen(!open); } }; const setRefs = React.useCallback( (node: HTMLElement | null) => { if (forwardedRef) { if (typeof forwardedRef === "function") { forwardedRef(node as HTMLButtonElement); } else { forwardedRef.current = node as HTMLButtonElement; } } (triggerRef as React.MutableRefObject).current = node; }, [forwardedRef, triggerRef], ); if (asChild && React.isValidElement(children)) { const childOnClick = (children as any).props?.onClick as | ((e: React.MouseEvent) => void) | undefined; return React.cloneElement(children as any, { ...props, ref: setRefs, onClick: (e: React.MouseEvent) => { // Invoke child's onClick first so it can prevent default behavior childOnClick?.(e); if (e.defaultPrevented) return; // Fall back to MenuTrigger's own click handler handleClick(e as any); }, }); } return ( ); }); MenuTrigger.displayName = "MenuTrigger"; /* ------------------------------------------------------------------------------------------------- * MenuContent * -----------------------------------------------------------------------------------------------*/ interface MenuContentProps extends Omit, "ref"> { align?: "start" | "center" | "end"; side?: "top" | "bottom" | "left" | "right"; sideOffset?: number; alignOffset?: number; matchTriggerWidth?: boolean; } function MenuPortal({ children, open, }: { children: React.ReactNode; open: boolean; }) { const [shouldRender, setShouldRender] = React.useState(false); React.useEffect(() => { if (open) { setShouldRender(true); } }, [open]); if (!shouldRender) return null; return createPortal( { if (!open) { setShouldRender(false); } }} > {children} , document.body, ); } export const MenuContent = React.forwardRef( ( { className, align = "start", side = "bottom", sideOffset = 8, alignOffset = 0, matchTriggerWidth = false, children, ...props }, ref, ) => { const { open, setOpen, triggerRef } = useMenu(); const [position, setPosition] = React.useState<{ top: number; left: number; } | null>(null); const [menuWidth, setMenuWidth] = React.useState(0); const menuRef = React.useRef(null); React.useEffect(() => { if (open && triggerRef.current) { const updatePosition = () => { if (!triggerRef.current) return; const rect = triggerRef.current.getBoundingClientRect(); const menuPosition = { top: 0, left: 0 }; // Calculate vertical position based on side if (side === "top") { menuPosition.top = rect.top - sideOffset; } else if (side === "bottom") { menuPosition.top = rect.bottom + sideOffset; } else if (side === "left" || side === "right") { menuPosition.top = rect.top; } // Calculate horizontal position based on side and alignment if (side === "left") { menuPosition.left = rect.left - sideOffset; } else if (side === "right") { menuPosition.left = rect.right + sideOffset; } else if (side === "top" || side === "bottom") { // For top/bottom placement, handle alignment if (align === "start") { menuPosition.left = rect.left + alignOffset; } else if (align === "center") { menuPosition.left = rect.left + rect.width / 2; } else if (align === "end") { menuPosition.left = rect.right + alignOffset; } } setPosition(menuPosition); if (matchTriggerWidth) { setMenuWidth(rect.width); } }; // Initial position calculation updatePosition(); // Update position on scroll/resize window.addEventListener("scroll", updatePosition, true); window.addEventListener("resize", updatePosition); return () => { window.removeEventListener("scroll", updatePosition, true); window.removeEventListener("resize", updatePosition); }; } }, [ open, side, align, sideOffset, alignOffset, matchTriggerWidth, triggerRef, ]); // Separate effect to handle menu width calculations after render React.useEffect(() => { if (open && position && menuRef.current && triggerRef.current) { const rect = triggerRef.current.getBoundingClientRect(); const menuRect = menuRef.current.getBoundingClientRect(); // Adjust position for end alignment if (align === "end" && (side === "top" || side === "bottom")) { setPosition((prev) => ({ ...prev!, left: rect.right - menuRect.width + alignOffset, })); } else if ( align === "center" && (side === "top" || side === "bottom") ) { setPosition((prev) => ({ ...prev!, left: rect.left + rect.width / 2 - menuRect.width / 2, })); } } }, [open, position, align, side, alignOffset]); return ( {open && position && ( <> {/* Backdrop */} setOpen(false)} /> {/* Menu */} { if (ref) { if (typeof ref === "function") { ref(node); } } menuRef.current = node; }} initial={{ opacity: 0, y: -6, scale: 1, filter: "blur(1px)" }} animate={{ opacity: 1, y: 0, scale: 1, filter: "blur(0px)" }} exit={{ opacity: 0, y: 8, scale: 0.98, filter: "blur(1px)" }} transition={{ ease: [0.1, 0.1, 0.25, 1], duration: 0.2, }} className={cn( "fixed z-[100] min-w-[8rem] overflow-hidden rounded-16 border border-border-faint bg-white", className, )} style={{ top: position.top, left: position.left, boxShadow: "0px 12px 24px rgba(0, 0, 0, 0.08), 0px 4px 8px rgba(0, 0, 0, 0.04)", width: matchTriggerWidth && menuWidth ? menuWidth : undefined, transform: (() => { let transforms = []; // Handle vertical transforms for top/bottom sides if (side === "top") { transforms.push("translateY(-100%)"); } // Handle horizontal transforms for left/right sides if (side === "left") { transforms.push("translateX(-100%)"); } // No transform needed for align="end" since we calculate position directly if (side === "top" || side === "bottom") { if (align === "center") { // Center alignment already handled in position calculation } } else if (side === "left" || side === "right") { if (align === "center") { transforms.push("translateY(-50%)"); } else if (align === "end") { transforms.push("translateY(-100%)"); } } return transforms.join(" ") || undefined; })(), }} {...props} > {children} )} ); }, ); MenuContent.displayName = "MenuContent"; /* ------------------------------------------------------------------------------------------------- * MenuItem * -----------------------------------------------------------------------------------------------*/ interface MenuItemProps extends React.ButtonHTMLAttributes { inset?: boolean; variant?: "default" | "danger" | "active" | "ghost"; } export const MenuItem = React.forwardRef( ( { className, inset, onClick, variant = "default", children, ...props }, ref, ) => { const { setOpen } = useMenu(); const handleClick = (e: React.MouseEvent) => { onClick?.(e); if (!e.defaultPrevented) { setOpen(false); } }; const baseStyles = "relative flex w-full cursor-pointer select-none items-center gap-8 rounded-12 px-8 py-6 min-h-[36px] text-sm outline-none transition-all data-[disabled]:pointer-events-none data-[disabled]:opacity-50 active:scale-[0.98]"; const variantStyles = { default: "text-foreground-dimmer hover:text-foreground hover:bg-transparent", danger: "text-red-600/70 hover:text-red-600 hover:bg-red-50", active: "text-heat-100 bg-heat-8 hover:bg-heat-12", ghost: "hover:bg-transparent", }; return ( ); }, ); MenuItem.displayName = "MenuItem"; /* ------------------------------------------------------------------------------------------------- * MenuLabel * -----------------------------------------------------------------------------------------------*/ interface MenuLabelProps extends React.HTMLAttributes { inset?: boolean; } export const MenuLabel = React.forwardRef( ({ className, inset, ...props }, ref) => (
), ); MenuLabel.displayName = "MenuLabel"; /* ------------------------------------------------------------------------------------------------- * MenuSeparator * -----------------------------------------------------------------------------------------------*/ export const MenuSeparator = React.forwardRef< HTMLDivElement, React.HTMLAttributes >(({ className, ...props }, ref) => (
)); MenuSeparator.displayName = "MenuSeparator"; /* ------------------------------------------------------------------------------------------------- * MenuGroup * -----------------------------------------------------------------------------------------------*/ interface MenuGroupProps extends React.HTMLAttributes { withHoverBackground?: boolean; } export const MenuGroup = React.forwardRef( ({ className, withHoverBackground = true, children, ...props }, ref) => { const { backgroundRef, onMouseEnter, onMouseLeave, onClick } = useDropdownHover(); if (withHoverBackground) { // Clone children and add hover handlers const enhancedChildren = React.Children.map(children, (child) => { if (React.isValidElement(child) && child.type === MenuItem) { const isActive = child.props.variant === "active"; const isDanger = child.props.variant === "danger"; // Don't apply hover background to active or danger items const shouldApplyHoverBg = !isActive && !isDanger; return React.cloneElement(child as React.ReactElement, { onMouseEnter: shouldApplyHoverBg ? (e: React.MouseEvent) => { onMouseEnter(e); child.props.onMouseEnter?.(e); } : child.props.onMouseEnter, onMouseLeave: shouldApplyHoverBg ? (e: React.MouseEvent) => { onMouseLeave(); child.props.onMouseLeave?.(e); } : child.props.onMouseLeave, onClick: (e: React.MouseEvent) => { if (shouldApplyHoverBg) onClick(e); child.props.onClick?.(e); }, }); } return child; }); return (
{enhancedChildren}
); } return (
{children}
); }, ); MenuGroup.displayName = "MenuGroup"; /* ------------------------------------------------------------------------------------------------- * MenuHeader * -----------------------------------------------------------------------------------------------*/ export { MenuHeader } from "./menu-header"; /* * Example Usage: * * * * * * * setOpen(false)} /> * * * * * Action Item * * * * Active Item * * * * * * Delete * * * * * * MenuGroup automatically handles hover background animations * MenuItem has built-in styling with variants: default, active, danger, ghost * All padding and spacing is handled centrally */