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