"use client"; import Image from "next/image"; import { useEffect, useMemo, useRef, useState } from "react"; type Props = { images: string[]; title?: string; autoPlay?: boolean; intervalMs?: number; }; export default function GalleryCarousel({ images, title = "Gallery", autoPlay = true, intervalMs = 4500, }: Props) { const containerRef = useRef(null); const itemRefs = useRef<(HTMLButtonElement | null)[]>([]); const [active, setActive] = useState(0); const [paused, setPaused] = useState(false); const safeImages = useMemo(() => images.filter(Boolean), [images]); function clampIndex(i: number) { const n = safeImages.length; if (n === 0) return 0; return (i + n) % n; } function scrollToIndex(i: number) { const idx = clampIndex(i); setActive(idx); const track = containerRef.current; const el = itemRefs.current[idx]; if (!track || !el) return; const trackRect = track.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); const target = track.scrollLeft + (elRect.left + elRect.width / 2) - (trackRect.left + trackRect.width / 2); track.scrollTo({ left: target, behavior: "smooth" }); } // Auto-advance useEffect(() => { if (!autoPlay || paused || safeImages.length <= 1) return; const t = setInterval(() => scrollToIndex(active + 1), intervalMs); return () => clearInterval(t); // eslint-disable-next-line react-hooks/exhaustive-deps }, [autoPlay, paused, active, intervalMs, safeImages.length]); // Keep active in sync when user swipes/scrolls useEffect(() => { const el = containerRef.current; if (!el || safeImages.length <= 1) return; let raf = 0; const onScroll = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { const rect = el.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; let bestIdx = active; let bestDist = Number.POSITIVE_INFINITY; for (let i = 0; i < safeImages.length; i++) { const item = itemRefs.current[i]; if (!item) continue; const r = item.getBoundingClientRect(); const itemCenter = r.left + r.width / 2; const d = Math.abs(itemCenter - centerX); if (d < bestDist) { bestDist = d; bestIdx = i; } } if (bestIdx !== active) setActive(bestIdx); }); }; el.addEventListener("scroll", onScroll, { passive: true }); return () => { el.removeEventListener("scroll", onScroll); cancelAnimationFrame(raf); }; }, [active, safeImages.length]); if (safeImages.length === 0) return null; return (

{title}

{/* Track */}
setPaused(true)} onMouseLeave={() => setPaused(false)} className="mt-4 flex w-full max-w-full gap-4 overflow-x-auto overflow-y-hidden overscroll-x-contain pb-3 scrollbar-hide px-2" style={{ scrollSnapType: "x mandatory" }} > {safeImages.map((img, i) => ( ))}
{/* Dots */} {safeImages.length > 1 && (
{safeImages.map((_, i) => (
)}
); }