Sections

Features Bento

Five-cell bento feature grid with self-drawn illustrations per cell and a 40ms reveal stagger.

Install

npx shadcn@latest add @paragon/features-bento

features-bento.tsx

"use client";

import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

const REVEAL =
  "transition-[opacity,translate,filter] duration-500 ease-out motion-reduce:transition-none";
const HIDDEN = "translate-y-3 opacity-0 blur-[4px]";
const SHOWN = "translate-y-0 opacity-100 blur-[0px]";

/* ---- Self-drawn cell illustrations (divs and hand-built SVG only) ---- */

function SparklineIllustration() {
  return (
    <div aria-hidden className="pointer-events-none select-none">
      <svg viewBox="0 0 260 72" className="h-20 w-full" fill="none">
        <path
          d="M0 58 L24 52 L48 55 L72 44 L96 47 L120 36 L144 40 L168 26 L192 30 L216 16 L240 20 L260 8"
          stroke="currentColor"
          strokeWidth="1.5"
          strokeLinejoin="round"
          className="text-primary"
        />
        <path
          d="M0 58 L24 52 L48 55 L72 44 L96 47 L120 36 L144 40 L168 26 L192 30 L216 16 L240 20 L260 8 L260 72 L0 72 Z"
          className="fill-primary/8"
        />
        <circle cx="260" cy="8" r="3" className="fill-primary" />
      </svg>
      <div className="mt-2 flex items-center justify-between text-[10px] text-muted-foreground">
        <span>events / sec</span>
        <span className="tabular-nums">42,807 live</span>
      </div>
    </div>
  );
}

const TOGGLE_ROWS = [
  { label: "PII masking", on: true },
  { label: "Row-level access", on: true },
  { label: "Public link sharing", on: false },
];

function ToggleRowsIllustration() {
  return (
    <div aria-hidden className="pointer-events-none select-none space-y-2">
      {TOGGLE_ROWS.map((row) => (
        <div
          key={row.label}
          className="flex items-center justify-between rounded-lg border border-border px-3 py-2"
        >
          <span className="text-xs text-muted-foreground">{row.label}</span>
          <span
            className={cn(
              "relative inline-flex h-4 w-7 rounded-full",
              row.on ? "bg-primary" : "bg-muted",
            )}
          >
            <span
              className={cn(
                "absolute top-0.5 left-0.5 size-3 rounded-full bg-background shadow-border",
                row.on && "translate-x-3",
              )}
            />
          </span>
        </div>
      ))}
    </div>
  );
}

const AVATARS = [
  { initials: "MK", tint: "bg-sky-500/15 text-sky-700 dark:text-sky-300" },
  {
    initials: "RD",
    tint: "bg-violet-500/15 text-violet-700 dark:text-violet-300",
  },
  {
    initials: "AS",
    tint: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300",
  },
  { initials: "JT", tint: "bg-amber-500/15 text-amber-700 dark:text-amber-300" },
];

function AvatarClusterIllustration() {
  return (
    <div aria-hidden className="pointer-events-none flex items-center select-none">
      <div className="flex -space-x-2">
        {AVATARS.map((avatar) => (
          <span
            key={avatar.initials}
            className={cn(
              "flex size-8 items-center justify-center rounded-full text-[10px] font-semibold ring-2 ring-card",
              avatar.tint,
            )}
          >
            {avatar.initials}
          </span>
        ))}
      </div>
      <span className="ml-3 text-xs text-muted-foreground">
        +23 editing this space
      </span>
    </div>
  );
}

function QueryIllustration() {
  return (
    <div
      aria-hidden
      className="pointer-events-none rounded-lg border border-border bg-muted/40 p-3 font-mono text-[11px] leading-relaxed select-none"
    >
      <p>
        <span className="text-muted-foreground">SELECT</span> account_id,
        sum(amount)
      </p>
      <p>
        <span className="text-muted-foreground">FROM</span> events.checkout
      </p>
      <p>
        <span className="text-muted-foreground">WHERE</span> ts &gt; now() - 7d
      </p>
    </div>
  );
}

const AUDIT_ROWS = [
  { text: "s.okafor exported cohort", time: "2m" },
  { text: "schema change approved", time: "1h" },
  { text: "API key rotated", time: "3h" },
];

