Imagemodals
Imagemodals é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
UI Layouts
MIT
overlays
O que é
Imagemodals é um componente de sobreposição para React e Tailwind CSS, da biblioteca UI Layouts, com licença MIT. Copia e cola no teu projeto.
Como usar
Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.
Cinco cópias grátis por mês. Sem cartão.
Precisa de
npm i motion lucide-react
Licença
Este componente vem de UI Layouts e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.
Imagemodals
'use client';
import { transition } from '@/lib/utils';
import { XIcon } from 'lucide-react';
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
import React, { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
export default function Dialog() {
const [index, setIndex] = useState(5);
const [isOpen, setIsOpen] = useState(false);
useEffect(() => {
if (isOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isOpen]);
type Item = {
id: number;
imgSrc?: string;
videoSrc?: string;
};
const items: Item[] = [
{
id: 1,
imgSrc:
'https://images.unsplash.com/photo-1726824766931-4bd8b59af37c?q=80&w=1000&auto=format&fit=crop',
},
{
id: 2,
videoSrc: 'https://videos.pexels.com/video-files/7710243/7710243-uhd_2560_1440_30fps.mp4',
},
];
const currentItem = items[index];
const [carouselWidth, setCarouselWidth] = useState(0);
const carousel = useRef(null);
useEffect(() => {
setCarouselWidth(
// @ts-expect-error
carousel.current.scrollWidth - carousel.current.offsetWidth
);
}, [carousel]);
return (
<div className='relative h-full'>
<MotionConfig transition={transition}>
<motion.div
ref={carousel}
drag='x'
dragElastic={0.2}
dragConstraints={{ right: 0, left: -carouselWidth }}
dragTransition={{ bounceDamping: 30 }}
transition={{ duration: 8, ease: 'easeInOut' }}
className='flex w-full gap-4 py-10'
>
{items.map((item, i) => {
return (
<>
<motion.div
// @ts-expect-error
key={item}
className='w-full flex relative flex-col overflow-hidden border dark:bg-black bg-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-950'
layoutId={`dialog-${item?.id}`}
style={{
borderRadius: '12px',
}}
tabIndex={i}
onClick={() => {
setIndex(i);
setIsOpen(true);
}}
>
{item?.imgSrc && (
<motion.div layoutId={`dialog-img-${item.id}`} className='w-full h-full'>
<img
src={item.imgSrc}
alt='A desk lamp designed by Edouard Wilfrid Buquet in 1925. It features a double-arm design and is made from nickel-plated brass, aluminium and varnished wood.'
className=' w-full object-cover'
/>
</motion.div>
)}
{item?.videoSrc && (
<motion.div layoutId={`dialog-video-${item.id}`} className='w-full h-full'>
<video autoPlay muted loop className='h-full w-full object-cover rounded-xs'>
<source src={item.videoSrc!} type='video/mp4' />
</video>
</motion.div>
)}
</motion.div>
</>
);
})}
</motion.div>
<AnimatePresence initial={false} mode='sync'>
{isOpen && (
<>
<motion.div
key={`backdrop-${currentItem.id}`}
className='fixed inset-0 h-full w-full dark:bg-black/25 bg-white/95 backdrop-blur-xs '
variants={{ open: { opacity: 1 }, closed: { opacity: 0 } }}
initial='closed'
animate='open'
exit='closed'
onClick={() => {
setIsOpen(false);
}}
/>
<motion.div
key='dialog'
className='pointer-events-none fixed inset-0 flex items-center justify-center z-50'
>
<motion.div
className='pointer-events-auto relative flex flex-col overflow-hidden dark:bg-neutral-950 bg-neutral-200 border w-[80%] h-[90%] '
layoutId={`dialog-${currentItem.id}`}
tabIndex={-1}
style={{
borderRadius: '24px',
}}
>
{items[index]?.imgSrc && (
<motion.div layoutId={`dialog-img-${currentItem.id}`} className='w-full h-full'>
<img src={currentItem.imgSrc} alt='' className='h-full w-full object-cover' />
</motion.div>
)}
{items[index]?.videoSrc && (
<motion.div
layoutId={`dialog-video-${currentItem.id}`}
className='w-full h-full'
>
<video
autoPlay
muted
loop
controls
className='h-full w-full object-cover rounded-xs'
>
<source src={currentItem.videoSrc!} type='video/mp4' />
</video>
</motion.div>
)}
<button
onClick={() => setIsOpen(false)}
className='absolute right-6 top-6 p-3 text-zinc-50 cursor-pointer dark:bg-neutral-900 bg-neutral-400 hover:bg-neutral-500 rounded-full dark:hover:bg-neutral-800'
type='button'
aria-label='Close dialog'
>
<XIcon size={24} />
</button>
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
</MotionConfig>
</div>
);
}
'use client';
import { useMediaQuery } from '@/hooks/use-media-query';
import { cn } from '@/lib/utils';
import { XIcon } from 'lucide-react';
import { AnimatePresence, MotionConfig, motion } from 'motion/react';
import React, { useEffect, useId, useState } from 'react';
export const transition = {
type: 'spring',
stiffness: 300, // Increased from 80
damping: 30, // Increased from 10
mass: 0.5, // Decreased from 0.9
} as const;
interface IMediaModal {
imgSrc?: string;
videoSrc?: string;
className?: string;
}
export function MediaModal({ imgSrc, videoSrc, className }: IMediaModal) {
const [isMediaModalOpen, setIsMediaModalOpen] = useState(false);
const uniqueId = useId();
useEffect(() => {
if (isMediaModalOpen) {
document.body.classList.add('overflow-hidden');
} else {
document.body.classList.remove('overflow-hidden');
}
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsMediaModalOpen(false);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isMediaModalOpen]);
return (
<>
<MotionConfig transition={transition}>
<>
<motion.div
className='w-full h-full flex relative flex-col overflow-hidden border cursor-zoom-in dark:bg-black bg-neutral-300 hover:bg-neutral-200 dark:hover:bg-neutral-950'
layoutId={`dialog-${uniqueId}`}
style={{
borderRadius: '12px',
}}
onClick={() => {
setIsMediaModalOpen(true);
}}
>
{imgSrc && (
<motion.div layoutId={`dialog-img-${uniqueId}`} className='w-full h-full'>
<img
src={imgSrc}
alt='A desk lamp designed by Edouard Wilfrid Buquet in 1925. It features a double-arm design and is made from nickel-plated brass, aluminium and varnished wood.'
className=' w-full object-cover h-full'
/>
</motion.div>
)}
{videoSrc && (
<motion.div layoutId={`dialog-video-${uniqueId}`} className='w-full h-full'>
<video autoPlay muted loop className='h-full w-full object-cover rounded-xs'>
<source src={videoSrc!} type='video/mp4' />
</video>
</motion.div>
)}
</motion.div>
</>
<AnimatePresence initial={false} mode='popLayout'>
{isMediaModalOpen && (
<>
<motion.div
key={`backdrop-${uniqueId}`}
className='fixed inset-0 h-full w-full z-50 dark:bg-black/25 bg-white/95 backdrop-blur-xs '
variants={{ open: { opacity: 1 }, closed: { opacity: 0 } }}
initial='closed'
animate='open'
exit='closed'
onClick={() => {
setIsMediaModalOpen(false);
}}
/>
<motion.div
key='dialog'
className='pointer-events-none fixed inset-0 flex items-center justify-center z-50'
>
<motion.div
className={cn(
'pointer-events-auto relative flex flex-col overflow-hidden dark:bg-neutral-950 bg-neutral-200 border w-[80%] h-[90%]',
imgSrc && 'cursor-zoom-out'
)}
layoutId={`dialog-${uniqueId}`}
layout={isMediaModalOpen}
tabIndex={-1}
style={{
borderRadius: '24px',
}}
>
{imgSrc && (
<motion.div
layoutId={`dialog-img-${uniqueId}`}
className='w-full h-full'
onClick={() => setIsMediaModalOpen(false)}
>
<img src={imgSrc} alt='' className='h-full w-full object-cover' />
</motion.div>
)}
{videoSrc && (
<motion.div layoutId={`dialog-video-${uniqueId}`} className='w-full h-full'>
<video
autoPlay
muted
loop
controls
className='h-full w-full object-cover rounded-xs'
>
<source src={videoSrc!} type='video/mp4' />
</video>
</motion.div>
)}
{videoSrc && (
<button
onClick={() => setIsMediaModalOpen(false)}
className='absolute right-6 top-6 p-3 text-zinc-50 cursor-pointer dark:bg-neutral-900 bg-neutral-400 hover:bg-neutral-500 rounded-xl dark:hover:bg-neutral-800'
type='button'
aria-label='Close dialog'
>
<XIcon size={24} />
</button>
)}
</motion.div>
</motion.div>
</>
)}
</AnimatePresence>
</MotionConfig>
</>
);
}