feat: add InfiniteMovingCards component with customizable scroll animation
This commit is contained in:
		
							
								
								
									
										105
									
								
								src/components/magicui/infinite-moving-cards.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/components/magicui/infinite-moving-cards.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
				
			|||||||
 | 
					"use client";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { cn } from "@/lib/utils";
 | 
				
			||||||
 | 
					import React, { useCallback, useEffect, useState } from "react";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const InfiniteMovingCards = ({
 | 
				
			||||||
 | 
					  items,
 | 
				
			||||||
 | 
					  direction = "left",
 | 
				
			||||||
 | 
					  speed = "fast",
 | 
				
			||||||
 | 
					  pauseOnHover = true,
 | 
				
			||||||
 | 
					  className,
 | 
				
			||||||
 | 
					}: {
 | 
				
			||||||
 | 
					  items: React.ReactNode[];
 | 
				
			||||||
 | 
					  direction?: "left" | "right";
 | 
				
			||||||
 | 
					  speed?: "fast" | "normal" | "slow";
 | 
				
			||||||
 | 
					  pauseOnHover?: boolean;
 | 
				
			||||||
 | 
					  className?: string;
 | 
				
			||||||
 | 
					}): JSX.Element => {
 | 
				
			||||||
 | 
					  const containerRef = React.useRef<HTMLDivElement>(null);
 | 
				
			||||||
 | 
					  const scrollerRef = React.useRef<HTMLUListElement>(null);
 | 
				
			||||||
 | 
					  const [start, setStart] = useState(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getDirection = useCallback(() => {
 | 
				
			||||||
 | 
					    if (containerRef.current) {
 | 
				
			||||||
 | 
					      if (direction === "left") {
 | 
				
			||||||
 | 
					        containerRef.current.style.setProperty("--animation-direction", "forwards");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        containerRef.current.style.setProperty("--animation-direction", "reverse");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [direction]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getSpeed = useCallback(() => {
 | 
				
			||||||
 | 
					    if (containerRef.current) {
 | 
				
			||||||
 | 
					      if (speed === "fast") {
 | 
				
			||||||
 | 
					        containerRef.current.style.setProperty("--animation-duration", "20s");
 | 
				
			||||||
 | 
					      } else if (speed === "normal") {
 | 
				
			||||||
 | 
					        containerRef.current.style.setProperty("--animation-duration", "40s");
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        containerRef.current.style.setProperty("--animation-duration", "80s");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [speed]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const addAnimation = useCallback(() => {
 | 
				
			||||||
 | 
					    if (containerRef.current && scrollerRef.current) {
 | 
				
			||||||
 | 
					      const scrollerContent = Array.from(scrollerRef.current.children);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      scrollerContent.forEach((item) => {
 | 
				
			||||||
 | 
					        const duplicatedItem = item.cloneNode(true);
 | 
				
			||||||
 | 
					        if (scrollerRef.current) {
 | 
				
			||||||
 | 
					          scrollerRef.current.appendChild(duplicatedItem);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      getDirection();
 | 
				
			||||||
 | 
					      getSpeed();
 | 
				
			||||||
 | 
					      setStart(true);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }, [getDirection, getSpeed]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  useEffect(() => {
 | 
				
			||||||
 | 
					    addAnimation();
 | 
				
			||||||
 | 
					  }, [addAnimation]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <div
 | 
				
			||||||
 | 
					      ref={containerRef}
 | 
				
			||||||
 | 
					      className={cn(
 | 
				
			||||||
 | 
					        "scroller relative z-20 max-w-7xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)]",
 | 
				
			||||||
 | 
					        className
 | 
				
			||||||
 | 
					      )}
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <ul
 | 
				
			||||||
 | 
					        ref={scrollerRef}
 | 
				
			||||||
 | 
					        className={cn(
 | 
				
			||||||
 | 
					          "flex min-w-full shrink-0 gap-4 py-4 w-max flex-nowrap",
 | 
				
			||||||
 | 
					          start && "animate-scroll",
 | 
				
			||||||
 | 
					          pauseOnHover && "hover:[animation-play-state:paused]"
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {items.map((item, idx) => (
 | 
				
			||||||
 | 
					          <li
 | 
				
			||||||
 | 
					            className="w-[350px] max-w-full relative rounded-2xl border border-b-0 flex-shrink-0 border-slate-700 px-8 py-6 md:w-[450px]"
 | 
				
			||||||
 | 
					            style={{
 | 
				
			||||||
 | 
					              background:
 | 
				
			||||||
 | 
					                "linear-gradient(180deg, var(--slate-800), var(--slate-900))",
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					            key={idx}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <blockquote>
 | 
				
			||||||
 | 
					              <div
 | 
				
			||||||
 | 
					                aria-hidden="true"
 | 
				
			||||||
 | 
					                className="user-select-none -z-1 pointer-events-none absolute -left-0.5 -top-0.5 h-[calc(100%_+_4px)] w-[calc(100%_+_4px)]"
 | 
				
			||||||
 | 
					              ></div>
 | 
				
			||||||
 | 
					              <span className="relative z-20 text-sm leading-[1.6] text-gray-100 font-normal">
 | 
				
			||||||
 | 
					                {item}
 | 
				
			||||||
 | 
					              </span>
 | 
				
			||||||
 | 
					            </blockquote>
 | 
				
			||||||
 | 
					          </li>
 | 
				
			||||||
 | 
					        ))}
 | 
				
			||||||
 | 
					      </ul>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
		Reference in New Issue
	
	Block a user