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>
  );
}

Avis

Process 3D Stack — React Process Section — Incubator