logo

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