{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-editorial",
  "title": "Editorial Hero",
  "author": "ecomcn <hello@ecomcn.dev>",
  "description": "An asymmetric split hero: an oversized serif headline on the left, a campaign image on the right with a product caption sitting on it. 1.05fr / 0.95fr, not 50/50. The image is the page's largest contentful paint, so it loads eagerly with fetchpriority=high inside a real aspect-ratio box and never shifts; renderImage hands the props to next/image. No state and no \"use client\": it renders on the server.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/registry/ecomcn/hero-editorial/hero-editorial.tsx",
      "content": "import * as React from \"react\"\nimport { ArrowUpRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\n/*\n * The top of the home page: an oversized headline on the left, a campaign\n * image on the right with a product caption sitting on it.\n *\n *   <HeroEditorial\n *     eyebrow=\"Autumn / Winter\"\n *     title=\"Made to be resoled.\"\n *     actions={<a href=\"/shop\" className={buttonVariants({ size: \"lg\" })}>Shop the collection</a>}\n *     image={{ src: \"/campaign.jpg\", alt: \"The Vester boot on a stone step\", width: 1600, height: 2000 }}\n *     caption={{ title: \"Vester Chelsea Boot\", price: 420, href: \"/p/vester\" }}\n *   />\n *\n * A 1.05fr / 0.95fr split rather than 50/50 — the small imbalance is what\n * stops it looking like a template. The caption sits on the image, not\n * under it. The image is almost always the page's largest contentful paint,\n * so it loads eagerly with `fetchpriority=\"high\"` inside a box with a real\n * aspect ratio, and the page never shifts when it arrives. No state, no\n * \"use client\": it renders on the server, so the image is in the first\n * HTML.\n */\n\nexport interface HeroImage {\n  src: string\n  alt: string\n  srcSet?: string\n  /** Defaults to the hero's column: \"(min-width: 1024px) 48vw, 100vw\". */\n  sizes?: string\n  width?: number\n  height?: number\n}\n\nexport interface HeroCaption {\n  /** \"Featured\", \"New in\". */\n  eyebrow?: string\n  title: string\n  /** Major units: 420 for $420. */\n  price?: number\n  href?: string\n}\n\nexport interface HeroEditorialProps extends Omit<React.HTMLAttributes<HTMLElement>, \"title\"> {\n  eyebrow?: React.ReactNode\n  title: React.ReactNode\n  description?: React.ReactNode\n  /** Your buttons or links. */\n  actions?: React.ReactNode\n  image: HeroImage\n  /**\n   * Render the image yourself — next/image with `priority`, say. You get\n   * the props the block would put on its own <img>.\n   */\n  renderImage?: (props: React.ImgHTMLAttributes<HTMLImageElement> & { src: string; alt: string }) => React.ReactNode\n  caption?: HeroCaption\n  currency?: string\n  /** Rendered on the server, so it can't follow the visitor: defaults to en-US. */\n  locale?: string\n  /** The image box. Defaults to \"4 / 5\". */\n  aspectRatio?: string\n  /** \"h1\" on a home page; \"h2\" when the page already has one. */\n  headingLevel?: \"h1\" | \"h2\"\n}\n\nexport function HeroEditorial({\n  eyebrow,\n  title,\n  description,\n  actions,\n  image,\n  renderImage,\n  caption,\n  currency = \"USD\",\n  locale = \"en-US\",\n  aspectRatio = \"4 / 5\",\n  headingLevel = \"h1\",\n  className,\n  ...props\n}: HeroEditorialProps) {\n  const Heading = headingLevel\n  const imgProps = {\n    src: image.src,\n    alt: image.alt,\n    srcSet: image.srcSet,\n    sizes: image.sizes ?? \"(min-width: 1024px) 48vw, 100vw\",\n    width: image.width,\n    height: image.height,\n    // The largest paint on the page: fetch it first, never lazily.\n    fetchPriority: \"high\" as const,\n    loading: \"eager\" as const,\n    decoding: \"async\" as const,\n    className: \"absolute inset-0 size-full object-cover\",\n  }\n  const price =\n    caption?.price !== undefined ? new Intl.NumberFormat(locale, { style: \"currency\", currency }).format(caption.price) : null\n\n  const captionBody = caption ? (\n    <>\n      <span className=\"flex flex-col gap-0.5\">\n        <span className=\"text-[11px] tracking-[0.16em] text-muted-foreground uppercase\">{caption.eyebrow ?? \"Featured\"}</span>\n        <span className=\"text-sm font-medium\">{caption.title}</span>\n      </span>\n      {price ? <span className=\"text-sm tabular-nums\">{price}</span> : null}\n      {caption.href ? (\n        <ArrowUpRight\n          aria-hidden\n          className=\"size-4 shrink-0 transition-transform group-hover/caption:translate-x-0.5 group-hover/caption:-translate-y-0.5\"\n        />\n      ) : null}\n    </>\n  ) : null\n  const captionClass =\n    \"absolute right-4 bottom-4 left-4 flex items-center gap-4 border bg-background/90 px-4 py-3 text-foreground backdrop-blur-sm sm:right-auto sm:min-w-64\"\n\n  return (\n    <section\n      data-slot=\"hero-editorial\"\n      className={cn(\"grid items-stretch gap-10 lg:grid-cols-[1.05fr_0.95fr] lg:gap-0\", className)}\n      {...props}\n    >\n      <div data-slot=\"hero-editorial-copy\" className=\"flex flex-col justify-end gap-6 lg:py-16 lg:pr-14\">\n        {eyebrow ? (\n          <p className=\"text-xs tracking-[0.18em] text-muted-foreground uppercase\">{eyebrow}</p>\n        ) : null}\n        <Heading className=\"font-serif text-5xl leading-[0.95] tracking-tight text-balance sm:text-6xl xl:text-7xl\">{title}</Heading>\n        {description ? <p className=\"max-w-md text-[15px] leading-relaxed text-muted-foreground\">{description}</p> : null}\n        {actions ? <div className=\"flex flex-wrap items-center gap-3 pt-2\">{actions}</div> : null}\n      </div>\n\n      <figure data-slot=\"hero-editorial-media\" className=\"relative m-0 overflow-hidden bg-secondary\" style={{ aspectRatio }}>\n        {renderImage ? (\n          renderImage(imgProps)\n        ) : (\n          // eslint-disable-next-line @next/next/no-img-element -- a block can't assume next/image\n          <img {...imgProps} alt={image.alt} />\n        )}\n        {caption ? (\n          caption.href ? (\n            <figcaption>\n              <a href={caption.href} className={cn(captionClass, \"group/caption transition-colors hover:bg-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none\")}>\n                {captionBody}\n              </a>\n            </figcaption>\n          ) : (\n            <figcaption className={captionClass}>{captionBody}</figcaption>\n          )\n        ) : null}\n      </figure>\n    </section>\n  )\n}\n",
      "type": "registry:component",
      "target": "components/ecomcn/hero-editorial.tsx"
    }
  ],
  "categories": [
    "ecommerce",
    "marketing"
  ],
  "type": "registry:block"
}