function AuditIllustration() {
  return (
    <div aria-hidden className="pointer-events-none select-none space-y-1.5">
      {AUDIT_ROWS.map((row) => (
        <div
          key={row.text}
          className="flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5"
        >
          <span className="size-1.5 shrink-0 rounded-full bg-emerald-500" />
          <span className="truncate text-[11px] text-muted-foreground">
            {row.text}
          </span>
          <span className="ml-auto text-[10px] tabular-nums text-muted-foreground/70">
            {row.time}
          </span>
        </div>
      ))}
    </div>
  );
}

/* ---- Cells ---- */

const CELLS: {
  title: string;
  description: string;
  large?: boolean;
  illustration: React.ReactNode;
}[] = [
  {
    title: "Real-time pipelines",
    description:
      "Stream product events into your warehouse with sub-second latency — no batch windows, no stale dashboards.",
    large: true,
    illustration: <SparklineIllustration />,
  },
  {
    title: "Granular governance",
    description:
      "Masking, row-level access, and sharing policies enforced at the query layer, not in a wiki.",
    large: true,
    illustration: <ToggleRowsIllustration />,
  },
  {
    title: "Built for teams",
    description: "Shared spaces with live presence and comment threads.",
    illustration: <AvatarClusterIllustration />,
  },
  {
    title: "Query anywhere",
    description: "One SQL surface across warehouse, lake, and stream.",
    illustration: <QueryIllustration />,
  },
  {
    title: "Complete audit trail",
    description: "Every export, schema change, and key rotation, logged.",
    illustration: <AuditIllustration />,
  },
];

export interface FeaturesBentoProps extends React.ComponentProps<"section"> {
  eyebrow?: string;
  headline?: string;
  subcopy?: string;
}

/**
 * Five-cell bento grid (two large, three small). Each cell is an outline
 * card with a small self-drawn illustration; hover lifts the depth border
 * to shadow-border-hover. Cells reveal with a 40ms stagger, first
 * in-view only.
 */
export function FeaturesBento({
  eyebrow = "Platform",
  headline = "Everything your data team ships on",
  subcopy = "Fathom replaces the glue code between your event stream, warehouse, and BI layer with one governed platform.",
  className,
  ...props
}: FeaturesBentoProps) {
  const ref = React.useRef<HTMLElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });
  const reduced = useReducedMotion();
  const revealed = inView || !!reduced;

  return (
    <section
      ref={ref}
      aria-labelledby="features-bento-headline"
      className={cn("w-full bg-background px-4 py-16 sm:px-6 md:py-24", className)}
      {...props}
    >
      <div className="mx-auto w-full max-w-5xl">
        <div
          className={cn("max-w-2xl", REVEAL, revealed ? SHOWN : HIDDEN)}
          style={{ transitionDelay: "0ms" }}
        >
          <p className="text-sm font-medium text-muted-foreground">{eyebrow}</p>
          <h2
            id="features-bento-headline"
            className="mt-2 text-3xl font-semibold tracking-tight text-balance sm:text-4xl"
          >
            {headline}
          </h2>
        </div>
        <p
          className={cn(
            "mt-4 max-w-xl text-base text-pretty text-muted-foreground",
            REVEAL,
            revealed ? SHOWN : HIDDEN,
          )}
          style={{ transitionDelay: "80ms" }}
        >
          {subcopy}
        </p>

        <div className="mt-10 grid gap-4 md:grid-cols-6">
          {CELLS.map((cell, i) => (
            <div
              key={cell.title}
              className={cn(
                "flex flex-col justify-between gap-5 rounded-xl bg-card p-6 shadow-border transition-[box-shadow] duration-150 ease-out hover:shadow-border-hover",
                cell.large ? "md:col-span-3" : "md:col-span-2",
                REVEAL,
                revealed ? SHOWN : HIDDEN,
              )}
              style={{ transitionDelay: `${160 + i * 40}ms` }}
            >
              <div>
                <h3 className="text-sm font-medium">{cell.title}</h3>
                <p className="mt-1.5 text-sm text-pretty text-muted-foreground">
                  {cell.description}
                </p>
              </div>
              {cell.illustration}
            </div>
          ))}
        </div>
      </div>
    </section>
  );
}