Retour au catalogue

Bento Draggable

Cartes bento draggable et reordonnables avec animations spring au relachement.

bentocomplex Both Responsive a11y
playfulboldsaasagencygrid
Theme
"use client";

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

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

interface BentoDraggableProps {
  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 DraggableCard({ item }: { item: BentoItem }) {
  const Icon = getIcon(item.icon);
  const GripIcon = getIcon("GripVertical");

  return (
    <Reorder.Item
      value={item}
      className="rounded-2xl border p-6 cursor-grab active:cursor-grabbing"
      style={{
        borderColor: "var(--color-border)",
        backgroundColor: "var(--color-background-card)",
      }}
      whileDrag={{
        scale: 1.03,
        boxShadow: "0 10px 40px color-mix(in srgb, var(--color-foreground) 10%, transparent)",
      }}
      transition={{ type: "spring", stiffness: 300, damping: 25 }}
    >
      <div className="flex items-start gap-4">
        {GripIcon && (
          <div className="mt-1 shrink-0 opacity-40">
            <GripIcon className="h-4 w-4" style={{ color: "var(--color-foreground-muted)" }} />
          </div>
        )}
        <div className="flex-1">
          <div className="flex items-center gap-3 mb-2">
            {Icon && <Icon className="h-5 w-5 shrink-0" style={{ color: "var(--color-accent)" }} />}
            <h3 className="text-base font-semibold" style={{ color: "var(--color-foreground)" }}>{item.title}</h3>
          </div>
          <p className="text-sm leading-relaxed" style={{ color: "var(--color-foreground-muted)" }}>{item.description}</p>
        </div>
      </div>
    </Reorder.Item>
  );
}

export default function BentoDraggable({ badge, title, subtitle, items: initialItems }: BentoDraggableProps) {
  const [items, setItems] = React.useState(initialItems);

  return (
    <section className="py-[var(--section-padding-y,6rem)]" style={{ backgroundColor: "var(--color-background)" }}>
      <div className="mx-auto max-w-3xl 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>

        <Reorder.Group
          axis="y"
          values={items}
          onReorder={setItems}
          className="flex flex-col gap-4"
        >
          {items.map((item) => (
            <DraggableCard key={item.id} item={item} />
          ))}
        </Reorder.Group>
      </div>
    </section>
  );
}

Avis

Bento Draggable — React Bento Section — Incubator