shadimage

Bento Grid

src/components/thegridcn/bento-grid.tsx

"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

interface BentoGridItem {
  description?: string;
  icon?: React.ReactNode;
  span?: "1x1" | "2x1" | "1x2" | "2x2";
  title: string;
  variant?: "default" | "highlight";
}

interface BentoGridProps extends React.HTMLAttributes<HTMLDivElement> {
  columns?: 2 | 3 | 4;
  items: BentoGridItem[];
}

const spanMap: Record<string, string> = {
  "1x1": "",
  "1x2": "md:row-span-2",
  "2x1": "md:col-span-2",
  "2x2": "md:col-span-2 md:row-span-2",
};

const colsMap: Record<number, string> = {
  2: "md:grid-cols-2",
  3: "md:grid-cols-3",
  4: "md:grid-cols-4",
};

export function BentoGrid({
  items,
  columns = 3,
  className,
  ...props
}: BentoGridProps) {
  // Stagger card reveal
  const [revealedIdx, setRevealedIdx] = React.useState(-1);
  React.useEffect(() => {
    let idx = 0;
    const interval = setInterval(() => {
      setRevealedIdx(idx);
      idx++;
      if (idx >= items.length) {
        clearInterval(interval);
      }
    }, 80);
    return () => clearInterval(interval);
  }, [items.length]);

  return (
    <div
      data-slot="tron-bento-grid"
      className={cn("grid grid-cols-1 gap-3", colsMap[columns], className)}
      {...props}
    >
      {items.map((item, i) => (
        <div
          key={i}
          className={cn(
            "group relative overflow-hidden rounded border bg-card/80 p-5 backdrop-blur-sm transition-all duration-400",
            item.variant === "highlight"
              ? "border-primary/50 shadow-[0_0_20px_rgba(var(--primary-rgb,0,180,255),0.06)]"
              : "border-primary/20",
            spanMap[item.span || "1x1"],
            i <= revealedIdx
              ? "translate-y-0 opacity-100"
              : "translate-y-3 opacity-0"
          )}
        >
          {/* Scanline overlay */}
          <div className="pointer-events-none absolute inset-0 bg-[repeating-linear-gradient(0deg,transparent,transparent_2px,rgba(0,0,0,0.03)_2px,rgba(0,0,0,0.03)_4px)]" />

          {/* Hover glow sweep */}
          <div className="pointer-events-none absolute -inset-px rounded opacity-0 transition-opacity duration-500 group-hover:opacity-100">
            <div
              className="absolute inset-0 bg-gradient-to-r from-transparent via-primary/8 to-transparent"
              style={{ animation: "bentoSweep 2s ease-in-out infinite" }}
            />
          </div>

          <div className="relative">
            {item.icon ? (
              <div className="mb-3 flex h-8 w-8 items-center justify-center rounded border border-primary/30 bg-primary/10 text-primary/80">
                {item.icon}
              </div>
            ) : null}
            <h3 className="font-medium font-mono text-foreground/90 text-sm uppercase tracking-wider">
              {item.title}
            </h3>
            {item.description ? (
              <p className="mt-1.5 text-foreground/50 text-xs leading-relaxed">
                {item.description}
              </p>
            ) : null}
          </div>

          {/* Corner decorations */}
          <div className="pointer-events-none absolute top-0 left-0 h-2.5 w-2.5 border-primary/40 border-t border-l transition-all group-hover:h-3.5 group-hover:w-3.5 group-hover:border-primary/60" />
          <div className="pointer-events-none absolute top-0 right-0 h-2.5 w-2.5 border-primary/40 border-t border-r transition-all group-hover:h-3.5 group-hover:w-3.5 group-hover:border-primary/60" />
          <div className="pointer-events-none absolute bottom-0 left-0 h-2.5 w-2.5 border-primary/40 border-b border-l transition-all group-hover:h-3.5 group-hover:w-3.5 group-hover:border-primary/60" />
          <div className="pointer-events-none absolute right-0 bottom-0 h-2.5 w-2.5 border-primary/40 border-r border-b transition-all group-hover:h-3.5 group-hover:w-3.5 group-hover:border-primary/60" />
        </div>
      ))}

      <style jsx>{`
        @keyframes bentoSweep {
          0%, 100% { transform: translateX(-100%); }
          50% { transform: translateX(100%); }
        }
      `}</style>
    </div>
  );
}
Where it goes
bento-grid
Visual style
bento