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

162 lines
3.8 KiB
TypeScript

'use client';
import * as React from 'react';
import {
type HTMLMotionProps,
motion,
type SpringOptions,
type Transition,
useMotionValue,
useSpring,
} from 'motion/react';
import { cn } from '@workspace/ui/lib/utils';
type StarLayerProps = HTMLMotionProps<'div'> & {
count: number;
size: number;
transition: Transition;
starColor: string;
};
function generateStars(count: number, starColor: string) {
const shadows: string[] = [];
for (let i = 0; i < count; i++) {
const x = Math.floor(Math.random() * 4000) - 2000;
const y = Math.floor(Math.random() * 4000) - 2000;
shadows.push(`${x}px ${y}px ${starColor}`);
}
return shadows.join(', ');
}
function StarLayer({
count = 1000,
size = 1,
transition = { repeat: Infinity, duration: 50, ease: 'linear' },
starColor = '#fff',
className,
...props
}: StarLayerProps) {
const [boxShadow, setBoxShadow] = React.useState<string>('');
React.useEffect(() => {
setBoxShadow(generateStars(count, starColor));
}, [count, starColor]);
return (
<motion.div
data-slot="star-layer"
animate={{ y: [0, -2000] }}
transition={transition}
className={cn('absolute top-0 left-0 w-full h-[2000px]', className)}
{...props}
>
<div
className="absolute bg-transparent rounded-full"
style={{
width: `${size}px`,
height: `${size}px`,
boxShadow: boxShadow,
}}
/>
<div
className="absolute bg-transparent rounded-full top-[2000px]"
style={{
width: `${size}px`,
height: `${size}px`,
boxShadow: boxShadow,
}}
/>
</motion.div>
);
}
type StarsBackgroundProps = React.ComponentProps<'div'> & {
factor?: number;
speed?: number;
transition?: SpringOptions;
starColor?: string;
pointerEvents?: boolean;
};
function StarsBackground({
children,
className,
factor = 0.05,
speed = 50,
transition = { stiffness: 50, damping: 20 },
starColor = '#fff',
pointerEvents = true,
...props
}: StarsBackgroundProps) {
const offsetX = useMotionValue(1);
const offsetY = useMotionValue(1);
const springX = useSpring(offsetX, transition);
const springY = useSpring(offsetY, transition);
const handleMouseMove = React.useCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
const centerX = window.innerWidth / 2;
const centerY = window.innerHeight / 2;
const newOffsetX = -(e.clientX - centerX) * factor;
const newOffsetY = -(e.clientY - centerY) * factor;
offsetX.set(newOffsetX);
offsetY.set(newOffsetY);
},
[offsetX, offsetY, factor],
);
return (
<div
data-slot="stars-background"
className={cn(
'relative size-full overflow-hidden bg-[radial-gradient(ellipse_at_bottom,_#262626_0%,_#000_100%)]',
className,
)}
onMouseMove={handleMouseMove}
{...props}
>
<motion.div
style={{ x: springX, y: springY }}
className={cn({ 'pointer-events-none': !pointerEvents })}
>
<StarLayer
count={1000}
size={1}
transition={{ repeat: Infinity, duration: speed, ease: 'linear' }}
starColor={starColor}
/>
<StarLayer
count={400}
size={2}
transition={{
repeat: Infinity,
duration: speed * 2,
ease: 'linear',
}}
starColor={starColor}
/>
<StarLayer
count={200}
size={3}
transition={{
repeat: Infinity,
duration: speed * 3,
ease: 'linear',
}}
starColor={starColor}
/>
</motion.div>
{children}
</div>
);
}
export {
StarLayer,
StarsBackground,
type StarLayerProps,
type StarsBackgroundProps,
};