Infinite scrolling slider component that smoothly loops through its children.
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 duration?: number;
11 durationOnHover?: number;
12 direction?: 'horizontal' | 'vertical';
13 reverse?: boolean;
14 className?: string;
15};
16
17export function InfiniteSlider({
18 children,
19 gap = 16,
20 duration = 25,
21 durationOnHover,
22 direction = 'horizontal',
23 reverse = false,
24 className,
25}: InfiniteSliderProps) {
26 const [currentDuration, setCurrentDuration] = useState(duration);
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 if (isTransitioning) {
40 controls = animate(translation, [translation.get(), to], {
41 ease: 'linear',
42 duration:
43 currentDuration * Math.abs((translation.get() - to) / contentSize),
44 onComplete: () => {
45 setIsTransitioning(false);
46 setKey((prevKey) => prevKey + 1);
47 },
48 });
49 } else {
50 controls = animate(translation, [from, to], {
51 ease: 'linear',
52 duration: currentDuration,
53 repeat: Infinity,
54 repeatType: 'loop',
55 repeatDelay: 0,
56 onRepeat: () => {
57 translation.set(from);
58 },
59 });
60 }
61
62 return controls?.stop;
63 }, [
64 key,
65 translation,
66 currentDuration,
67 width,
68 height,
69 gap,
70 isTransitioning,
71 direction,
72 reverse,
73 ]);
74
75 const hoverProps = durationOnHover
76 ? {
77 onHoverStart: () => {
78 setIsTransitioning(true);
79 setCurrentDuration(durationOnHover);
80 },
81 onHoverEnd: () => {
82 setIsTransitioning(true);
83 setCurrentDuration(duration);
84 },
85 }
86 : {};
87
88 return (
89 <div className={cn('overflow-hidden', className)}>
90 <motion.div
91 className='flex w-max'
92 style={{
93 ...(direction === 'horizontal'
94 ? { x: translation }
95 : { y: translation }),
96 gap: `${gap}px`,
97 flexDirection: direction === 'horizontal' ? 'row' : 'column',
98 }}
99 ref={ref}
100 {...hoverProps}
101 >
102 {children}
103 {children}
104 </motion.div>
105 </div>
106 );
107}
108