"use client"; import React, { useEffect, useRef, useState, useCallback } from "react"; import { cn } from "../../lib/utils"; // Assuming this is a utility like clsx or classnames export interface ParticleConfig { x: number; y: number; vx: number; vy: number; scale: number; rotation: number; rotationDirection: string; siner: number; steps: number; friction: number; element: Element | null; } export interface AnimatedBubbleParticlesProps { /** Container class name */ className?: string; /** Background color (used if no background class provided in className) */ backgroundColor?: string; // Keep this prop for fallback /** Particle color */ particleColor?: string; /** Particle size in pixels */ particleSize?: number; /** Spawn interval in milliseconds */ spawnInterval?: number; /** Container height */ height?: string; // This can be '100vh', '500px', etc. /** Container width */ width?: string; // This can be '100vw', 'full', '80%', '500px', etc. /** Enable gooey blur effect */ enableGooEffect?: boolean; /** Blur strength for goo effect */ blurStrength?: number; /** Pause animation when window is not focused */ pauseOnBlur?: boolean; /** Z-index for layering */ zIndex?: number; /** Friction range for particle movement */ friction?: { min: number; max: number }; /** Scale range for particles */ scaleRange?: { min: number; max: number }; /** Content to be displayed inside the particle container */ children?: React.ReactNode; } const AnimatedBubbleParticles: React.FC = ({ className, backgroundColor = "#edf3f8", // Default fallback background color particleColor = "#3e82f7", particleSize = 30, spawnInterval = 180, height = "100vh", // Default to 100vh for full viewport height width = "100vw", // Default to 100vw for full viewport width enableGooEffect = true, blurStrength = 12, pauseOnBlur = true, zIndex = 1, friction = { min: 1, max: 2 }, scaleRange = { min: 0.4, max: 2.4 }, children, }) => { const containerRef = useRef(null); const particlesRef = useRef(null); const animationRef = useRef(); const intervalRef = useRef(); const particlesArrayRef = useRef([]); const isPausedRef = useRef(false); const gooIdRef = useRef('goo-' + Math.random().toString(36).substring(2, 11)); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const createParticleElement = useCallback(() => { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.style.cssText = "display: block;" + "width: " + particleSize + "px;" + "height: " + particleSize + "px;" + "position: absolute;" + "transform: translateZ(0px);"; svg.setAttribute("viewBox", "0 0 67.4 67.4"); const circle = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); circle.setAttribute("cx", "33.7"); circle.setAttribute("cy", "33.7"); circle.setAttribute("r", "33.7"); circle.setAttribute("fill", particleColor); svg.appendChild(circle); return svg; }, [particleSize, particleColor]); const createParticle = useCallback((): ParticleConfig => { const element = createParticleElement(); if (particlesRef.current) { particlesRef.current.appendChild(element); } const x = Math.random() * dimensions.width; const y = dimensions.height + 100; const steps = dimensions.height / 2; const frictionValue = friction.min + Math.random() * (friction.max - friction.min); const scale = scaleRange.min + Math.random() * (scaleRange.max - scaleRange.min); const siner = (dimensions.width / 2.5) * Math.random(); const rotationDirection = Math.random() > 0.5 ? "+" : "-"; element.style.transform = "translateX(" + x + "px) translateY(" + y + "px)"; return { x, y, vx: 0, vy: 0, scale, rotation: 0, rotationDirection, siner, steps, friction: frictionValue, element, }; }, [createParticleElement, dimensions, friction, scaleRange]); const updateParticle = (particle: ParticleConfig): boolean => { particle.y -= particle.friction; const left = particle.x + Math.sin((particle.y * Math.PI) / particle.steps) * particle.siner; const top = particle.y; const rotation = particle.rotationDirection + (particle.y + particleSize); if (particle.element) { const element = particle.element as SVGElement; element.style.transform = "translateX(" + left + "px) translateY(" + top + "px) scale(" + particle.scale + ") rotate(" + rotation + "deg)"; } if (particle.y < -particleSize) { if (particle.element && particle.element.parentNode) { particle.element.parentNode.removeChild(particle.element); } return false; } return true; }; const animate = useCallback(() => { if (isPausedRef.current) { animationRef.current = requestAnimationFrame(animate); return; } particlesArrayRef.current = particlesArrayRef.current.filter(updateParticle); animationRef.current = requestAnimationFrame(animate); }, []); const spawnParticle = useCallback(() => { if (!isPausedRef.current && dimensions.width > 0 && dimensions.height > 0) { const particle = createParticle(); particlesArrayRef.current.push(particle); } }, [dimensions, createParticle]); useEffect(() => { const updateDimensions = () => { if (containerRef.current) { const rect = containerRef.current.getBoundingClientRect(); setDimensions({ width: rect.width, height: rect.height }); } }; updateDimensions(); window.addEventListener("resize", updateDimensions); return () => window.removeEventListener("resize", updateDimensions); }, []); useEffect(() => { if (!pauseOnBlur) return; const handleBlur = () => { isPausedRef.current = true; }; const handleFocus = () => { isPausedRef.current = false; }; window.addEventListener("blur", handleBlur); window.addEventListener("focus", handleFocus); return () => { window.removeEventListener("blur", handleBlur); window.removeEventListener("focus", handleFocus); }; }, [pauseOnBlur]); useEffect(() => { if (dimensions.width > 0 && dimensions.height > 0) { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } if (intervalRef.current) { clearInterval(intervalRef.current); } animationRef.current = requestAnimationFrame(animate); intervalRef.current = window.setInterval(spawnParticle, spawnInterval); } return () => { if (animationRef.current) { cancelAnimationFrame(animationRef.current); } if (intervalRef.current) { clearInterval(intervalRef.current); } particlesArrayRef.current.forEach((particle) => { if (particle.element && particle.element.parentNode) { particle.element.parentNode.removeChild(particle.element); } }); particlesArrayRef.current = []; }; }, [dimensions, spawnInterval, animate, spawnParticle]); // Determine the background class to apply const backgroundClass = (() => { // Check if a 'bg-' class is already present in the user-provided className if (className && className.split(' ').some(cls => cls.startsWith('bg-'))) { return ''; // User provided a background class, don't add default } // If no 'bg-' class is found, use the backgroundColor prop as a Tailwind arbitrary value return `bg-[${backgroundColor}]`; })(); return (
{children}
{enableGooEffect && ( )}
); }; export { AnimatedBubbleParticles }; export default AnimatedBubbleParticles;