{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-section",
  "title": "Hero Section",
  "description": "A props-driven landing hero with eyebrow, dual CTAs, social-proof bar and an optional visual that collapses to a centred column when omitted.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "src/registry/blocks/hero-section.tsx",
      "content": "import * as React from \"react\"\nimport { Star } from \"lucide-react\"\nimport { cn } from \"@/lib/utils\"\n\n/**\n * A CTA must actually go somewhere: either an href (a real id on this page or a real\n * route) or an onClick. The type makes a dead button with neither impossible.\n */\nexport type HeroAction =\n  | { label: React.ReactNode; href: string; onClick?: React.MouseEventHandler<HTMLAnchorElement> }\n  | { label: React.ReactNode; href?: undefined; onClick: React.MouseEventHandler<HTMLButtonElement> }\n\nexport interface HeroProofAvatar {\n  /** Used for alt text and the initials fallback — required */\n  name: string\n  /** Falls back to the name's initials when missing, so there is never a broken image */\n  src?: string\n}\n\nexport interface HeroProof {\n  /** Overlapping avatar stack; at most 5 render, the rest collapse into \"+N\" */\n  avatars?: HeroProofAvatar[]\n  /** 0–5; out-of-range values and NaN are clamped. Omit it to hide the stars */\n  rating?: number\n  /** Social-proof copy, e.g. \"Trusted by 4,000+ teams\" */\n  label?: React.ReactNode\n  /** Customer logo row — pass wordmark strings or your own <svg> */\n  logos?: React.ReactNode[]\n}\n\nexport interface HeroSectionProps\n  extends Omit<React.ComponentPropsWithoutRef<\"section\">, \"title\" | \"children\"> {\n  /** Small label above the headline (version, announcement, category) — display only, not clickable */\n  eyebrow?: React.ReactNode\n  title: React.ReactNode\n  subtitle?: React.ReactNode\n  primaryAction?: HeroAction\n  secondaryAction?: HeroAction\n  proof?: HeroProof\n  /** Screenshot, illustration or video. With one the block goes two-column; without it, a centred single column */\n  visual?: React.ReactNode\n  /** Defaults to start when there is a visual, center when there is not */\n  align?: \"start\" | \"center\"\n  /** h1 above the fold; switch to h2 when the block sits mid-page */\n  titleAs?: \"h1\" | \"h2\"\n}\n\nconst CTA_BASE = cn(\n  \"inline-flex min-w-0 items-center justify-center gap-2 rounded-lg px-5 py-2.5\",\n  \"text-sm font-medium whitespace-nowrap transition-colors\",\n  \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background\",\n)\n\nfunction HeroCta({ action, tone }: { action: HeroAction; tone: \"primary\" | \"secondary\" }) {\n  const className = cn(\n    CTA_BASE,\n    tone === \"primary\"\n      ? \"bg-primary text-primary-foreground hover:bg-primary/90\"\n      : \"border hover:bg-primary/10\",\n  )\n\n  // the href branch renders a real link (anchors and routes keep native browser behaviour); otherwise a button with onClick.\n  return action.href === undefined ? (\n    <button className={cn(className, \"cursor-pointer\")} onClick={action.onClick} type=\"button\">\n      {action.label}\n    </button>\n  ) : (\n    <a className={className} href={action.href} onClick={action.onClick}>\n      {action.label}\n    </a>\n  )\n}\n\nfunction initials(name: string) {\n  const parts = name.trim().split(/\\s+/).filter(Boolean)\n  if (parts.length === 0) return \"?\"\n  return (parts[0][0] + (parts.length > 1 ? parts[parts.length - 1][0] : \"\")).toUpperCase()\n}\n\nfunction ProofBar({ proof, centered }: { proof: HeroProof; centered: boolean }) {\n  const shown = proof.avatars?.slice(0, 5) ?? []\n  const overflow = (proof.avatars?.length ?? 0) - shown.length\n  // an out-of-range or NaN rating would break the star row — clamp before use\n  const rating =\n    proof.rating === undefined || !Number.isFinite(proof.rating)\n      ? undefined\n      : Math.min(5, Math.max(0, proof.rating))\n  const stars = rating === undefined ? 0 : Math.round(rating)\n\n  return (\n    <div className={cn(\"flex flex-col gap-4\", centered && \"items-center\")}>\n      <div\n        className={cn(\n          \"flex flex-wrap items-center gap-x-4 gap-y-2\",\n          centered && \"justify-center\",\n        )}\n      >\n        {shown.length > 0 && (\n          <div className=\"flex -space-x-2\">\n            {shown.map(avatar =>\n              avatar.src ? (\n                // eslint-disable-next-line @next/next/no-img-element -- registry components stay framework-agnostic, no next/image\n                <img\n                  alt={avatar.name}\n                  className=\"size-8 rounded-full object-cover ring-2 ring-background\"\n                  key={avatar.name}\n                  src={avatar.src}\n                />\n              ) : (\n                <span\n                  aria-hidden=\"true\"\n                  className=\"flex size-8 items-center justify-center rounded-full bg-muted text-xs font-medium text-muted-foreground ring-2 ring-background\"\n                  key={avatar.name}\n                  title={avatar.name}\n                >\n                  {initials(avatar.name)}\n                </span>\n              ),\n            )}\n            {overflow > 0 && (\n              <span className=\"flex size-8 items-center justify-center rounded-full bg-secondary text-xs font-medium text-secondary-foreground ring-2 ring-background\">\n                +{overflow}\n              </span>\n            )}\n          </div>\n        )}\n\n        {rating !== undefined && (\n          <div className=\"flex items-center gap-1.5\">\n            <span aria-hidden=\"true\" className=\"flex items-center gap-0.5\">\n              {Array.from({ length: 5 }, (_, i) => (\n                <Star\n                  className={cn(\n                    \"size-4\",\n                    i < stars ? \"fill-primary text-primary\" : \"text-muted-foreground/50\",\n                  )}\n                  key={i}\n                />\n              ))}\n            </span>\n            <span className=\"text-sm font-medium tabular-nums\">{rating.toFixed(1)}</span>\n            <span className=\"sr-only\">out of 5</span>\n          </div>\n        )}\n\n        {proof.label && (\n          <p className=\"min-w-0 text-sm break-words text-muted-foreground\">{proof.label}</p>\n        )}\n      </div>\n\n      {proof.logos && proof.logos.length > 0 && (\n        <ul\n          className={cn(\n            \"flex flex-wrap items-center gap-x-6 gap-y-3 text-sm font-medium text-muted-foreground\",\n            centered && \"justify-center\",\n          )}\n        >\n          {proof.logos.map((logo, i) => (\n            <li className=\"min-w-0\" key={i}>\n              {logo}\n            </li>\n          ))}\n        </ul>\n      )}\n    </div>\n  )\n}\n\nexport function HeroSection({\n  eyebrow,\n  title,\n  subtitle,\n  primaryAction,\n  secondaryAction,\n  proof,\n  visual,\n  align,\n  titleAs: Heading = \"h1\",\n  className,\n  ...props\n}: HeroSectionProps) {\n  // with no visual, collapse to a centred single column rather than leaving half the row empty\n  const centered = (align ?? (visual ? \"start\" : \"center\")) === \"center\"\n  const split = Boolean(visual) && !centered\n\n  return (\n    <section className={cn(\"@container/hero w-full\", className)} {...props}>\n      <div\n        className={cn(\n          \"mx-auto flex w-full max-w-6xl flex-col gap-10 px-6 py-16 @2xl/hero:py-20\",\n          centered && \"items-center\",\n          split && \"@5xl/hero:flex-row @5xl/hero:items-center @5xl/hero:gap-16\",\n        )}\n      >\n        {/* min-w-0: a long headline must not push the sibling column (the visual) out of the container */}\n        <div\n          className={cn(\n            \"flex w-full min-w-0 max-w-2xl flex-col gap-6\",\n            centered ? \"items-center text-center\" : \"flex-1 items-start\",\n          )}\n        >\n          {eyebrow && (\n            <p className=\"inline-flex min-w-0 max-w-full items-center gap-2 rounded-full border bg-muted/60 px-3 py-1 text-xs font-medium break-words text-muted-foreground\">\n              {eyebrow}\n            </p>\n          )}\n\n          {/* w-full: the column is an items-start flex, so a fit-content headline lets one\n              unbreakable word (min-content ignores break-words) stretch past the container —\n              measured at 375px, document scrollWidth was 628. Pinning it to the container\n              width is what gives break-words a chance to break. */}\n          <Heading className=\"w-full text-4xl font-semibold tracking-tight text-balance break-words @2xl/hero:text-5xl @5xl/hero:text-6xl\">\n            {title}\n          </Heading>\n\n          {subtitle && (\n            <p className=\"w-full max-w-xl text-base text-pretty break-words text-muted-foreground @2xl/hero:text-lg\">\n              {subtitle}\n            </p>\n          )}\n\n          {(primaryAction || secondaryAction) && (\n            <div\n              className={cn(\n                \"flex w-full flex-wrap items-center gap-3\",\n                centered && \"justify-center\",\n              )}\n            >\n              {primaryAction && <HeroCta action={primaryAction} tone=\"primary\" />}\n              {secondaryAction && <HeroCta action={secondaryAction} tone=\"secondary\" />}\n            </div>\n          )}\n\n          {proof && <ProofBar centered={centered} proof={proof} />}\n        </div>\n\n        {visual && (\n          <div className={cn(\"w-full min-w-0\", split ? \"flex-1\" : \"max-w-3xl\")}>\n            <div className=\"relative aspect-[4/3] overflow-hidden rounded-xl border bg-muted/40 shadow-sm @2xl/hero:aspect-[16/10] [&_img]:size-full [&_img]:object-cover\">\n              {visual}\n            </div>\n          </div>\n        )}\n      </div>\n    </section>\n  )\n}\n\nexport default HeroSection\n",
      "type": "registry:block"
    }
  ],
  "type": "registry:block"
}