Spaces:
Sleeping
Sleeping
| import { useEffect } from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { HiX } from 'react-icons/hi'; | |
| import './Modal.css'; | |
| /** | |
| * Modal component with overlay, animation, and close behavior | |
| */ | |
| export default function Modal({ isOpen, onClose, title, children, size = 'md', showClose = true }) { | |
| // Prevent body scroll when modal is open | |
| useEffect(() => { | |
| if (isOpen) { | |
| document.body.style.overflow = 'hidden'; | |
| } else { | |
| document.body.style.overflow = ''; | |
| } | |
| return () => { document.body.style.overflow = ''; }; | |
| }, [isOpen]); | |
| // Close on Escape | |
| useEffect(() => { | |
| const handleEsc = (e) => e.key === 'Escape' && onClose(); | |
| if (isOpen) window.addEventListener('keydown', handleEsc); | |
| return () => window.removeEventListener('keydown', handleEsc); | |
| }, [isOpen, onClose]); | |
| return ( | |
| <AnimatePresence> | |
| {isOpen && ( | |
| <div className="modal-overlay" onClick={onClose}> | |
| <motion.div | |
| className={`modal modal--${size}`} | |
| initial={{ opacity: 0, scale: 0.9, y: 20 }} | |
| animate={{ opacity: 1, scale: 1, y: 0 }} | |
| exit={{ opacity: 0, scale: 0.9, y: 20 }} | |
| transition={{ type: 'spring', damping: 25, stiffness: 300 }} | |
| onClick={(e) => e.stopPropagation()} | |
| > | |
| {(title || showClose) && ( | |
| <div className="modal__header"> | |
| {title && <h3 className="modal__title">{title}</h3>} | |
| {showClose && ( | |
| <button className="modal__close" onClick={onClose} aria-label="Close modal"> | |
| <HiX size={20} /> | |
| </button> | |
| )} | |
| </div> | |
| )} | |
| <div className="modal__body"> | |
| {children} | |
| </div> | |
| </motion.div> | |
| </div> | |
| )} | |
| </AnimatePresence> | |
| ); | |
| } | |