import { cn } from "@/lib/utils"; import React, { useCallback, useRef, useState } from "react"; interface MobileSliderProps { /** Items to display in the slider */ children: React.ReactNode[]; /** Additional classes for the container */ className?: string; /** Show dot indicators */ showDots?: boolean; /** Items per slide on mobile (default 1) */ itemsPerSlide?: number; } export default function MobileSlider({ children, className, showDots = true, itemsPerSlide = 1, }: Readonly) { const [currentIndex, setCurrentIndex] = useState(0); const [isDragging, setIsDragging] = useState(false); const [startX, setStartX] = useState(0); const [translateX, setTranslateX] = useState(0); const trackRef = useRef(null); const containerRef = useRef(null); const items = React.Children.toArray(children); const slideCount = Math.ceil(items.length / itemsPerSlide); const goTo = useCallback( (index: number) => { const clamped = Math.max(0, Math.min(index, slideCount - 1)); setCurrentIndex(clamped); setTranslateX(0); }, [slideCount], ); // Touch events const onTouchStart = (e: React.TouchEvent) => { setIsDragging(true); setStartX(e.touches[0].clientX); }; const onTouchMove = (e: React.TouchEvent) => { if (!isDragging) return; const diff = e.touches[0].clientX - startX; setTranslateX(diff); }; const onTouchEnd = () => { setIsDragging(false); const threshold = 60; if (translateX < -threshold) { goTo(currentIndex + 1); } else if (translateX > threshold) { goTo(currentIndex - 1); } else { setTranslateX(0); } }; const slideIndexes = Array.from({ length: slideCount }, (_, idx) => idx); const slidePercent = (100 / slideCount) * currentIndex; const dragPercent = (translateX / (containerRef.current?.offsetWidth ?? 300)) * (100 / slideCount); const totalOffset = -slidePercent + dragPercent; return (
{/* Mobile-only slider */}
{slideIndexes.map((slideIndex) => (
{items .slice( slideIndex * itemsPerSlide, (slideIndex + 1) * itemsPerSlide, ) .map((item) => (
{item}
))}
))}
{/* Dot indicators, mobile only */} {showDots && slideCount > 1 && (
{(() => { const MAX_DOTS = 5; let translateIndex = 0; if (slideCount > MAX_DOTS) { // Try to keep the active dot in the center (index 2 of the 5 visible dots) translateIndex = Math.min( Math.max(0, currentIndex - 2), slideCount - MAX_DOTS, ); } return (
MAX_DOTS ? "5.75rem" : "auto" }} >
{slideIndexes.map((i) => { // Small edge dots to indicate scrollability const isLeftEdge = slideCount > MAX_DOTS && translateIndex > 0 && i === translateIndex; const isRightEdge = slideCount > MAX_DOTS && translateIndex < slideCount - MAX_DOTS && i === translateIndex + MAX_DOTS - 1; let dotClass = "w-3 h-3 bg-primary hover:bg-primary/85"; if (i === currentIndex) { dotClass = "w-5 h-5 bg-primary-foreground scale-110"; } else if (isLeftEdge || isRightEdge) { dotClass = "w-2.5 h-2.5 bg-primary/70 hover:bg-primary/90"; } return (
); })}
); })()}
)} {/* Desktop grid, visible on sm+ */}
{items}
); }