UNPKG

lightswind

Version:

A collection of beautifully crafted React Components, Blocks & Templates for Modern Developers. Create stunning web applications effortlessly by using our 160+ professional and animated react components.

316 lines (315 loc) 14.8 kB
import { jsx as _jsx } from "react/jsx-runtime"; // @ts-nocheck import * as React from "react"; import ReactDOM from "react-dom"; import { cn } from "../../lib/utils"; import { cva } from "class-variance-authority"; import { motion, AnimatePresence } from "framer-motion"; const DropdownMenuContext = React.createContext(undefined); const DropdownMenu = ({ children, defaultOpen = false, open: controlledOpen, onOpenChange, hoverMode = false, }) => { 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) => { if (!isControlled) { setUncontrolledOpen(value); } else if (onOpenChange) { const nextOpen = typeof value === "function" ? value(controlledOpen ?? false) : value; onOpenChange(nextOpen); } }, [isControlled, onOpenChange, controlledOpen]); // Notify parent of uncontrolled state changes — outside state updaters to avoid Strict Mode issues React.useEffect(() => { if (!isControlled && onOpenChange) { onOpenChange(uncontrolledOpen); } }, [uncontrolledOpen, isControlled, onOpenChange]); const timeoutRef = React.useRef(null); React.useEffect(() => { return () => { if (timeoutRef.current) clearTimeout(timeoutRef.current); }; }, []); return (_jsx(DropdownMenuContext.Provider, { value: { open: open || false, setOpen, hoverMode, triggerRef, timeoutRef }, children: children })); }; const DropdownMenuTrigger = React.forwardRef(({ children, asChild, ...props }, ref) => { const context = React.useContext(DropdownMenuContext); if (!context) throw new Error("DropdownMenuTrigger must be used within a DropdownMenu"); const { setOpen, hoverMode, triggerRef, timeoutRef } = context; const handleClick = (e) => { e.stopPropagation(); if (hoverMode) { // In hover mode, click should only open (not toggle), to avoid // fighting with the hover timers on first interaction if (timeoutRef.current) clearTimeout(timeoutRef.current); setOpen(true); } else { setOpen((prev) => !prev); } if (props.onClick) props.onClick(e); }; React.useImperativeHandle(ref, () => { if (!triggerRef.current) return document.createElement("button"); return triggerRef.current; }, [triggerRef]); const handleMouseEnter = (e) => { if (hoverMode) { // Always clear any pending timer (open or close) if (timeoutRef.current) clearTimeout(timeoutRef.current); // Schedule open — setOpen(true) is idempotent if already open timeoutRef.current = setTimeout(() => setOpen(true), 150); } if (props.onMouseEnter) props.onMouseEnter(e); }; const handleMouseLeaveTrigger = (e) => { if (hoverMode) { if (timeoutRef.current) clearTimeout(timeoutRef.current); timeoutRef.current = setTimeout(() => setOpen(false), 200); } if (props.onMouseLeave) props.onMouseLeave(e); }; const { onClick, onMouseEnter, onMouseLeave, ...otherProps } = props; if (asChild) { const child = React.Children.only(children); return React.cloneElement(child, { ...child.props, ref: (node) => { triggerRef.current = node; if (typeof ref === "function") ref(node); else if (ref) ref.current = node; // Handle child's original ref const childRef = child.ref; if (childRef) { if (typeof childRef === "function") childRef(node); else if (childRef.hasOwnProperty("current")) childRef.current = node; } }, onClick: (e) => { handleClick(e); if (child.props.onClick) child.props.onClick(e); }, onMouseEnter: (e) => { handleMouseEnter(e); if (child.props.onMouseEnter) child.props.onMouseEnter(e); }, // MouseLeave on the trigger is handled by the dropdown content to avoid premature close. // No onMouseLeave here. ...otherProps, }); } return (_jsx("button", { ref: (node) => { triggerRef.current = node; if (typeof ref === "function") ref(node); else if (ref) ref.current = node; }, type: "button", onClick: handleClick, onMouseEnter: handleMouseEnter, ...otherProps, children: children })); }); DropdownMenuTrigger.displayName = "DropdownMenuTrigger"; const dropdownMenuContentVariants = cva("z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md", { variants: { variant: { default: "", contextMenu: "min-w-0", }, }, defaultVariants: { variant: "default", }, }); const DropdownMenuContent = React.forwardRef(({ className, children, align = "center", alignOffset = 0, side = "bottom", sideOffset = 4, variant, ...props }, ref) => { const context = React.useContext(DropdownMenuContext); if (!context) throw new Error("DropdownMenuContent must be used within a DropdownMenu"); const { open, setOpen, hoverMode, triggerRef } = context; const menuRef = React.useRef(null); const [position, setPosition] = React.useState({ top: 0, left: 0 }); const [mounted, setMounted] = React.useState(false); const [positioned, setPositioned] = React.useState(false); React.useEffect(() => { setMounted(true); }, []); // Reset positioned to false ONLY when closed, avoids flickering when children change React.useEffect(() => { if (!open) { setPositioned(false); } }, [open]); // Body scroll lock removed // Previous logic was interfering with page navigation // Close on click outside React.useEffect(() => { if (!open) return; const handleClickOutside = (e) => { if (menuRef.current && !menuRef.current.contains(e.target) && triggerRef.current && !triggerRef.current.contains(e.target)) { setOpen(false); } }; document.addEventListener("mousedown", handleClickOutside); return () => document.removeEventListener("mousedown", handleClickOutside); }, [open, setOpen, triggerRef]); // Position updates React.useEffect(() => { if (!open || !triggerRef.current) return; const updatePosition = () => { if (!triggerRef.current) return; const triggerRect = triggerRef.current.getBoundingClientRect(); let menuRect = menuRef.current?.getBoundingClientRect(); if (!menuRect) { // Temporary render to get size if not yet rendered // This part in a Portal scenario is tricky because ref might be null initially. // We'll rely on a second pass or basic estimation if ref is null, // but Framer Motion should attach ref quickly. // For simplicity in this fix, if no menuRect, we assume a default width/height or wait for next tick. if (!menuRef.current) return; menuRect = menuRef.current.getBoundingClientRect(); } let top = 0; let left = 0; // Basic positioning logic tailored for Portal (relative to viewport) if (side === "bottom") { top = triggerRect.bottom + sideOffset; } else if (side === "top") { top = triggerRect.top - (menuRect?.height || 0) - sideOffset; } else if (side === "left" || side === "right") { top = triggerRect.top + triggerRect.height / 2 - (menuRect?.height || 0) / 2; } if (side === "right") { left = triggerRect.right + sideOffset; } else if (side === "left") { left = triggerRect.left - (menuRect?.width || 0) - sideOffset; } else { if (align === "start") left = triggerRect.left + alignOffset; else if (align === "center") left = triggerRect.left + triggerRect.width / 2 - (menuRect?.width || 0) / 2 + alignOffset; else if (align === "end") left = triggerRect.right - (menuRect?.width || 0) - alignOffset; } // Viewport collision detection const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; if (left + (menuRect?.width || 0) > windowWidth) left = windowWidth - (menuRect?.width || 0) - 8; if (left < 8) left = 8; if (top + (menuRect?.height || 0) > windowHeight) { if (side === "bottom" && triggerRect.top > (menuRect?.height || 0) + sideOffset) { top = triggerRect.top - (menuRect?.height || 0) - sideOffset; } else { const maxHeight = windowHeight - top - 8; if (menuRef.current) menuRef.current.style.maxHeight = `${maxHeight}px`; } } setPosition({ top, left }); }; // Run update immediately and on scroll/resize // Use rAF to ensure the DOM has painted the portal content before measuring const raf = requestAnimationFrame(() => { updatePosition(); setPositioned(true); }); window.addEventListener("scroll", updatePosition, true); window.addEventListener("resize", updatePosition); // Also update after a short delay to ensure Framer Motion has rendered the initial frame const timeout = setTimeout(() => { updatePosition(); setPositioned(true); }, 0); return () => { window.removeEventListener("scroll", updatePosition, true); window.removeEventListener("resize", updatePosition); cancelAnimationFrame(raf); clearTimeout(timeout); }; }, [open, align, alignOffset, side, sideOffset, triggerRef, children, variant, className, mounted]); if (!mounted) return null; return ReactDOM.createPortal(_jsx(AnimatePresence, { children: open && (_jsx(motion.div, { ref: (node) => { if (typeof ref === "function") ref(node); else if (ref) ref.current = node; menuRef.current = node; }, className: cn(dropdownMenuContentVariants({ variant }), "dropdown-scrollbar", className), style: { position: "fixed", top: `${position.top}px`, left: `${position.left}px`, zIndex: 99999, maxHeight: "calc(90vh - 60px)", overflowY: "auto", transformOrigin: side === "bottom" ? "top center" : side === "top" ? "bottom center" : side === "left" ? "center right" : "center left", }, initial: { opacity: 0, scale: 0.9, y: side === "bottom" ? -4 : side === "top" ? 4 : 0 }, animate: positioned ? { opacity: 1, scale: 1, y: 0, pointerEvents: "auto" } : { opacity: 0, scale: 0.9, pointerEvents: "none" }, exit: { opacity: 0, scale: 0.9, y: side === "bottom" ? -4 : side === "top" ? 4 : 0, transition: { duration: 0.15 }, pointerEvents: "none" }, transition: { type: "spring", damping: 20, stiffness: 300 }, onMouseEnter: (e) => { if (hoverMode && context.timeoutRef.current) { clearTimeout(context.timeoutRef.current); } if (props.onMouseEnter) props.onMouseEnter(e); }, onMouseLeave: (e) => { if (hoverMode) { context.timeoutRef.current = setTimeout(() => setOpen(false), 200); } if (props.onMouseLeave) props.onMouseLeave(e); }, "data-lenis-prevent": true, ...props, children: children })) }), document.body); }); DropdownMenuContent.displayName = "DropdownMenuContent"; const DropdownMenuLabel = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("px-2 py-1.5 text-sm font-semibold", className), ...props }))); DropdownMenuLabel.displayName = "DropdownMenuLabel"; const DropdownMenuItem = React.forwardRef(({ className, inset, asChild, disabled = false, ...props }, ref) => { const context = React.useContext(DropdownMenuContext); if (!context) throw new Error("DropdownMenuItem must be used within a DropdownMenu"); const { setOpen } = context; const handleClick = (e) => { if (disabled) { e.preventDefault(); return; } setOpen(false); if (props.onClick) props.onClick(e); }; return (_jsx("div", { ref: ref, className: cn("relative flex gap-1 cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", inset && "pl-8", className), onClick: handleClick, "data-disabled": disabled ? "" : undefined, ...props })); }); DropdownMenuItem.displayName = "DropdownMenuItem"; const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("-mx-1 my-1 h-px bg-muted", className), ...props }))); DropdownMenuSeparator.displayName = "DropdownMenuSeparator"; const DropdownMenuGroup = React.forwardRef(({ className, ...props }, ref) => (_jsx("div", { ref: ref, className: cn("space-y-1", className), ...props }))); DropdownMenuGroup.displayName = "DropdownMenuGroup"; export { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuLabel, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuGroup, };