114 lines
2.7 KiB
TypeScript
114 lines
2.7 KiB
TypeScript
'use client';
|
|
import { cn } from '@/lib/utils';
|
|
import { useMotionValue, animate, motion } from 'motion/react';
|
|
import { useState, useEffect } from 'react';
|
|
import useMeasure from 'react-use-measure';
|
|
|
|
export type InfiniteSliderProps = {
|
|
children: React.ReactNode;
|
|
gap?: number;
|
|
speed?: number;
|
|
speedOnHover?: number;
|
|
direction?: 'horizontal' | 'vertical';
|
|
reverse?: boolean;
|
|
className?: string;
|
|
};
|
|
|
|
export function InfiniteSlider({
|
|
children,
|
|
gap = 16,
|
|
speed = 100,
|
|
speedOnHover,
|
|
direction = 'horizontal',
|
|
reverse = false,
|
|
className,
|
|
}: InfiniteSliderProps) {
|
|
const [currentSpeed, setCurrentSpeed] = useState(speed);
|
|
const [ref, { width, height }] = useMeasure();
|
|
const translation = useMotionValue(0);
|
|
const [isTransitioning, setIsTransitioning] = useState(false);
|
|
const [key, setKey] = useState(0);
|
|
|
|
useEffect(() => {
|
|
let controls;
|
|
const size = direction === 'horizontal' ? width : height;
|
|
const contentSize = size + gap;
|
|
const from = reverse ? -contentSize / 2 : 0;
|
|
const to = reverse ? 0 : -contentSize / 2;
|
|
|
|
const distanceToTravel = Math.abs(to - from);
|
|
const duration = distanceToTravel / currentSpeed;
|
|
|
|
if (isTransitioning) {
|
|
const remainingDistance = Math.abs(translation.get() - to);
|
|
const transitionDuration = remainingDistance / currentSpeed;
|
|
|
|
controls = animate(translation, [translation.get(), to], {
|
|
ease: 'linear',
|
|
duration: transitionDuration,
|
|
onComplete: () => {
|
|
setIsTransitioning(false);
|
|
setKey((prevKey) => prevKey + 1);
|
|
},
|
|
});
|
|
} else {
|
|
controls = animate(translation, [from, to], {
|
|
ease: 'linear',
|
|
duration: duration,
|
|
repeat: Infinity,
|
|
repeatType: 'loop',
|
|
repeatDelay: 0,
|
|
onRepeat: () => {
|
|
translation.set(from);
|
|
},
|
|
});
|
|
}
|
|
|
|
return controls?.stop;
|
|
}, [
|
|
key,
|
|
translation,
|
|
currentSpeed,
|
|
width,
|
|
height,
|
|
gap,
|
|
isTransitioning,
|
|
direction,
|
|
reverse,
|
|
]);
|
|
|
|
const hoverProps = speedOnHover
|
|
? {
|
|
onHoverStart: () => {
|
|
setIsTransitioning(true);
|
|
setCurrentSpeed(speedOnHover);
|
|
},
|
|
onHoverEnd: () => {
|
|
setIsTransitioning(true);
|
|
setCurrentSpeed(speed);
|
|
},
|
|
}
|
|
: {};
|
|
|
|
return (
|
|
<div className={cn('overflow-hidden', className)}>
|
|
<motion.div
|
|
className='flex w-max'
|
|
style={{
|
|
...(direction === 'horizontal'
|
|
? { x: translation }
|
|
: { y: translation }),
|
|
gap: `${gap}px`,
|
|
flexDirection: direction === 'horizontal' ? 'row' : 'column',
|
|
}}
|
|
ref={ref}
|
|
{...hoverProps}
|
|
>
|
|
{children}
|
|
{children}
|
|
</motion.div>
|
|
</div>
|
|
);
|
|
}
|
|
|