# Hero, agency

Studio hero with oversized display type, animated grid, availability CTA and a client wordmark marquee.

- Type: block
- Category: Hero
- Tier: Free
- Dependencies: lucide-react, motion

## Install

```bash
npx shadcn@latest add https://facadeui.com/r/hero-agency.json
```

## Usage

```tsx
import { HeroAgency } from "@/components/facade/hero-agency";

export default function Page() {
  return <HeroAgency />;
}
```

## Source

```tsx
"use client";

import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { ArrowRight, ArrowUpRight, Calendar } from "lucide-react";
import { cn } from "@/lib/utils";

export interface HeroAgencyProps {
  eyebrow?: string;
  /** Headline lines. Wrap a word in *asterisks* to render it in the brand accent italic. */
  headline?: string[];
  description?: string;
  primaryCta?: { label: string; href: string };
  secondaryCta?: { label: string; href: string };
  availability?: string;
  clientsLabel?: string;
  clients?: string[];
  className?: string;
}

const EASE = [0.22, 1, 0.36, 1] as const;

const WORDMARK_STYLES = [
  "font-display font-bold tracking-tight",
  "font-serif italic font-medium",
  "font-mono font-semibold uppercase tracking-widest text-[0.85em]",
  "font-sans font-black uppercase tracking-tighter",
  "font-display font-light tracking-[0.2em] uppercase text-[0.9em]",
  "font-serif font-semibold tracking-tight",
];

function renderLine(line: string) {
  const parts = line.split(/(\*[^*]+\*)/g).filter(Boolean);
  return parts.map((p, i) =>
    p.startsWith("*") && p.endsWith("*") ? (
      <span key={i} className="font-serif font-normal italic text-brand">
        {p.slice(1, -1)}
      </span>
    ) : (
      <React.Fragment key={i}>{p}</React.Fragment>
    ),
  );
}

function AnimatedGrid() {
  const reduce = useReducedMotion();
  const cells = React.useMemo(
    () => [
      [2, 1], [5, 3], [9, 2], [12, 5], [3, 6], [14, 1], [7, 7], [10, 8], [1, 9], [15, 7],
    ],
    [],
  );
  return (
    <div aria-hidden className="pointer-events-none absolute inset-0 -z-10 overflow-hidden">
      <div
        className="absolute inset-0 [mask-image:radial-gradient(ellipse_80%_70%_at_50%_30%,black,transparent)]"
        style={{
          backgroundImage:
            "linear-gradient(to right, color-mix(in oklch, var(--foreground) 7%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklch, var(--foreground) 7%, transparent) 1px, transparent 1px)",
          backgroundSize: "72px 72px",
        }}
      >
        {cells.map(([cx, cy], i) => (
          <motion.span
            key={i}
            className="absolute size-[72px] bg-brand/10"
            style={{ left: cx * 72, top: cy * 72 }}
            initial={{ opacity: 0 }}
            animate={reduce ? { opacity: 0.5 } : { opacity: [0, 1, 0] }}
            transition={{
              duration: 4,
              repeat: reduce ? 0 : Infinity,
              delay: i * 0.9,
              repeatDelay: 3,
              ease: "easeInOut",
            }}
          />
        ))}
      </div>
      {/* Sweeping scan line */}
      {!reduce && (
        <motion.div
          className="absolute inset-x-0 h-40 bg-linear-to-b from-transparent via-brand/[0.07] to-transparent"
          initial={{ top: "-20%" }}
          animate={{ top: "110%" }}
          transition={{ duration: 9, repeat: Infinity, ease: "linear", repeatDelay: 2 }}
        />
      )}
      <div className="absolute left-1/2 top-[-12rem] h-[30rem] w-[60rem] -translate-x-1/2 rounded-full bg-brand/15 blur-[140px]" />
    </div>
  );
}

export function HeroAgency({
  eyebrow = "Northline Studio, Denver CO",
  headline = ["We build brands", "people *remember*."],
  description = "Strategy, identity and conversion-first websites for ambitious local businesses and real estate teams. Senior people only. Launches in six weeks, not six months.",
  primaryCta = { label: "Book a call", href: "#book" },
  secondaryCta = { label: "See our work", href: "#work" },
  availability = "2 spots left for Q1",
  clientsLabel = "Trusted by teams at",
  clients = [
    "Summit Roofing",
    "Mile High Dental",
    "Alder & Vine",
    "COMPASS RIDGE",
    "Peak Realty",
    "Ironwood",
    "Redline Auto",
    "Larimer Coffee",
  ],
  className,
}: HeroAgencyProps) {
  const reduce = useReducedMotion();

  return (
    <section
      className={cn(
        "relative isolate overflow-hidden bg-background text-foreground",
        className,
      )}
    >
      <AnimatedGrid />

      <div className="mx-auto max-w-7xl px-5 pb-16 pt-20 sm:px-8 sm:pt-28 lg:pt-36">
        <motion.div
          initial={{ opacity: 0, y: 10 }}
          animate={{ opacity: 1, y: 0 }}
          transition={{ duration: 0.6, ease: EASE }}
          className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground"
        >
          <span className="inline-flex items-center gap-2 rounded-full border border-border bg-card/70 px-3 py-1 font-medium backdrop-blur">
            <span className="size-1.5 rounded-full bg-brand" aria-hidden />
            {eyebrow}
          </span>
          <span className="hidden sm:inline">Brand, Web, Growth</span>
        </motion.div>

        <h1 className="mt-8 font-display text-[clamp(2.75rem,12vw,3.25rem)] font-semibold leading-[0.92] tracking-[-0.04em] sm:text-[5.5rem] lg:text-[8.5rem]">
          {headline.map((line, i) => (
            <span key={i} className="block overflow-hidden pb-[0.06em]">
              <motion.span
                className="block"
                initial={reduce ? { opacity: 0 } : { y: "110%", rotate: 2 }}
                animate={reduce ? { opacity: 1 } : { y: "0%", rotate: 0 }}
                transition={{ duration: 1.1, delay: 0.1 + i * 0.12, ease: EASE }}
              >
                {renderLine(line)}
              </motion.span>
            </span>
          ))}
        </h1>

        <div className="mt-10 grid gap-10 lg:mt-14 lg:grid-cols-[1fr_auto] lg:items-end">
          <motion.p
            initial={{ opacity: 0, y: 12 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.8, delay: 0.4, ease: EASE }}
            className="max-w-xl text-lg leading-relaxed text-muted-foreground text-pretty sm:text-xl"
          >
            {description}
          </motion.p>

          <motion.div
            initial={{ opacity: 0, y: 12 }}
            animate={{ opacity: 1, y: 0 }}
            transition={{ duration: 0.8, delay: 0.5, ease: EASE }}
            className="flex flex-col gap-4 sm:flex-row sm:items-center"
          >
            <div className="flex flex-col items-start gap-2.5">
              <a
                href={primaryCta.href}
                className="group inline-flex h-14 items-center gap-3 rounded-full bg-primary pl-2 pr-6 text-[15px] font-semibold text-primary-foreground transition-transform hover:-translate-y-px focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
              >
                <span className="flex size-10 items-center justify-center rounded-full bg-brand text-brand-foreground transition-transform duration-500 group-hover:rotate-[-8deg]">
                  <Calendar className="size-4" aria-hidden />
                </span>
                {primaryCta.label}
                <ArrowRight className="size-4 transition-transform group-hover:translate-x-1" aria-hidden />
              </a>
              <span className="flex items-center gap-2 pl-2 text-sm text-muted-foreground">
                <span className="relative flex size-2" aria-hidden>
                  <span className="absolute inline-flex size-full animate-ping-slow rounded-full bg-success/70" />
                  <span className="relative inline-flex size-2 rounded-full bg-success" />
                </span>
                {availability}
              </span>
            </div>
            <a
              href={secondaryCta.href}
              className="group inline-flex h-14 items-center gap-1.5 self-start rounded-full px-5 text-[15px] font-semibold text-foreground underline-offset-4 transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
            >
              {secondaryCta.label}
              <ArrowUpRight className="size-4 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" aria-hidden />
            </a>
          </motion.div>
        </div>
      </div>

      {/* Client marquee */}
      <motion.div
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        transition={{ duration: 1, delay: 0.8 }}
        className="border-y border-border bg-card/40 backdrop-blur"
      >
        <div className="mx-auto flex max-w-7xl flex-col gap-4 px-5 py-6 sm:px-8 md:flex-row md:items-center md:gap-10">
          <p className="shrink-0 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
            {clientsLabel}
          </p>
          <div
            className="group relative flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]"
            style={{ ["--marquee-gap" as string]: "3.5rem", ["--marquee-duration" as string]: "38s" }}
          >
            <ul
              className="flex w-max shrink-0 animate-marquee items-center gap-[var(--marquee-gap)] group-hover:[animation-play-state:paused]"
            >
              {[...clients, ...clients].map((c, i) => (
                <li
                  key={i}
                  aria-hidden={i >= clients.length}
                  className={cn(
                    "whitespace-nowrap text-xl text-foreground/45 transition-colors hover:text-foreground",
                    WORDMARK_STYLES[i % clients.length % WORDMARK_STYLES.length],
                  )}
                >
                  {c}
                </li>
              ))}
            </ul>
          </div>
        </div>
      </motion.div>
    </section>
  );
}

export default HeroAgency;
```
