Retour au catalogue

Bento 3D Tilt

Chaque carte bento a un tilt 3D independant suivant la position du curseur. Effet depth premium.

bentocomplex Both Responsive a11y
boldplayfulsaasagencyuniversalgrid
Theme
"use client";

import React from "react";
import {
  motion,
  useMotionValue,
  useSpring,
  useTransform,
} from "framer-motion";
import * as LucideIcons from "lucide-react";

interface BentoItem {
  id: string;
  title: string;
  description: string;
  icon?: string;
  span?: string;
}

interface Bento3dTiltProps {
  badge?: string;
  title?: string;
  subtitle?: string;
  items: BentoItem[];
}

function getIcon(name?: string) {
  if (!name) return null;
  return (LucideIcons as unknown as Record<string, React.ElementType>)[name] || null;
}

function TiltCard({ item, index }: { item: BentoItem; index: number }) {
  const Icon = getIcon(item.icon);
  const ref = React.useRef<HTMLDivElement>(null);
  const mx = useMotionValue(0);
  const my = useMotionValue(0);
  const rotateX = useSpring(useTransform(my, [-0.5, 0.5], [10, -10]), { stiffness: 250, damping: 25 });
  const rotateY = useSpring(useTransform(mx, [-0.5, 0.5], [-10, 10]), { stiffness: 250, damping: 25 });

  function handleMove(e: React.MouseEvent) {
    const rect = ref.current?.getBoundingClientRect();
    if (!rect) return;
    mx.set((e.clientX - rect.left) / rect.width - 0.5);
    my.set((e.clientY - rect.top) / rect.height - 0.5);
  }

  function handleLeave() {
    mx.set(0);
    my.set(0);
  }

  return (
    <motion.div
      ref={ref}
      onMouseMove={handleMove}
      onMouseLeave={handleLeave}
      initial={{ opacity: 0, y: 24 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: "-40px" }}
      transition={{ delay: index * 0.1, duration: 0.5 }}
      className={`rounded-2xl border p-6 ${item.span || "col-span-1"}`}
      style={{
        perspective: "800px",
        rotateX,
        rotateY,
        borderColor: "var(--color-border)",
        backgroundColor: "var(--color-background-card)",
      }}
    >
      {Icon && (
        <div className="mb-4 inline-flex items-center justify-center rounded-xl p-3" style={{ backgroundColor: "var(--color-accent-subtle, var(--color-background-alt))" }}>
          <Icon className="h-5 w-5" style={{ color: "var(--color-accent)" }} />
        </div>
      )}
      <h3 className="text-lg font-semibold" style={{ color: "var(--color-foreground)" }}>{item.title}</h3>
      <p className="mt-2 text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>{item.description}</p>
    </motion.div>
  );
}

export default function Bento3dTilt({ badge, title, subtitle, items }: Bento3dTiltProps) {
  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
      <div className="mx-auto max-w-6xl px-[var(--container-padding-x,1.5rem)]">
        <motion.div
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5 }}
          className="text-center max-w-2xl mx-auto mb-16"
        >
          {badge && (
            <span className="inline-block text-xs font-medium tracking-wider uppercase px-3 py-1 rounded-full border mb-4" style={{ color: "var(--color-accent)", borderColor: "var(--color-border)" }}>
              {badge}
            </span>
          )}
          {title && <h2 className="text-3xl font-bold tracking-tight md:text-4xl lg:text-5xl" style={{ color: "var(--color-foreground)" }}>{title}</h2>}
          {subtitle && <p className="mt-4 text-base" style={{ color: "var(--color-foreground-muted)" }}>{subtitle}</p>}
        </motion.div>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-5">
          {items.map((item, i) => <TiltCard key={item.id} item={item} index={i} />)}
        </div>
      </div>
    </section>
  );
}

Avis

Bento 3D Tilt — React Bento Section — Incubator