/** * Modal Component * Reusable modal overlay with animations */ 'use client'; import { useEffect, useCallback, useRef } from 'react'; import { createPortal } from 'react-dom'; import styles from './Modal.module.css'; interface ModalProps { isOpen: boolean; onClose: () => void; children: React.ReactNode; title?: string; size?: 'small' | 'medium' | 'large'; } export function Modal({ isOpen, onClose, children, title, size = 'medium' }: ModalProps) { const overlayRef = useRef(null); const handleEscape = useCallback((e: KeyboardEvent) => { if (e.key === 'Escape') { onClose(); } }, [onClose]); useEffect(() => { if (!isOpen) return; // Add event listener document.addEventListener('keydown', handleEscape); // Prevent body scroll document.body.style.overflow = 'hidden'; return () => { document.removeEventListener('keydown', handleEscape); document.body.style.overflow = 'unset'; }; }, [isOpen, handleEscape]); // Pin the overlay to the VISUAL viewport, not the layout viewport. On mobile // the on-screen keyboard shrinks the visual viewport but not the layout one, // so a `position: fixed; inset: 0` overlay keeps full height — leaving the // bottom-anchored sheet (and the dim backdrop's lower half) hidden behind // the keyboard. Tracking visualViewport.height/offsetTop keeps the whole // overlay — backdrop and sheet — inside the visible area, above the keyboard. useEffect(() => { if (!isOpen) return; const vv = typeof window !== 'undefined' ? window.visualViewport : null; const el = overlayRef.current; if (!vv || !el) return; const sync = () => { el.style.top = `${vv.offsetTop}px`; el.style.height = `${vv.height}px`; el.style.bottom = 'auto'; }; sync(); vv.addEventListener('resize', sync); vv.addEventListener('scroll', sync); return () => { vv.removeEventListener('resize', sync); vv.removeEventListener('scroll', sync); }; }, [isOpen]); if (!isOpen || typeof window === 'undefined') return null; const handleOverlayClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return createPortal(
{title &&

{title}

}
{children}
, document.body ); }