"use client"; import { useEffect, useId, useRef, useState, type KeyboardEvent, type ReactNode } from "react"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; type DialogMorphProps = { /** Button label. */ trigger: ReactNode; /** Dialog heading. Also its name for screen readers. */ title: string; /** Small text under the heading. */ description?: string; /** Dialog body. Pass a function to get a close() for your own buttons. */ children?: ReactNode | ((close: () => void) => ReactNode); /** Keep the dialog inside the nearest positioned parent instead of the screen. */ contained?: boolean; /** Replaces the default button look. */ triggerClassName?: string; /** Replaces the default dialog look. */ className?: string; }; const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; const MORPH = { type: "spring" as const, stiffness: 380, damping: 34 }; /* DialogMorph: the button itself grows into the dialog and shrinks back into place on close. Focus stays trapped inside while open and returns to the button after. */ export function DialogMorph({ trigger, title, description, children, contained = false, triggerClassName = "inline-flex items-center gap-2 bg-[#a78bfa] px-5 py-2.5 text-sm font-medium text-[#1a1530] outline-none focus-visible:ring-2 focus-visible:ring-white/70 focus-visible:ring-offset-2 focus-visible:ring-offset-[#0b0a14]", className = "border border-white/10 bg-[#141026] p-6 text-white shadow-2xl shadow-black/60", }: DialogMorphProps) { const reduce = useReducedMotion(); const id = useId(); const button = useRef(null); const panel = useRef(null); const wasOpen = useRef(false); const [open, setOpen] = useState(false); const close = () => setOpen(false); const shared = reduce ? { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 } } : { layoutId: `morph-${id}` }; // Focus moves in on open and back to the button on close. useEffect(() => { if (open) (panel.current?.querySelector(FOCUSABLE) ?? panel.current)?.focus({ preventScroll: true }); else if (wasOpen.current) button.current?.focus({ preventScroll: true }); wasOpen.current = open; }, [open]); // Lock page scroll while open. useEffect(() => { if (!open || contained) return; const prev = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = prev; }; }, [open, contained]); const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") return close(); if (e.key !== "Tab" || !panel.current) return; const items = [...panel.current.querySelectorAll(FOCUSABLE)]; const edge = e.shiftKey ? items[0] : items[items.length - 1]; if (document.activeElement === edge || document.activeElement === panel.current) { e.preventDefault(); (e.shiftKey ? items[items.length - 1] : items[0])?.focus(); } }; return ( <> {open ? ( {trigger} ) : ( setOpen(true)} className={triggerClassName} > {trigger} )} {open && (

{title}

{description && (

{description}

)} {children &&
{typeof children === "function" ? children(close) : children}
}
)}
); }