"use client" import { useCallback, useEffect, useRef, useLayoutEffect } from "react" import { createPortal } from "react-dom" import { X } from "lucide-react" import { cn } from "@/lib/utils" const sizeClasses = { sm: "max-w-sm", md: "max-w-md", lg: "max-w-lg", xl: "max-w-xl", full: "max-w-full w-full h-full m-0 rounded-none", } type ModalSize = keyof typeof sizeClasses export interface ModalProps { isOpen: boolean onClose: () => void title?: string description?: string size?: ModalSize children?: React.ReactNode className?: string showCloseButton?: boolean } // Use useLayoutEffect for DOM reads/writes to avoid hydration mismatches const useIsomorphicLayoutEffect = typeof window !== "undefined" ? useLayoutEffect : useEffect export function Modal({ isOpen, onClose, title, description, size = "md", children, className, showCloseButton = true, }: ModalProps) { const modalRef = useRef(null) const previousActiveElement = useRef(null) // Escape handler const handleEscape = useCallback( (e: KeyboardEvent) => { if (e.key === "Escape") { onClose() } }, [onClose], ) useEffect(() => { if (!isOpen) return document.addEventListener("keydown", handleEscape) return () => document.removeEventListener("keydown", handleEscape) }, [isOpen, handleEscape]) // Body scroll lock useEffect(() => { if (isOpen) { const originalOverflow = document.body.style.overflow document.body.style.overflow = "hidden" return () => { document.body.style.overflow = originalOverflow } } }, [isOpen]) // Focus management: store previously focused element and restore on close useIsomorphicLayoutEffect(() => { if (isOpen) { previousActiveElement.current = document.activeElement as HTMLElement // Focus the modal header or the close button for accessibility const firstFocusable = modalRef.current?.querySelector( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ) firstFocusable?.focus() return () => { previousActiveElement.current?.focus() } } }, [isOpen]) // Focus trap useEffect(() => { if (!isOpen) return const handleTab = (e: KeyboardEvent) => { if (e.key !== "Tab" || !modalRef.current) return const focusableElements = modalRef.current.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', ) const first = focusableElements[0] const last = focusableElements[focusableElements.length - 1] if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault() last?.focus() } } else { if (document.activeElement === last) { e.preventDefault() first?.focus() } } } document.addEventListener("keydown", handleTab) return () => document.removeEventListener("keydown", handleTab) }, [isOpen]) if (!isOpen) return null return createPortal(
{/* Overlay */} , document.body, ) }