"use client"; import * as React from "react"; import { cn } from "./lib/utils"; interface PopoverContextValue { open: boolean; setOpen: (open: boolean) => void; triggerRef: React.RefObject; } const PopoverCtx = React.createContext(null); export interface PopoverProps { open?: boolean; defaultOpen?: boolean; onOpenChange?: (open: boolean) => void; children: React.ReactNode; } export function Popover({ open: controlledOpen, defaultOpen = false, onOpenChange, children, }: PopoverProps) { const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen); const isControlled = controlledOpen !== undefined; const open = isControlled ? controlledOpen : uncontrolledOpen; const triggerRef = React.useRef(null); const setOpen = React.useCallback( (nextOpen: boolean) => { if (!isControlled) { setUncontrolledOpen(nextOpen); } onOpenChange?.(nextOpen); }, [isControlled, onOpenChange] ); return (
{children}
); } export interface PopoverTriggerProps { children: React.ReactElement<{ onClick?: React.MouseEventHandler; "aria-haspopup"?: string; "aria-expanded"?: boolean; }>; } export function PopoverTrigger({ children }: PopoverTriggerProps) { const ctx = React.useContext(PopoverCtx); if (!ctx) throw new Error("PopoverTrigger must be used within Popover"); return React.cloneElement(children, { "aria-haspopup": "dialog", "aria-expanded": ctx.open, onClick: (e: React.MouseEvent) => { children.props.onClick?.(e); ctx.setOpen(!ctx.open); }, }); } export interface PopoverContentProps extends React.HTMLAttributes { align?: "start" | "center" | "end"; children: React.ReactNode; className?: string; /** Dual-mode: false renders instantly with no motion. Default true. */ animated?: boolean; } export function PopoverContent({ align = "center", children, className, animated = true, ...props }: PopoverContentProps) { const ctx = React.useContext(PopoverCtx); const contentRef = React.useRef(null); React.useEffect(() => { if (!ctx?.open) return; function handleClickOutside(event: MouseEvent) { if (contentRef.current && !contentRef.current.contains(event.target as Node)) { ctx?.setOpen(false); } } function handleKeyDown(event: KeyboardEvent) { if (event.key === "Escape") { ctx?.setOpen(false); } } document.addEventListener("mousedown", handleClickOutside); document.addEventListener("keydown", handleKeyDown); return () => { document.removeEventListener("mousedown", handleClickOutside); document.removeEventListener("keydown", handleKeyDown); }; }, [ctx]); if (!ctx?.open) return null; return (
{children}
); }