83 lines
2.6 KiB
TypeScript
83 lines
2.6 KiB
TypeScript
import React from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { cn } from '../../lib/cn';
|
|
import { useClickOutside } from '../../hooks/useClickOutside';
|
|
|
|
type ModalBackdrop = 'opaque' | 'blur';
|
|
|
|
interface ModalProps {
|
|
className?: string;
|
|
children?: React.ReactNode;
|
|
backdrop?: ModalBackdrop;
|
|
open: boolean;
|
|
defaultOpen?: boolean;
|
|
onOpenChange: (state: boolean) => void;
|
|
}
|
|
|
|
const modalbgVariants = {
|
|
closed: { opacity: 0, backdropFilter: 'blur(0px)' },
|
|
open: { opacity: 1, backdropFilter: 'blur(6px)' },
|
|
};
|
|
|
|
const modalVariants = {
|
|
closed: { opacity: 0, scale: 0.9 },
|
|
open: { opacity: 1, scale: 1 },
|
|
};
|
|
|
|
export const Modal: React.FC<ModalProps> = ({
|
|
children,
|
|
open,
|
|
backdrop,
|
|
className,
|
|
onOpenChange,
|
|
}) => {
|
|
const ref = React.useRef<HTMLDivElement>(null);
|
|
|
|
useClickOutside(ref, () => {
|
|
onOpenChange(false);
|
|
});
|
|
|
|
return (
|
|
<div>
|
|
<AnimatePresence>
|
|
{open && (
|
|
<motion.div
|
|
initial={modalbgVariants.closed}
|
|
animate={modalbgVariants.open}
|
|
exit={modalbgVariants.closed}
|
|
transition={{ duration: 0.15 }}
|
|
className={cn(
|
|
'fixed top-0 left-0 h-svh w-svw z-50',
|
|
backdrop === 'opaque' && 'bg-[#00000055]',
|
|
)}
|
|
style={
|
|
backdrop === 'blur'
|
|
? undefined
|
|
: { backdropFilter: 'none' }
|
|
}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
<div className="fixed top-0 left-0 h-svh w-svw flex items-center justify-center pointer-events-none z-50">
|
|
<AnimatePresence>
|
|
{open && (
|
|
<motion.div
|
|
ref={ref}
|
|
className={cn(
|
|
'h-fit w-fit rounded-md pointer-events-auto',
|
|
className,
|
|
)}
|
|
initial={modalVariants.closed}
|
|
animate={modalVariants.open}
|
|
exit={modalVariants.closed}
|
|
transition={{ duration: 0.15 }}
|
|
>
|
|
{children}
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|