|
| 1 | +'use client'; |
| 2 | + |
| 3 | +import { cn } from '@/utils/cn'; |
| 4 | +import React, { useEffect, useState } from 'react'; |
| 5 | + |
| 6 | +export const InfiniteMovingCards = ({ |
| 7 | + items, |
| 8 | + direction = 'left', |
| 9 | + speed = 'fast', |
| 10 | + pauseOnHover = true, |
| 11 | + className, |
| 12 | +}: { |
| 13 | + items: { |
| 14 | + name: string; |
| 15 | + title: string; |
| 16 | + }[]; |
| 17 | + direction?: 'left' | 'right'; |
| 18 | + speed?: 'fast' | 'normal' | 'slow'; |
| 19 | + pauseOnHover?: boolean; |
| 20 | + className?: string; |
| 21 | +}) => { |
| 22 | + const containerRef = React.useRef<HTMLDivElement>(null); |
| 23 | + const scrollerRef = React.useRef<HTMLUListElement>(null); |
| 24 | + |
| 25 | + useEffect(() => { |
| 26 | + addAnimation(); |
| 27 | + }, []); |
| 28 | + |
| 29 | + const [start, setStart] = useState(false); |
| 30 | + |
| 31 | + function addAnimation() { |
| 32 | + if (containerRef.current && scrollerRef.current) { |
| 33 | + const scrollerContent = Array.from(scrollerRef.current.children); |
| 34 | + |
| 35 | + scrollerContent.forEach((item) => { |
| 36 | + const duplicatedItem = item.cloneNode(true); |
| 37 | + if (scrollerRef.current) { |
| 38 | + scrollerRef.current.appendChild(duplicatedItem); |
| 39 | + } |
| 40 | + }); |
| 41 | + |
| 42 | + getDirection(); |
| 43 | + getSpeed(); |
| 44 | + setStart(true); |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + const getDirection = () => { |
| 49 | + if (containerRef.current) { |
| 50 | + if (direction === 'left') { |
| 51 | + containerRef.current.style.setProperty( |
| 52 | + '--animation-direction', |
| 53 | + 'forwards' |
| 54 | + ); |
| 55 | + } else { |
| 56 | + containerRef.current.style.setProperty( |
| 57 | + '--animation-direction', |
| 58 | + 'reverse' |
| 59 | + ); |
| 60 | + } |
| 61 | + } |
| 62 | + }; |
| 63 | + |
| 64 | + const getSpeed = () => { |
| 65 | + if (containerRef.current) { |
| 66 | + if (speed === 'fast') { |
| 67 | + containerRef.current.style.setProperty('--animation-duration', '20s'); |
| 68 | + } else if (speed === 'normal') { |
| 69 | + containerRef.current.style.setProperty('--animation-duration', '40s'); |
| 70 | + } else { |
| 71 | + containerRef.current.style.setProperty('--animation-duration', '80s'); |
| 72 | + } |
| 73 | + } |
| 74 | + }; |
| 75 | + |
| 76 | + return ( |
| 77 | + <div |
| 78 | + ref={containerRef} |
| 79 | + className={cn( |
| 80 | + 'scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]', |
| 81 | + className |
| 82 | + )} |
| 83 | + > |
| 84 | + <ul |
| 85 | + ref={scrollerRef} |
| 86 | + className={cn( |
| 87 | + 'flex min-w-full shrink-0 gap-4 py-16 w-max flex-nowrap', |
| 88 | + start && 'animate-scroll-right', |
| 89 | + pauseOnHover && 'hover:[animation-play-state:paused]' |
| 90 | + )} |
| 91 | + > |
| 92 | + {items.map((item, idx) => ( |
| 93 | + <li |
| 94 | + className={cn( |
| 95 | + 'bg-black-100 w-[150px] relative rounded-2xl border flex-shrink-0 border-black-50 p-4 md:w-[200px] transition-all', |
| 96 | + idx % 2 === 0 ? '-translate-y-16' : 'translate-y-16' |
| 97 | + )} |
| 98 | + key={item.name} |
| 99 | + > |
| 100 | + {/* Roadmap marker line */} |
| 101 | + <div |
| 102 | + className={cn( |
| 103 | + 'absolute left-1/2 w-px bg-black-50', |
| 104 | + idx % 2 === 0 |
| 105 | + ? 'bottom-0 h-[25px] translate-y-full' |
| 106 | + : 'top-0 h-[25px] -translate-y-full' |
| 107 | + )} |
| 108 | + > |
| 109 | + {/* Dot at the connection point */} |
| 110 | + <div |
| 111 | + className={cn( |
| 112 | + 'absolute left-1/2 size-1.5 rounded-full bg-black-50 -translate-x-1/2', |
| 113 | + idx % 2 !== 0 ? 'top-0' : 'bottom-0' |
| 114 | + )} |
| 115 | + /> |
| 116 | + </div> |
| 117 | + |
| 118 | + <div |
| 119 | + aria-hidden="true" |
| 120 | + className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]" |
| 121 | + ></div> |
| 122 | + <div className="relative z-20 flex size-full justify-center items-center"> |
| 123 | + <span className="text-center leading-[1.6] text-white font-ubuntu font-normal"> |
| 124 | + {item.title} |
| 125 | + </span> |
| 126 | + </div> |
| 127 | + </li> |
| 128 | + ))} |
| 129 | + </ul> |
| 130 | + </div> |
| 131 | + ); |
| 132 | +}; |
0 commit comments