A infinite-slider component from Motion Primitives.
1'use client';
2import { cn } from '@/lib/utils';
3import { useMotionValue, animate, motion } from 'motion/react';
4import { useState, useEffect } from 'react';
5import useMeasure from 'react-use-measure';
6
7export type InfiniteSliderProps = {
8 children: React.ReactNode;
9 gap?: number;
10 speed?: number;
11 speedOnHover?: number;
12 direction?: 'horizontal' | 'vertical';
13 reverse?: boolean;
14 className?: string;
15};
16
17export function InfiniteSlider({
18 children,
19 gap = 16,
20 speed = 100,
21 speedOnHover,
22 direction = 'horizontal',
23 reverse = false,
24 className,
25}: InfiniteSliderProps) {
26 const [currentSpeed, setCurrentSpeed] = useState(speed);
27 const [ref, { width, height }] = useMeasure();
28 const translation = useMotionValue(0);
29 const [isTransitioning, setIsTransitioning] = useState(false);
30 const [key, setKey] = useState(0);
31
32 useEffect(() => {
33 let controls;
34 const size = direction === 'horizontal' ? width : height;
35 const contentSize = size + gap;
36 const from = reverse ? -contentSize / 2 : 0;
37 const to = reverse ? 0 : -contentSize / 2;
38
39 const distanceToTravel = Math.abs(to - from);
40 const duration = distanceToTravel / currentSpeed;
41
42 if (isTransitioning) {
43 const remainingDistance = Math.abs(translation.get() - to);
44 const transitionDuration = remainingDistance / currentSpeed;
45
46 controls = animate(translation, [translation.get(), to], {
47 ease: 'linear',
48 duration: transitionDuration,
49 onComplete: () => {
50 setIsTransitioning(false);
51 setKey((prevKey) => prevKey + 1);
52 },
53 });
54 } else {
55 controls = animate(translation, [from, to], {
56 ease: 'linear',
57 duration: duration,
58 repeat: Infinity,
59 repeatType: 'loop',
60 repeatDelay: 0,
61 onRepeat: () => {
62 translation.set(from);
63 },
64 });
65 }
66
67 return controls?.stop;
68 }, [
69 key,
70 translation,
71 currentSpeed,
72 width,
73 height,
74 gap,
75 isTransitioning,
76 direction,
77 reverse,
78 ]);
79
80 const hoverProps = speedOnHover
81 ? {
82 onHoverStart: () => {
83 setIsTransitioning(true);
84 setCurrentSpeed(speedOnHover);
85 },
86 onHoverEnd: () => {
87 setIsTransitioning(true);
88 setCurrentSpeed(speed);
89 },
90 }
91 : {};
92
93 return (
94 <div className={cn('overflow-hidden', className)}>
95 <motion.div
96 className='flex w-max'
97 style={{
98 ...(direction === 'horizontal'
99 ? { x: translation }
100 : { y: translation }),
101 gap: `${gap}px`,
102 flexDirection: direction === 'horizontal' ? 'row' : 'column',
103 }}
104 ref={ref}
105 {...hoverProps}
106 >
107 {children}
108 {children}
109 </motion.div>
110 </div>
111 );
112}
113