105 lines
3.6 KiB
TypeScript
105 lines
3.6 KiB
TypeScript
"use client";
|
|
import { useState } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { ArrowRight } from "lucide-react";
|
|
import { MobileFrame } from "./MobileFrame";
|
|
|
|
export default function MobileStack({ images }: { images: string[] }) {
|
|
const [currentIndex, setCurrentIndex] = useState(0);
|
|
|
|
const DRAG_THRESHOLD = -150;
|
|
|
|
const getRelativeIndex = (index: number) => {
|
|
const len = images.length;
|
|
return (index - currentIndex + len) % len;
|
|
};
|
|
|
|
const next = () => setCurrentIndex((prev) => (prev + 1) % images.length);
|
|
|
|
return (
|
|
<div className="relative h-[750px] w-full flex flex-col items-center py-20 overflow-hidden group">
|
|
<div className="relative h-[650px] w-full flex justify-center items-center">
|
|
<AnimatePresence initial={false}>
|
|
{images.map((img, index) => {
|
|
const relIndex = getRelativeIndex(index);
|
|
const isTop = relIndex === 0;
|
|
|
|
const xOffset = relIndex * 90;
|
|
|
|
if (relIndex > 5) return null;
|
|
|
|
return (
|
|
<motion.div
|
|
key={img}
|
|
style={{ zIndex: images.length - relIndex }}
|
|
initial={{ opacity: 0, x: 400 }}
|
|
animate={{
|
|
opacity: 1,
|
|
x: isTop ? 0 : xOffset,
|
|
scale: isTop ? 1 : 0.96,
|
|
filter: isTop ? "brightness(1)" : "brightness(0.4)",
|
|
pointerEvents: isTop ? "auto" : "all",
|
|
}}
|
|
exit={{
|
|
x: -1000,
|
|
opacity: 0,
|
|
transition: { duration: 0.4, ease: "easeIn" },
|
|
}}
|
|
transition={{
|
|
duration: 0.6,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
}}
|
|
whileHover={
|
|
!isTop ? { scale: 0.98, filter: "brightness(0.6)" } : {}
|
|
}
|
|
drag={isTop ? "x" : false}
|
|
dragConstraints={{ left: 0, right: 0 }}
|
|
dragElastic={0.8}
|
|
onDrag={(_, info) => {
|
|
if (isTop && info.offset.x < DRAG_THRESHOLD) {
|
|
next();
|
|
}
|
|
}}
|
|
onDragEnd={(_, info) => {
|
|
// Backup check for quick flicks
|
|
if (isTop && info.offset.x < -100) {
|
|
next();
|
|
}
|
|
}}
|
|
onClick={() => !isTop && setCurrentIndex(index)}
|
|
className="absolute"
|
|
>
|
|
<div
|
|
className={`${isTop ? "cursor-grab active:cursor-grabbing" : "cursor-pointer"}`}
|
|
>
|
|
<MobileFrame>
|
|
<img
|
|
src={img}
|
|
alt="App Screenshot"
|
|
draggable="false"
|
|
className="w-full h-full object-cover select-none"
|
|
/>
|
|
</MobileFrame>
|
|
</div>
|
|
</motion.div>
|
|
);
|
|
})}
|
|
</AnimatePresence>
|
|
|
|
{/* Navigation Button */}
|
|
<div className="absolute -bottom-12 z-[100]">
|
|
<button
|
|
onClick={next}
|
|
className="flex items-center gap-3 px-6 py-3 rounded-full bg-black/40 backdrop-blur-xl border border-white/5 text-white hover:bg-white/10 transition-all opacity-0 group-hover:opacity-100 group-hover:translate-y-[-20px]"
|
|
>
|
|
<span className="text-[10px] font-mono uppercase tracking-[0.2em]">
|
|
Next Screen
|
|
</span>
|
|
<ArrowRight size={16} />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|