Retour au catalogue
Process 3D Stack
Etapes de processus empilees en couches 3D qui se separent au scroll pour reveler chaque etape. Perspective et profondeur.
processcomplex Both Responsive a11y
boldelegantagencysaasuniversalstacked
Theme
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
interface Step {
number: string;
title: string;
description: string;
}
interface Process3dStackProps {
title?: string;
subtitle?: string;
steps?: Step[];
}
const E: [number, number, number, number] = [0.16, 1, 0.3, 1];
export default function Process3dStack({
title = "Nos etapes cles",
subtitle = "PROCESSUS",
steps = [],
}: Process3dStackProps) {
const [expanded, setExpanded] = useState(false);
const [active, setActive] = useState<number | null>(null);
return (
<section
style={{
paddingTop: "var(--section-padding-y)",
paddingBottom: "var(--section-padding-y)",
background: "var(--color-background)",
}}
>
<div
className="mx-auto"
style={{
maxWidth: "var(--container-max-width)",
paddingLeft: "var(--container-padding-x)",
paddingRight: "var(--container-padding-x)",
}}
>
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: E }}
className="text-center mb-14"
>
<p className="text-xs font-semibold uppercase tracking-widest mb-2" style={{ color: "var(--color-accent)" }}>{subtitle}</p>
<h2 className="text-3xl md:text-4xl font-bold tracking-tight" style={{ color: "var(--color-foreground)" }}>{title}</h2>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-10 items-center max-w-5xl mx-auto">
{/* 3D Stack */}
<motion.div
initial={{ opacity: 0 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.6, ease: E }}
className="relative mx-auto"
style={{ perspective: 1000, minHeight: 320 }}
onMouseEnter={() => setExpanded(true)}
onMouseLeave={() => { setExpanded(false); setActive(null); }}
>
{steps.map((step, i) => {
const total = steps.length;
const isActive = active === i;
const offset = expanded ? i * 60 : i * 8;
const rotateX = expanded ? -5 : -15 + i * 2;
const scale = expanded ? 1 : 1 - i * 0.03;
return (
<motion.div
key={step.number}
onClick={() => setActive(isActive ? null : i)}
className="cursor-pointer rounded-xl p-6"
style={{
position: i === 0 ? "relative" : "absolute",
top: 0,
left: 0,
right: 0,
background: isActive ? "var(--color-accent)" : "var(--color-background-alt)",
border: `1px solid ${isActive ? "var(--color-accent)" : "var(--color-border)"}`,
borderRadius: "var(--radius-xl)",
zIndex: total - i,
transformOrigin: "center bottom",
}}
animate={{
y: offset,
rotateX,
scale,
boxShadow: expanded
? "0 8px 32px rgba(0,0,0,0.1)"
: `0 ${i * 2}px ${i * 8}px rgba(0,0,0,0.05)`,
}}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<div className="flex items-center gap-4">
<span
className="text-2xl font-black"
style={{ color: isActive ? "var(--color-background)" : "var(--color-accent)", opacity: 0.4 }}
>
{step.number}
</span>
<div>
<h3
className="text-sm font-bold"
style={{ color: isActive ? "var(--color-background)" : "var(--color-foreground)" }}
>
{step.title}
</h3>
{expanded && (
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
className="text-xs mt-1"
style={{ color: isActive ? "var(--color-background)" : "var(--color-foreground-muted)", opacity: 0.8 }}
>
{step.description}
</motion.p>
)}
</div>
</div>
</motion.div>
);
})}
</motion.div>
{/* Detail panel */}
<div>
<AnimatePresence mode="wait">
{active !== null && steps[active] ? (
<motion.div
key={active}
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.35, ease: E }}
className="rounded-xl p-8"
style={{
background: "var(--color-background-alt)",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius-xl)",
}}
>
<span className="text-xs font-bold uppercase tracking-widest" style={{ color: "var(--color-accent)" }}>
Etape {steps[active].number}
</span>
<h3 className="text-2xl font-bold mt-2 mb-4" style={{ color: "var(--color-foreground)" }}>
{steps[active].title}
</h3>
<p className="text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>
{steps[active].description}
</p>
</motion.div>
) : (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="text-center py-12"
>
<p className="text-sm" style={{ color: "var(--color-foreground-muted)" }}>
Survolez et cliquez sur une etape pour en voir les details
</p>
</motion.div>
)}
</AnimatePresence>
</div>
</div>
</div>
</section>
);
}