Sections

Hero Centered

Centered hero with announcement pill, dual CTAs, and a CSS-built dashboard frame that reveals in three staggered groups.

Install

npx shadcn@latest add @paragon/hero-centered

Also installs: button

hero-centered.tsx

"use client";

import * as React from "react";
import { useInView, useReducedMotion } from "motion/react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/paragon/ui/button";

/* Section reveal language: opacity + 12px rise + 4px blur, 3 groups
   staggered 80ms, first in-view only. Reduced motion renders the final
   state immediately (no movement to remove — `revealed` is true on
   first paint). */
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]";

const SIDEBAR_ITEMS = ["Overview", "Revenue", "Forecasts", "Reports", "Alerts"];

const STATS = [
  { label: "MRR", value: "$284,610", delta: "+8.2%" },
  { label: "Net retention", value: "118%", delta: "+2.1%" },
  { label: "Active accounts", value: "1,842", delta: "+64" },
];

const CHART_BARS = [34, 48, 41, 56, 50, 63, 58, 72, 66, 78, 84, 92];

export interface HeroCenteredProps extends React.ComponentProps<"section"> {
  announcement?: string;
  headline?: string;
  subcopy?: string;
  primaryCta?: string;
  secondaryCta?: string;
}

/**
 * Centered marketing hero: announcement pill, balanced headline, subcopy,
 * dual CTAs, and a CSS-built product frame (fake dashboard made of real
 * elements) under a shadow-overlay with a top-edge highlight.
 */
export function HeroCentered({
  announcement = "Now in GA: revenue anomaly detection",
  headline = "Understand every dollar of revenue",
  subcopy = "Trellis unifies billing, CRM, and product data into one revenue model — with forecasts your CFO will actually sign off on.",
  primaryCta = "Start free trial",
  secondaryCta = "Book a demo",
  className,
  ...props
}: HeroCenteredProps) {
  const ref = React.useRef<HTMLElement>(null);
  const inView = useInView(ref, { once: true, margin: "-80px" });
  const reduced = useReducedMotion();
  const revealed = inView || !!reduced;
  const group = (i: number): React.CSSProperties => ({
    transitionDelay: `${i * 80}ms`,
  });

  return (
    <section
      ref={ref}
      aria-labelledby="hero-centered-headline"
      className={cn(
        "relative w-full overflow-hidden bg-background px-4 py-16 sm:px-6 md:py-24",
        className,
      )}
      {...props}
    >
      {/* Faint top wash — the only background effect. */}
      <div
        aria-hidden
        className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-[radial-gradient(70%_100%_at_50%_0%,var(--color-muted),transparent)] opacity-70"
      />

      <div className="relative mx-auto max-w-3xl text-center">
        <div className={cn(REVEAL, revealed ? SHOWN : HIDDEN)} style={group(0)}>
          <a
            href="#"
            className="inline-flex items-center gap-1.5 rounded-full bg-card py-1 pr-2.5 pl-3 text-xs font-medium text-muted-foreground shadow-border transition-[box-shadow,color] duration-150 ease-out hover:text-foreground hover:shadow-border-hover"
          >
            <span
              aria-hidden
              className="size-1.5 rounded-full bg-emerald-500"
            />
            {announcement}
            <ArrowRight aria-hidden className="size-3" />
          </a>
          <h1
            id="hero-centered-headline"
            className="mt-6 text-4xl font-semibold tracking-tight text-balance sm:text-5xl"
          >
            {headline}
          </h1>
        </div>

        <div className={cn(REVEAL, revealed ? SHOWN : HIDDEN)} style={group(1)}>
          <p className="mx-auto mt-4 max-w-xl text-base text-pretty text-muted-foreground sm:text-lg">
            {subcopy}
          </p>
          <div className="mt-8 flex flex-wrap items-center justify-center gap-3">
            <Button size="lg">{primaryCta}</Button>
            <Button size="lg" variant="outline">
              {secondaryCta}
            </Button>
          </div>
        </div>

        <p
          className={cn(
            "mt-4 text-xs text-muted-foreground",
            REVEAL,
            revealed ? SHOWN : HIDDEN,
          )}
          style={group(1)}
        >
          Free for 14 days · No credit card required
        </p>
      </div>

      {/* Product frame — a fake dashboard built from real elements. */}
      <div
        aria-hidden
        className={cn(
          "pointer-events-none relative mx-auto mt-14 w-full max-w-4xl select-none",
          REVEAL,
          revealed ? SHOWN : HIDDEN,
        )}
        style={group(2)}
      >
        <div className="relative overflow-hidden rounded-xl bg-card text-left shadow-overlay">
          {/* Top-edge highlight. */}
          <div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-foreground/25 to-transparent" />

          <div className="flex">
            <aside className="hidden w-44 shrink-0 flex-col gap-0.5 border-r border-border p-3 sm:flex">
              <div className="mb-2 flex items-center gap-2 px-2 py-1.5">
                <div className="size-4 rounded-sm bg-primary" />
                <span className="text-xs font-semibold">Trellis</span>
              </div>
              {SIDEBAR_ITEMS.map((item, i) => (
                <div
                  key={item}
                  className={cn(
                    "rounded-md px-2 py-1.5 text-[11px]",
                    i === 0
                      ? "bg-muted font-medium text-foreground"
                      : "text-muted-foreground",
                  )}
                >
                  {item}
                </div>
              ))}
            </aside>

            <div className="min-w-0 flex-1 p-4">
              <div className="flex items-center justify-between gap-2">
                <div>
                  <p className="text-xs font-medium">Revenue overview</p>
                  <p className="text-[10px] text-muted-foreground">
                    Q3 2026 · updated 2m ago
                  </p>
                </div>
                <div className="rounded-md bg-primary px-2 py-1 text-[10px] font-medium text-primary-foreground">
                  Export
                </div>
              </div>

              <div className="mt-3 grid grid-cols-3 gap-2">
                {STATS.map((stat) => (
                  <div
                    key={stat.label}
                    className="rounded-lg border border-border p-2.5"
                  >
                    <p className="truncate text-[10px] text-muted-foreground">
                      {stat.label}
                    </p>
                    <p className="mt-0.5 truncate text-sm font-semibold tabular-nums">
                      {stat.value}
                    </p>
                    <p className="text-[10px] tabular-nums text-emerald-600 dark:text-emerald-400">
                      {stat.delta}
                    </p>
                  </div>
                ))}
              </div>

              <div className="mt-3 rounded-lg border border-border p-3">
                <div className="flex items-baseline justify-between">
                  <p className="text-[10px] font-medium">
                    Monthly recurring revenue
                  </p>
                  <p className="text-[10px] text-muted-foreground">Jan – Dec</p>
                </div>
                <div className="mt-2 flex h-24 items-end gap-1.5">
                  {CHART_BARS.map((height, i) => (
                    <div
                      key={i}
                      className={cn(
                        "flex-1 rounded-t-sm",
                        i === CHART_BARS.length - 1
                          ? "bg-primary"
                          : "bg-primary/15",
                      )}
                      style={{ height: `${height}%` }}
                    />
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}