Stats

Stats 02 — Narrative Stats

Split layout pairing an eyebrow, headline, description and CTA link with a 2×2 grid of large animated numbers, labels and supporting copy.

$ npx shadcn@latest add @ludusvibe/stats-02
Open in v0

Code

Installed to components/sections/stats-02.tsx — it's yours to edit.

"use client"

import * as React from "react"
import { ArrowRight } from "lucide-react"

import { buttonVariants } from "@/components/ui/button"
import { AnimatedNumber } from "@/components/ludus/animated-number"
import { Reveal } from "@/components/ludus/reveal"
import { cn } from "@/lib/utils"

type Cta = { label: string; href: string }

type Stat = {
  value: number
  label: string
  description: string
  prefix?: string
  suffix?: string
  /** Intl.NumberFormat options, e.g. `{ notation: "compact" }` for 120M-style values. */
  format?: Intl.NumberFormatOptions
}

export type Stats02Props = {
  eyebrow?: string
  title?: React.ReactNode
  description?: string
  cta?: Cta
  /** Rendered as a 2×2 grid of animated numbers. */
  stats?: Stat[]
  /** Heading element — use "h1" when this is the page's main heading. */
  as?: "h1" | "h2"
  className?: string
}

const defaultStats: Stat[] = [
  {
    value: 8700,
    suffix: "+",
    label: "Customers worldwide",
    description: "Teams of every size run on Lumen, across 60+ industries.",
  },
  {
    value: 3.4,
    suffix: "×",
    format: { maximumFractionDigits: 1 },
    label: "Faster time to insight",
    description: "Median across teams migrating from legacy BI stacks.",
  },
  {
    value: 120_000_000,
    format: { notation: "compact" },
    label: "Events every day",
    description: "Ingested, enriched and ready to query in seconds.",
  },
  {
    value: 99.98,
    suffix: "%",
    format: { maximumFractionDigits: 2 },
    label: "Uptime, last 12 months",
    description: "Backed by an enterprise SLA and 24/7 on-call support.",
  },
]

export function Stats02({
  eyebrow = "By the numbers",
  title = "Growth that speaks for itself",
  description = "Lumen powers product decisions at thousands of companies — from two-person startups to global enterprises. Here's what that looks like in practice.",
  cta = { label: "Read customer stories", href: "#" },
  stats = defaultStats,
  as: Heading = "h2",
  className,
}: Stats02Props) {
  return (
    <section className={cn("bg-background px-6 py-24", className)}>
      <div className="mx-auto grid max-w-6xl items-center gap-14 lg:grid-cols-2 lg:gap-20">
        <div>
          <Reveal>
            <p className="text-sm font-medium tracking-widest text-primary uppercase">
              {eyebrow}
            </p>
          </Reveal>
          <Reveal delay={0.1}>
            <Heading className="mt-4 max-w-lg text-balance font-heading text-4xl font-semibold tracking-tight md:text-5xl">
              {title}
            </Heading>
          </Reveal>
          <Reveal delay={0.2}>
            <p className="mt-5 max-w-lg text-pretty text-muted-foreground md:text-lg">
              {description}
            </p>
          </Reveal>
          <Reveal delay={0.3}>
            <a
              href={cta.href}
              className={cn(
                buttonVariants({ size: "lg", variant: "outline" }),
                "group mt-8"
              )}
            >
              {cta.label}
              <ArrowRight className="transition-transform group-hover:translate-x-0.5" />
            </a>
          </Reveal>
        </div>

        <div className="grid gap-x-8 gap-y-10 sm:grid-cols-2">
          {stats.map((stat, i) => (
            <Reveal key={i} delay={0.1 + i * 0.1} className="border-t pt-6">
              <div className="font-heading text-4xl font-semibold tracking-tighter md:text-5xl">
                <AnimatedNumber
                  value={stat.value}
                  prefix={stat.prefix}
                  suffix={stat.suffix}
                  format={stat.format}
                />
              </div>
              <div className="mt-3 font-medium">{stat.label}</div>
              <p className="mt-1 text-sm text-pretty text-muted-foreground">
                {stat.description}
              </p>
            </Reveal>
          ))}
        </div>
      </div>
    </section>
  )
}