"use client"; import { AnimatePresence, motion } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { modalContentVariants, modalOverlayVariants } from "@/lib/motion"; export type ConfirmDialogProps = { open: boolean; onOpenChange: (open: boolean) => void; title: string; description: string; confirmLabel?: string; cancelLabel?: string; variant?: "danger" | "default"; loading?: boolean; onConfirm: () => void | Promise; }; /** * Portal-based confirm modal (not Dialog.Root) so it can safely stack * above Visual Manager / other Base UI dialogs without nested-root bugs. */ export function ConfirmDialog({ open, onOpenChange, title, description, confirmLabel = "Confirm", cancelLabel = "Cancel", variant = "danger", loading, onConfirm, }: ConfirmDialogProps) { const [busy, setBusy] = useState(false); const [mounted, setMounted] = useState(false); const isLoading = loading ?? busy; useEffect(() => setMounted(true), []); useEffect(() => { if (!open) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape" && !isLoading) onOpenChange(false); }; document.addEventListener("keydown", onKey); return () => document.removeEventListener("keydown", onKey); }, [open, isLoading, onOpenChange]); async function handleConfirm() { setBusy(true); try { await onConfirm(); onOpenChange(false); } finally { setBusy(false); } } if (!mounted) return null; const isOpen = open; return createPortal( {isOpen ? ( ) : null} , document.body, ); } export type ConfirmOptions = { title: string; description: string; confirmLabel?: string; cancelLabel?: string; variant?: "danger" | "default"; }; /** Imperative confirm — returns a Promise. Render `{dialog}` in the tree. */ export function useConfirmDialog() { const [opts, setOpts] = useState<(ConfirmOptions & { open: boolean }) | null>( null, ); const resolver = useRef<((value: boolean) => void) | null>(null); const confirm = useCallback((options: ConfirmOptions) => { return new Promise((resolve) => { resolver.current = resolve; setOpts({ ...options, open: true }); }); }, []); const settle = useCallback((value: boolean) => { const resolve = resolver.current; resolver.current = null; setOpts(null); resolve?.(value); }, []); const dialog = opts ? ( { if (!open) settle(false); }} title={opts.title} description={opts.description} confirmLabel={opts.confirmLabel} cancelLabel={opts.cancelLabel} variant={opts.variant} onConfirm={() => settle(true)} /> ) : null; return { confirm, dialog }; }