import React, { useRef, useEffect, useState, useCallback, ReactNode } from 'react'; import { useSpring, animated } from '@react-spring/web'; import { useDrag } from '@use-gesture/react'; import clsx from 'clsx'; interface InfiniteCarouselProps { children: React.ReactNode; height: string; speed?: number; gap?: number; overlay?: ReactNode; overlayFunc?: (index: number) => void; overlayClassName?: string; } const InfiniteCarousel: React.FC = ({ children, height, speed = 20000, gap = 16, overlay = undefined, overlayFunc = undefined, overlayClassName = "" }) => { const containerRef = useRef(null); const [containerWidth, setContainerWidth] = useState(0); const itemCount = React.Children.count(children); const [isDragging, setIsDragging] = useState(false); const [itemWidth, setItemWidth] = useState(0); const [isInfinite, setIsInfinite] = useState(true); const dragStartX = useRef(0); useEffect(() => { const handleResize = () => { if (containerRef.current) { const containerWidth = containerRef.current.clientWidth; setContainerWidth(containerWidth); const firstChild = containerRef.current.firstElementChild?.firstElementChild as HTMLElement; if (firstChild) { const childWidth = firstChild.offsetWidth; setItemWidth(childWidth); const totalContentWidth = (childWidth + gap) * itemCount - gap; setIsInfinite(totalContentWidth > containerWidth); } } }; handleResize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }, [gap, itemCount]); const totalWidth = (itemWidth + gap) * itemCount; const [{ x }, api] = useSpring(() => ({ from: { x: 0 }, to: { x: -totalWidth }, config: { duration: speed }, loop: true, })); const startAnimation = useCallback(() => { if (isInfinite) { api.start({ from: { x: x.get() }, to: { x: x.get() - totalWidth }, config: { duration: speed }, loop: true, }); } else { api.stop(); api.start({ x: 0, immediate: true }); } }, [api, x, totalWidth, speed, isInfinite]); useEffect(() => { if (containerWidth > 0 && !isDragging) { startAnimation(); } }, [containerWidth, isDragging, startAnimation]); const bind = useDrag(({ down, movement: [mx], first }) => { if (!isInfinite) return; if (first) { setIsDragging(true); api.stop(); dragStartX.current = x.get(); } if (down) { let newX = dragStartX.current + mx; newX = ((newX % totalWidth) + totalWidth) % totalWidth; if (newX > 0) newX -= totalWidth; api.start({ x: newX, immediate: true }); } else { setIsDragging(false); startAnimation(); } }, { filterTaps: true, from: () => [x.get(), 0], }); return (
`translate3d(${x}px, 0, 0)`) : 'none', gap: `${gap}px`, width: 'fit-content', }} > {React.Children.map(children, (child, i) => (
{overlay !== undefined && overlayFunc !== undefined && (
overlayFunc(i)}> {overlay}
)}
{child}
))} {isInfinite && React.Children.map(children, (child, i) => (
{overlay !== undefined && overlayFunc !== undefined && (
overlayFunc(i)}> {overlay}
)}
{child}
))}
); }; export default InfiniteCarousel;