113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
|
|
interface GalleryProps {
|
|
images: string[];
|
|
}
|
|
|
|
export default function ImageCarousel({ images }: GalleryProps) {
|
|
const [[page, direction], setPage] = useState([0, 0]);
|
|
const [isAutoPlaying, setIsAutoPlaying] = useState(true);
|
|
|
|
const imageIndex = Math.abs(page % images.length);
|
|
|
|
const paginate = useCallback(
|
|
(newDirection: number) => {
|
|
setPage([page + newDirection, newDirection]);
|
|
},
|
|
[page],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!isAutoPlaying) return;
|
|
|
|
const interval = setInterval(() => {
|
|
paginate(1);
|
|
}, 5000);
|
|
|
|
return () => clearInterval(interval);
|
|
}, [paginate, isAutoPlaying]);
|
|
|
|
const variants = {
|
|
enter: (direction: number) => ({
|
|
x: direction > 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}),
|
|
center: { zIndex: 1, x: 0, opacity: 1 },
|
|
exit: (direction: number) => ({
|
|
zIndex: 0,
|
|
x: direction < 0 ? 1000 : -1000,
|
|
opacity: 0,
|
|
}),
|
|
};
|
|
|
|
return (
|
|
<div
|
|
className="relative aspect-video w-full overflow-hidden rounded-3xl border border-neutral-800 bg-neutral-900"
|
|
onMouseEnter={() => setIsAutoPlaying(false)}
|
|
onMouseLeave={() => setIsAutoPlaying(true)}
|
|
>
|
|
<AnimatePresence initial={false} custom={direction}>
|
|
<motion.img
|
|
key={page}
|
|
src={images[imageIndex]}
|
|
custom={direction}
|
|
variants={variants}
|
|
initial="enter"
|
|
animate="center"
|
|
exit="exit"
|
|
transition={{
|
|
x: { type: "spring", stiffness: 300, damping: 30 },
|
|
opacity: { duration: 0.2 },
|
|
}}
|
|
drag="x"
|
|
dragConstraints={{ left: 0, right: 0 }}
|
|
dragElastic={1}
|
|
onDragStart={() => setIsAutoPlaying(false)}
|
|
onDragEnd={(e, { offset }) => {
|
|
const swipe = Math.abs(offset.x) > 50;
|
|
if (swipe) paginate(offset.x > 0 ? -1 : 1);
|
|
}}
|
|
className="absolute h-full w-full object-cover cursor-grab active:cursor-grabbing"
|
|
/>
|
|
</AnimatePresence>
|
|
|
|
{/* Navigation Arrows */}
|
|
<div className="absolute inset-0 z-10 flex items-center justify-between p-4 pointer-events-none">
|
|
<button
|
|
className="p-2 rounded-full bg-black/50 backdrop-blur-md border border-white/10 text-white pointer-events-auto hover:bg-black/80 transition-all"
|
|
onClick={() => {
|
|
setIsAutoPlaying(false);
|
|
paginate(-1);
|
|
}}
|
|
>
|
|
<ChevronLeft size={24} />
|
|
</button>
|
|
<button
|
|
className="p-2 rounded-full bg-black/50 backdrop-blur-md border border-white/10 text-white pointer-events-auto hover:bg-black/80 transition-all"
|
|
onClick={() => {
|
|
setIsAutoPlaying(false);
|
|
paginate(1);
|
|
}}
|
|
>
|
|
<ChevronRight size={24} />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Progress Bar */}
|
|
{isAutoPlaying && (
|
|
<motion.div
|
|
key={imageIndex}
|
|
initial={{ width: "0%" }}
|
|
animate={{ width: "100%" }}
|
|
transition={{ duration: 5, ease: "linear" }}
|
|
className="absolute bottom-0 left-0 h-1 bg-blue-500/50 z-20"
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|