Five-cell bento feature grid with self-drawn illustrations per cell and a 40ms reveal stagger.
npx shadcn@latest add @paragon/features-bento"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 > 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>
);
}