import { useEffect, type ReactNode, type FormEvent, Children, isValidElement, cloneElement, } from "react"; import { motion, AnimatePresence } from "framer-motion"; import useModalLock from "../hooks/useModalLock"; import useReducedMotion from "../hooks/useReducedMotion"; export interface FormModalProps { isOpen: boolean; onClose: () => void; onSubmit?: () => void; // called when user clicks primary button (only if provided) title: string; /** Optional muted line shown under the title in the header. */ subtitle?: ReactNode; children: ReactNode; // modal body (form fields) submitLabel?: string; // primary button text cancelLabel?: string; // cancel button text loading?: boolean; /** Disable the primary (submit) button — e.g. until the form is valid. * Keeps the button visibly greyed out instead of clickable-but-inert. */ submitDisabled?: boolean; hideFooter?: boolean; // for "view-only" modals size?: "sm" | "md" | "lg"; // optional closeOnBackdrop?: boolean; // default true closeOnEsc?: boolean; // default true /** Slot for footer-left actions (e.g. delete). Rendered to the LEFT of * the standard cancel/submit group in the modal footer. */ footerLeft?: ReactNode; /** Hide the close × in the header. Default false. */ hideCloseButton?: boolean; } export default function FormModal({ isOpen, onClose, onSubmit, title, subtitle, children, submitLabel = "Uložit", cancelLabel = "Zrušit", loading = false, submitDisabled = false, hideFooter = false, size = "md", closeOnBackdrop = true, closeOnEsc = true, footerLeft, hideCloseButton = false, }: FormModalProps) { useModalLock(isOpen); const reducedMotion = useReducedMotion(); useEffect(() => { if (!isOpen || !closeOnEsc) return; function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape" && !loading) { onClose(); } } window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [isOpen, closeOnEsc, loading, onClose]); const handleBackdropClick = () => { if (closeOnBackdrop && !loading) onClose(); }; const handlePrimaryClick = (e: FormEvent) => { e.preventDefault(); if (onSubmit) onSubmit(); }; const titleId = "form-modal-title"; const modalClassName = size === "lg" ? "admin-modal admin-modal-lg" : "admin-modal"; // Snappier ease curve for modal entry — matches the "industrial/refined" // tone. Roughly equivalent to the cubic-bezier(0.16, 1, 0.3, 1) used by // a lot of design systems for "expo out" feel. const ease = [0.16, 1, 0.3, 1] as const; const duration = reducedMotion ? 0 : 0.22; // Stagger the body children slightly for a "drawing-in" reveal. We cap // the cascade so long forms don't drag on forever. const childrenArray = Children.toArray(children).filter(isValidElement); const stagger = reducedMotion ? 0 : Math.min(childrenArray.length, 5) * 0.03; return ( {isOpen && (

{title}

{subtitle &&

{subtitle}

}
{!hideCloseButton && ( )}
{reducedMotion || stagger === 0 ? children : Children.map(children, (child, i) => { if (!isValidElement(child)) return child; return ( {cloneElement(child)} ); })}
{!hideFooter && (
{footerLeft}
{onSubmit && ( )}
)}
)} ); }