255 lines
7.0 KiB
TypeScript
255 lines
7.0 KiB
TypeScript
// @ts-nocheck
|
|
'use client';
|
|
import { AnimatePresence, motion } from 'framer-motion';
|
|
import React, { useEffect, useRef, useState } from 'react';
|
|
import { cn } from '../../lib/utils';
|
|
|
|
export const BackgroundBeamsWithCollision = ({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) => {
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
const parentRef = useRef<HTMLDivElement>(null);
|
|
|
|
const beams = [
|
|
{
|
|
initialX: 10,
|
|
translateX: 10,
|
|
duration: 7,
|
|
repeatDelay: 3,
|
|
delay: 2,
|
|
},
|
|
{
|
|
initialX: 600,
|
|
translateX: 600,
|
|
duration: 3,
|
|
repeatDelay: 3,
|
|
delay: 4,
|
|
},
|
|
{
|
|
initialX: 100,
|
|
translateX: 100,
|
|
duration: 7,
|
|
repeatDelay: 7,
|
|
className: 'h-6',
|
|
},
|
|
{
|
|
initialX: 400,
|
|
translateX: 400,
|
|
duration: 5,
|
|
repeatDelay: 14,
|
|
delay: 4,
|
|
},
|
|
{
|
|
initialX: 800,
|
|
translateX: 800,
|
|
duration: 11,
|
|
repeatDelay: 2,
|
|
className: 'h-20',
|
|
},
|
|
{
|
|
initialX: 1000,
|
|
translateX: 1000,
|
|
duration: 4,
|
|
repeatDelay: 2,
|
|
className: 'h-12',
|
|
},
|
|
{
|
|
initialX: 1200,
|
|
translateX: 1200,
|
|
duration: 6,
|
|
repeatDelay: 4,
|
|
delay: 2,
|
|
className: 'h-6',
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div
|
|
ref={parentRef}
|
|
className={cn(
|
|
'relative flex h-96 w-full items-center justify-center overflow-hidden bg-gradient-to-b from-white to-neutral-100 md:h-[40rem] dark:from-neutral-950 dark:to-neutral-800',
|
|
// h-screen if you want bigger
|
|
className,
|
|
)}
|
|
>
|
|
{beams.map((beam) => (
|
|
<CollisionMechanism
|
|
key={beam.initialX + 'beam-idx'}
|
|
beamOptions={beam}
|
|
containerRef={containerRef}
|
|
parentRef={parentRef}
|
|
/>
|
|
))}
|
|
|
|
{children}
|
|
<div
|
|
ref={containerRef}
|
|
className='pointer-events-none absolute inset-x-0 bottom-0 w-full bg-neutral-100'
|
|
style={{
|
|
boxShadow:
|
|
'0 0 24px rgba(34, 42, 53, 0.06), 0 1px 1px rgba(0, 0, 0, 0.05), 0 0 0 1px rgba(34, 42, 53, 0.04), 0 0 4px rgba(34, 42, 53, 0.08), 0 16px 68px rgba(47, 48, 55, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1) inset',
|
|
}}
|
|
></div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const CollisionMechanism = React.forwardRef<
|
|
HTMLDivElement,
|
|
{
|
|
containerRef: React.RefObject<HTMLDivElement>;
|
|
parentRef: React.RefObject<HTMLDivElement>;
|
|
beamOptions?: {
|
|
initialX?: number;
|
|
translateX?: number;
|
|
initialY?: number;
|
|
translateY?: number;
|
|
rotate?: number;
|
|
className?: string;
|
|
duration?: number;
|
|
delay?: number;
|
|
repeatDelay?: number;
|
|
};
|
|
}
|
|
>(({ parentRef, containerRef, beamOptions = {} }, ref) => {
|
|
const beamRef = useRef<HTMLDivElement>(null);
|
|
const [collision, setCollision] = useState<{
|
|
detected: boolean;
|
|
coordinates: { x: number; y: number } | null;
|
|
}>({
|
|
detected: false,
|
|
coordinates: null,
|
|
});
|
|
const [beamKey, setBeamKey] = useState(0);
|
|
const [cycleCollisionDetected, setCycleCollisionDetected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const checkCollision = () => {
|
|
if (beamRef.current && containerRef.current && parentRef.current && !cycleCollisionDetected) {
|
|
const beamRect = beamRef.current.getBoundingClientRect();
|
|
const containerRect = containerRef.current.getBoundingClientRect();
|
|
const parentRect = parentRef.current.getBoundingClientRect();
|
|
|
|
if (beamRect.bottom >= containerRect.top) {
|
|
const relativeX = beamRect.left - parentRect.left + beamRect.width / 2;
|
|
const relativeY = beamRect.bottom - parentRect.top;
|
|
|
|
setCollision({
|
|
detected: true,
|
|
coordinates: {
|
|
x: relativeX,
|
|
y: relativeY,
|
|
},
|
|
});
|
|
setCycleCollisionDetected(true);
|
|
}
|
|
}
|
|
};
|
|
|
|
const animationInterval = setInterval(checkCollision, 50);
|
|
|
|
return () => clearInterval(animationInterval);
|
|
}, [cycleCollisionDetected, containerRef]);
|
|
|
|
useEffect(() => {
|
|
if (collision.detected && collision.coordinates) {
|
|
setTimeout(() => {
|
|
setCollision({ detected: false, coordinates: null });
|
|
setCycleCollisionDetected(false);
|
|
}, 2000);
|
|
|
|
setTimeout(() => {
|
|
setBeamKey((prevKey) => prevKey + 1);
|
|
}, 2000);
|
|
}
|
|
}, [collision]);
|
|
|
|
return (
|
|
<>
|
|
<motion.div
|
|
key={beamKey}
|
|
ref={beamRef}
|
|
animate='animate'
|
|
initial={{
|
|
translateY: beamOptions.initialY || '-200px',
|
|
translateX: beamOptions.initialX || '0px',
|
|
rotate: beamOptions.rotate || 0,
|
|
}}
|
|
variants={{
|
|
animate: {
|
|
translateY: beamOptions.translateY || '1800px',
|
|
translateX: beamOptions.translateX || '0px',
|
|
rotate: beamOptions.rotate || 0,
|
|
},
|
|
}}
|
|
transition={{
|
|
duration: beamOptions.duration || 8,
|
|
repeat: Infinity,
|
|
repeatType: 'loop',
|
|
ease: 'linear',
|
|
delay: beamOptions.delay || 0,
|
|
repeatDelay: beamOptions.repeatDelay || 0,
|
|
}}
|
|
className={cn(
|
|
'absolute left-0 top-20 m-auto h-14 w-px rounded-full bg-gradient-to-t from-indigo-500 via-purple-500 to-transparent',
|
|
beamOptions.className,
|
|
)}
|
|
/>
|
|
<AnimatePresence>
|
|
{collision.detected && collision.coordinates && (
|
|
<Explosion
|
|
key={`${collision.coordinates.x}-${collision.coordinates.y}`}
|
|
className=''
|
|
style={{
|
|
left: `${collision.coordinates.x}px`,
|
|
top: `${collision.coordinates.y}px`,
|
|
transform: 'translate(-50%, -50%)',
|
|
}}
|
|
/>
|
|
)}
|
|
</AnimatePresence>
|
|
</>
|
|
);
|
|
});
|
|
|
|
CollisionMechanism.displayName = 'CollisionMechanism';
|
|
|
|
const Explosion = ({ ...props }: React.HTMLProps<HTMLDivElement>) => {
|
|
const spans = Array.from({ length: 20 }, (_, index) => ({
|
|
id: index,
|
|
initialX: 0,
|
|
initialY: 0,
|
|
directionX: Math.floor(Math.random() * 80 - 40),
|
|
directionY: Math.floor(Math.random() * -50 - 10),
|
|
}));
|
|
|
|
return (
|
|
<div {...props} className={cn('absolute z-50 h-2 w-2', props.className)}>
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
exit={{ opacity: 0 }}
|
|
transition={{ duration: 1.5, ease: 'easeOut' }}
|
|
className='absolute -inset-x-10 top-0 m-auto h-2 w-10 rounded-full bg-gradient-to-r from-transparent via-indigo-500 to-transparent blur-sm'
|
|
></motion.div>
|
|
{spans.map((span) => (
|
|
<motion.span
|
|
key={span.id}
|
|
initial={{ x: span.initialX, y: span.initialY, opacity: 1 }}
|
|
animate={{
|
|
x: span.directionX,
|
|
y: span.directionY,
|
|
opacity: 0,
|
|
}}
|
|
transition={{ duration: Math.random() * 1.5 + 0.5, ease: 'easeOut' }}
|
|
className='absolute h-1 w-1 rounded-full bg-gradient-to-b from-indigo-500 to-purple-500'
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|