import { useState, useEffect, useCallback, useRef, type ReactNode } from 'react' import { X, Maximize2, Minimize2 } from 'lucide-react' import { cn } from '@/lib/utils' interface ModalProps { isOpen: boolean onClose: () => void title: string children: ReactNode /** Optional footer content that stays fixed at bottom (doesn't scroll) */ footer?: ReactNode size?: 'sm' | 'md' | 'lg' | 'xl' /** If true, a fullscreen toggle button appears in the modal header */ allowFullScreen?: boolean } const FOCUSABLE_SELECTOR = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' export function Modal({ isOpen, onClose, title, children, footer, size = 'md', allowFullScreen = false }: ModalProps) { const [isFullScreen, setIsFullScreen] = useState(() => { if (!allowFullScreen) return false try { return localStorage.getItem('rf-editor-fullscreen') === 'true' } catch { return false } }) const modalRef = useRef(null) const previousFocusRef = useRef(null) const toggleFullScreen = () => { const next = !isFullScreen setIsFullScreen(next) try { localStorage.setItem('rf-editor-fullscreen', String(next)) } catch { // localStorage unavailable — ignore } } // Close on Escape key const handleKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Escape') { onClose() } }, [onClose] ) // Body overflow lock + keyboard listener useEffect(() => { if (isOpen) { previousFocusRef.current = document.activeElement as HTMLElement document.addEventListener('keydown', handleKeyDown) document.body.style.overflow = 'hidden' } return () => { document.removeEventListener('keydown', handleKeyDown) document.body.style.overflow = '' } }, [isOpen, handleKeyDown]) // Focus trap: keep focus inside the modal useEffect(() => { if (!isOpen) { // Restore focus when modal closes previousFocusRef.current?.focus() return } const modal = modalRef.current if (!modal) return // Auto-focus first focusable element const timer = setTimeout(() => { const focusable = modal.querySelectorAll(FOCUSABLE_SELECTOR) if (focusable.length > 0) { focusable[0].focus() } }, 50) const trapFocus = (e: KeyboardEvent) => { if (e.key !== 'Tab') return const focusable = modal.querySelectorAll(FOCUSABLE_SELECTOR) if (focusable.length === 0) return const first = focusable[0] const last = focusable[focusable.length - 1] if (e.shiftKey && document.activeElement === first) { e.preventDefault() last.focus() } else if (!e.shiftKey && document.activeElement === last) { e.preventDefault() first.focus() } } modal.addEventListener('keydown', trapFocus) return () => { clearTimeout(timer) modal.removeEventListener('keydown', trapFocus) } }, [isOpen]) if (!isOpen) return null const sizeClasses = { sm: 'max-w-sm', md: 'max-w-md', lg: 'max-w-full sm:max-w-lg', xl: 'max-w-full sm:max-w-4xl', } return (
{/* Backdrop */} ) } export default Modal