"use client"; import { X } from "lucide-react"; import { type ReactNode, useEffect, useId, useRef, useState } from "react"; import { cn } from "@/lib/utils"; /** Matches the exit transition below. Closing is quicker than opening. */ const EXIT_MS = 150; export interface ModalProps { /** Whether the modal is open. */ open: boolean; /** Called when the modal asks to close, from Escape, the backdrop, or the close button. */ onOpenChange: (open: boolean) => void; /** Heading that also names the dialog for screen readers. */ title: ReactNode; /** Optional line under the title. */ description?: ReactNode; /** Buttons along the bottom edge. */ footer?: ReactNode; children?: ReactNode; className?: string; } /** * A centered dialog built on the native dialog element, so focus stays inside, * the page behind is inert, and focus returns to the trigger on close. It fades * and scales in from 0.97, and leaves a little faster than it arrived. */ export function Modal({ open, onOpenChange, title, description, footer, children, className, }: ModalProps) { const ref = useRef(null); const titleId = useId(); const descriptionId = useId(); // Drives the CSS transition. It lags one frame behind opening so the entrance can animate. const [shown, setShown] = useState(false); useEffect(() => { const dialog = ref.current; if (!dialog) return; if (open) { if (!dialog.open) dialog.showModal(); const frame = requestAnimationFrame(() => setShown(true)); return () => cancelAnimationFrame(frame); } setShown(false); if (!dialog.open) return; // Let the exit transition finish before the dialog leaves the top layer. const timeout = setTimeout(() => dialog.close(), EXIT_MS); return () => clearTimeout(timeout); }, [open]); // The native dialog does not stop the page behind it from scrolling. useEffect(() => { if (!open) return; const root = document.documentElement; const previous = root.style.overflow; root.style.overflow = "hidden"; return () => { root.style.overflow = previous; }; }, [open]); return ( { event.preventDefault(); onOpenChange(false); }} onClose={() => { if (open) onOpenChange(false); }} className="group fixed inset-0 m-0 h-dvh max-h-none w-screen max-w-none items-center justify-center overflow-hidden bg-transparent p-4 text-foreground backdrop:bg-transparent open:flex" > {children ?
{children}
: null} {footer ?
{footer}
: null}
); }