2025-08-20 04:12:49 -06:00

176 lines
5.6 KiB
TypeScript

'use client';
import * as React from 'react';
import {
motion,
type SpringOptions,
useMotionValue,
useSpring,
} from 'motion/react';
import { cn } from '@workspace/ui/lib/utils';
type BubbleBackgroundProps = React.ComponentProps<'div'> & {
interactive?: boolean;
transition?: SpringOptions;
colors?: {
first: string;
second: string;
third: string;
fourth: string;
fifth: string;
sixth: string;
};
};
function BubbleBackground({
ref,
className,
children,
interactive = false,
transition = { stiffness: 100, damping: 20 },
colors = {
first: '18,113,255',
second: '221,74,255',
third: '0,220,255',
fourth: '200,50,50',
fifth: '180,180,50',
sixth: '140,100,255',
},
...props
}: BubbleBackgroundProps) {
const containerRef = React.useRef<HTMLDivElement>(null);
React.useImperativeHandle(ref, () => containerRef.current as HTMLDivElement);
const mouseX = useMotionValue(0);
const mouseY = useMotionValue(0);
const springX = useSpring(mouseX, transition);
const springY = useSpring(mouseY, transition);
React.useEffect(() => {
if (!interactive) return;
const currentContainer = containerRef.current;
if (!currentContainer) return;
const handleMouseMove = (e: MouseEvent) => {
const rect = currentContainer.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
mouseX.set(e.clientX - centerX);
mouseY.set(e.clientY - centerY);
};
currentContainer?.addEventListener('mousemove', handleMouseMove);
return () =>
currentContainer?.removeEventListener('mousemove', handleMouseMove);
}, [interactive, mouseX, mouseY]);
return (
<div
ref={containerRef}
data-slot="bubble-background"
className={cn(
'relative size-full overflow-hidden bg-gradient-to-br from-violet-900 to-blue-900',
className,
)}
{...props}
>
<style>
{`
:root {
--first-color: ${colors.first};
--second-color: ${colors.second};
--third-color: ${colors.third};
--fourth-color: ${colors.fourth};
--fifth-color: ${colors.fifth};
--sixth-color: ${colors.sixth};
}
`}
</style>
<svg
xmlns="http://www.w3.org/2000/svg"
className="absolute top-0 left-0 w-0 h-0"
>
<defs>
<filter id="goo">
<feGaussianBlur
in="SourceGraphic"
stdDeviation="10"
result="blur"
/>
<feColorMatrix
in="blur"
mode="matrix"
values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -8"
result="goo"
/>
<feBlend in="SourceGraphic" in2="goo" />
</filter>
</defs>
</svg>
<div
className="absolute inset-0"
style={{ filter: 'url(#goo) blur(40px)' }}
>
<motion.div
className="absolute rounded-full size-[80%] top-[10%] left-[10%] mix-blend-hard-light bg-[radial-gradient(circle_at_center,rgba(var(--first-color),0.8)_0%,rgba(var(--first-color),0)_50%)]"
animate={{ y: [-50, 50, -50] }}
transition={{ duration: 30, ease: 'easeInOut', repeat: Infinity }}
/>
<motion.div
className="absolute inset-0 flex justify-center items-center origin-[calc(50%-400px)]"
animate={{ rotate: 360 }}
transition={{
duration: 20,
ease: 'linear',
repeat: Infinity,
repeatType: 'loop',
}}
>
<div className="rounded-full size-[80%] top-[10%] left-[10%] mix-blend-hard-light bg-[radial-gradient(circle_at_center,rgba(var(--second-color),0.8)_0%,rgba(var(--second-color),0)_50%)]" />
</motion.div>
<motion.div
className="absolute inset-0 flex justify-center items-center origin-[calc(50%+400px)]"
animate={{ rotate: 360 }}
transition={{ duration: 40, ease: 'linear', repeat: Infinity }}
>
<div className="absolute rounded-full size-[80%] bg-[radial-gradient(circle_at_center,rgba(var(--third-color),0.8)_0%,rgba(var(--third-color),0)_50%)] mix-blend-hard-light top-[calc(50%+200px)] left-[calc(50%-500px)]" />
</motion.div>
<motion.div
className="absolute rounded-full size-[80%] top-[10%] left-[10%] mix-blend-hard-light bg-[radial-gradient(circle_at_center,rgba(var(--fourth-color),0.8)_0%,rgba(var(--fourth-color),0)_50%)] opacity-70"
animate={{ x: [-50, 50, -50] }}
transition={{ duration: 40, ease: 'easeInOut', repeat: Infinity }}
/>
<motion.div
className="absolute inset-0 flex justify-center items-center origin-[calc(50%_-_800px)_calc(50%_+_200px)]"
animate={{ rotate: 360 }}
transition={{ duration: 20, ease: 'linear', repeat: Infinity }}
>
<div className="absolute rounded-full size-[160%] mix-blend-hard-light bg-[radial-gradient(circle_at_center,rgba(var(--fifth-color),0.8)_0%,rgba(var(--fifth-color),0)_50%)] top-[calc(50%-80%)] left-[calc(50%-80%)]" />
</motion.div>
{interactive && (
<motion.div
className="absolute rounded-full size-full mix-blend-hard-light bg-[radial-gradient(circle_at_center,rgba(var(--sixth-color),0.8)_0%,rgba(var(--sixth-color),0)_50%)] opacity-70"
style={{
x: springX,
y: springY,
}}
/>
)}
</div>
{children}
</div>
);
}
export { BubbleBackground, type BubbleBackgroundProps };