Ripple Container
Container flexível com animação de ripple (útil também para botões)
Código:
'use client';
import { AnimatePresence, motion } from 'motion/react';
import React, { useCallback, useRef, useState } from 'react';
interface RippleProps {
x: number;
y: number;
size: number;
id: number;
}
interface RippleContainerProps {
color?: string;
children: React.ReactElement<any, any>;
}
export function RippleContainer({ color = 'rgba(56, 56, 56, 0.4)', children }: RippleContainerProps) {
const [ripples, setRipples] = useState<RippleProps[]>([]);
const containerRef = useRef<HTMLElement>(null);
const rippleCount = useRef(0);
const addRipple = useCallback((event: React.MouseEvent<HTMLElement>) => {
const container = containerRef.current;
if (!container) return;
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const size = Math.max(rect.width, rect.height) * 2;
const id = rippleCount.current;
rippleCount.current += 1;
setRipples((prev) => [...prev, { x, y, size, id }]);
setTimeout(() => {
setRipples((prev) => prev.filter((r) => r.id !== id));
}, 850);
}, []);
const mergeClasses = (childClassName?: string, extraClassName?: string) =>
[childClassName, extraClassName].filter(Boolean).join(' ');
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
if (children.props.onClick) {
children.props.onClick(event);
}
addRipple(event);
};
// Aqui forçamos o type do children para garantir que temos acesso a children.props
const child = children as React.ReactElement<any, any>;
return React.cloneElement(child, {
ref: containerRef,
onClick: handleClick,
className: mergeClasses(child.props.className, 'relative overflow-hidden'),
children: (
<>
{child.props.children}
<AnimatePresence>
{ripples.map((ripple) => (
<motion.span
key={ripple.id}
initial={{
width: 0,
height: 0,
opacity: 0.5,
top: ripple.y,
left: ripple.x,
transform: 'translate(-50%, -50%)',
}}
animate={{
width: ripple.size,
height: ripple.size,
opacity: 0,
top: ripple.y,
left: ripple.x,
transform: 'translate(-50%, -50%)',
}}
exit={{ opacity: 0 }}
transition={{ duration: 0.85, ease: 'easeOut' }}
style={{
position: 'absolute',
borderRadius: '50%',
backgroundColor: color,
pointerEvents: 'none',
}}
/>
))}
</AnimatePresence>
</>
),
});
}
yarn add motion
Prévia:
Eu sou uma div