TetraKits UI Bento Grid — Growth Suite ← Library

Bento Grid — Growth Suite

Growth-suite motion bento with the same dark black theme with Framer Motion animations. Growth Insights, Approval Queue, Brand Library, Multi-Region Mesh tiles with status pills and #tags.

bento grid growthframer motion featuresmarketing bento reactanimated bentosaas feature grid
Live preview

Dependencies

react framer-motion lucide-react
npm install framer-motion lucide-react
"use client";

import { motion } from "framer-motion";

function cn(...inputs: (string | undefined | null | false | Record<string, boolean>)[]) {
  return inputs
    .flatMap((x) => {
      if (!x) return [];
      if (typeof x === "string") return [x];
      return Object.entries(x).filter(([, v]) => v).map(([k]) => k);
    })
    .join(" ");
}

export interface BentoItem {
  title: string;
  description: string;
  icon: React.ReactNode;
  status?: string;
  tags?: string[];
  meta?: string;
  cta?: string;
  colSpan?: number;
  hasPersistentHover?: boolean;
}

interface BentoGridProps {
  items?: BentoItem[];
}

import {
  CheckCircle,
  Globe,
  Layers,
  LineChart,
} from "lucide-react";

const growthItems: BentoItem[] = [
  {
    title: "Growth Insights",
    meta: "Q1 2026",
    description: "Cohort retention, funnel drop-off, and revenue attribution with AI-generated weekly summaries.",
    icon: <LineChart className="h-4 w-4 text-blue-400" />,
    status: "Live",
    tags: ["Cohort", "Forecast", "BI"],
    colSpan: 2,
    hasPersistentHover: true,
    cta: "Open reports →",
  },
  {
    title: "Approval Queue",
    meta: "36 pending",
    description: "Route contracts, budgets, and vendor requests through multi-step sign-off with audit trails.",
    icon: <CheckCircle className="h-4 w-4 text-emerald-400" />,
    status: "Updated",
    tags: ["Compliance", "Sign-off"],
    cta: "Review queue →",
  },
  {
    title: "Brand Library",
    meta: "1.2k assets",
    description: "Centralized logos, templates, and campaign creatives with auto-resize and CDN delivery.",
    icon: <Layers className="h-4 w-4 text-purple-400" />,
    tags: ["DAM", "CDN", "Templates"],
    colSpan: 2,
    cta: "Browse assets →",
  },
  {
    title: "Multi-Region Mesh",
    meta: "9 POPs",
    description: "Deploy workflows to US, EU, and APAC edge nodes with automatic failover and latency routing.",
    icon: <Globe className="h-4 w-4 text-sky-400" />,
    status: "Beta",
    tags: ["Infrastructure", "Edge"],
    cta: "View regions →",
  },
];

function BentoGrid({ items = growthItems }: BentoGridProps) {
  return (
    <div className="grid grid-cols-1 gap-3 p-4 md:grid-cols-3 mx-auto max-w-7xl">
      {items.map((item, index) => (
        <motion.div
          key={item.title}
          initial={{ opacity: 0, y: 20 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true, margin: "-40px" }}
          transition={{ duration: 0.45, delay: index * 0.07, ease: [0.22, 1, 0.36, 1] }}
          whileHover={{ y: -6, scale: 1.012 }}
          whileTap={{ scale: 0.995 }}
          className={cn(
            "group relative overflow-hidden rounded-xl border border-white/10 bg-black p-4",
            "shadow-[0_2px_12px_rgba(255,255,255,0.02)] transition-shadow duration-300",
            "hover:border-white/20 hover:shadow-[0_8px_32px_rgba(255,255,255,0.06)]",
            item.colSpan === 2 ? "md:col-span-2" : "col-span-1",
            item.hasPersistentHover && "-translate-y-1.5 shadow-[0_8px_32px_rgba(255,255,255,0.06)] border-white/20"
          )}
        >
          <motion.div
            className="pointer-events-none absolute inset-0"
            initial={false}
            animate={{ opacity: item.hasPersistentHover ? 1 : 0 }}
            whileHover={{ opacity: 1 }}
            transition={{ duration: 0.3 }}
          >
            <div className="absolute inset-0 bg-[length:4px_4px] bg-[radial-gradient(circle_at_center,rgba(255,255,255,0.04)_1px,transparent_1px)]" />
          </motion.div>

          <div className="relative flex flex-col space-y-3">
            <div className="flex items-center justify-between">
              <motion.div
                whileHover={{ scale: 1.08, rotate: 3 }}
                transition={{ type: "spring", stiffness: 400, damping: 18 }}
                className="flex h-8 w-8 items-center justify-center rounded-lg bg-white/10"
              >
                {item.icon}
              </motion.div>
              <span className="rounded-lg bg-white/10 px-2 py-1 text-xs font-medium text-zinc-300 backdrop-blur-sm">
                {item.status || "Active"}
              </span>
            </div>

            <div className="space-y-2">
              <h3 className="text-[15px] font-medium tracking-tight text-zinc-100">
                {item.title}
                {item.meta ? (
                  <span className="ml-2 text-xs font-normal text-zinc-500">{item.meta}</span>
                ) : null}
              </h3>
              <p className="text-sm leading-snug text-zinc-400">{item.description}</p>
            </div>

            <div className="mt-2 flex items-center justify-between">
              <div className="flex flex-wrap items-center gap-2 text-xs text-zinc-500">
                {item.tags?.map((tag) => (
                  <motion.span
                    key={tag}
                    whileHover={{ scale: 1.05, backgroundColor: "rgba(255,255,255,0.14)" }}
                    className="rounded-md bg-white/10 px-2 py-1 backdrop-blur-sm"
                  >
                    #{tag}
                  </motion.span>
                ))}
              </div>
              <motion.span
                initial={{ opacity: 0, x: -6 }}
                whileHover={{ opacity: 1, x: 0 }}
                className="text-xs text-zinc-400 opacity-0 transition-opacity group-hover:opacity-100"
              >
                {item.cta || "Explore →"}
              </motion.span>
            </div>
          </div>

          <div
            className={cn(
              "pointer-events-none absolute inset-0 -z-10 rounded-xl bg-gradient-to-br from-transparent via-white/10 to-transparent p-px",
              item.hasPersistentHover ? "opacity-100" : "opacity-0 group-hover:opacity-100",
              "transition-opacity duration-300"
            )}
          />
        </motion.div>
      ))}
    </div>
  );
}

export function GrowthBentoFeatures() {
  return (
    <section className="bg-black px-4 py-20 sm:px-6 lg:px-8">
      <div className="mx-auto max-w-7xl">
        <motion.div
          initial={{ opacity: 0, y: 12 }}
          whileInView={{ opacity: 1, y: 0 }}
          viewport={{ once: true }}
          transition={{ duration: 0.5 }}
          className="mb-10 max-w-2xl"
        >
          <p className="text-sm font-medium text-zinc-500">Growth workspace</p>
          <h2 className="mt-2 text-2xl font-semibold tracking-tight text-zinc-50 sm:text-3xl">Scale go-to-market without the sprawl</h2>
          <p className="mt-2 text-sm text-zinc-400">Insights, approvals, assets, and global delivery in one animated grid.</p>
        </motion.div>
        <BentoGrid />
      </div>
    </section>
  );
}

export { BentoGrid };