# Product Hero (Moonarc)

A product hero: a headline revealed word by word, two actions, a play chip that opens the demo video in a dialog, and the product screenshot in a browser window that leans back 20 degrees and stands upright as the page scrolls. The tilt is CSS on the scroll timeline. The section ships no script of its own; VideoDialog's script loads only when video is set.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/hero-product` (license key required)
- Tier B · category section · trigger load, scroll, click
- Readout: `<HeroProduct headline="…" shot={{ src, alt }} video={{ src, poster }}>`
- Browser support: limited (Chrome 135 · not Firefox · Safari 26.2); elsewhere: Firefox shows the frame upright and still. The video opens through the inline polyfill in browsers without invoker commands (Safari before 26.2, Firefox before 144), unless polyfill is false.
- Measured cost: 0 B JS of its own · uses Reveal + runtime (with dependencies 3.0 kB raw; CSS 23.2 kB raw)
- Page: https://moonarc.dev/components/hero-product/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `level` | `1 | 2` | `1` | Heading level of the headline: 1 on the page's own hero, 2 under another h1 (a demo). |
| `headline` | `string` | none | Headline, split on the server and revealed word by word. The headline slot wins over it. |
| `eyebrow` | `string` | none | Small label above the headline. The eyebrow slot wins over it. |
| `copy` | `string` | none | Supporting copy. The copy slot wins over it. |
| `primary` | `{ href, label }` | none | The main action, a link drawn as an ArrowButton. The actions slot wins over primary and secondary. |
| `secondary` | `{ href, label }` | none | A second link beside it. |
| `shot` | `{ src, alt, dark? }` | none | The product screenshot in the frame: an imported image (optimised by Astro) or a URL. alt describes what the product shows. dark is a second capture shown instead under the dark scheme (data-theme="dark" on the root, or a dark system with nothing chosen). The media slot wins over it and can hold anything. |
| `url` | `string` | `'https://example.com'` | The address in the frame's bar. |
| `tabs` | `string[]` | none | Tab labels in the frame's bar; the first is the active one. |
| `video` | `{ src, poster, title?, label?, tracks? }` | none | The demo video: a file or several (webm first, then mp4), a poster (URL or imported image), the dialog's name (title, default 'Product demo'), the play chip's name and visible label (label, default 'Watch the demo') and WebVTT caption tracks. Without tracks, captions are yours to add. Unset, no chip and no script. |
| `polyfill` | `boolean` | `true` | Adds the inline click handler (403 B raw) that opens the dialog in browsers without invoker commands. Pass false once every browser you support has them. |
| `tilt` | `number` | `20` | Start tilt of the frame, in degrees; 0 turns the tilt off. |
| `range` | `string` | `'0px 50vh'` | The stretch of scroll the frame stands up over, as a CSS animation-range on the nearest scroller's timeline (the page, or a scroll box around the section). The section is meant for the top of a page. |
| `tone` | `'surface' | 'muted' | 'ink' | 'brand'` | `unset (paints nothing)` | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. Each paints --background, --ma-tone-muted (over --muted, with a --border hairline), --ma-tone-ink or --ma-tone-brand (over --primary) with the matching text. With theme.css, a painted tone also re-declares --card, --border, --input and --ring inside the section. The primary action inverts on ink and brand. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding and the gap above the frame ×0.8 / ×1 / ×1.25 through --ma-density. |
| `align` | `'center' | 'start'` | `'center'` | Alignment of the text and the actions; the frame spans the column either way. |

## Usage

```astro
---
import HeroProduct from '@/components/moonarc/pro/HeroProduct.astro';
import dashboard from '../assets/dashboard-light.png';
import dashboardDark from '../assets/dashboard-dark.png';
---
<HeroProduct
  eyebrow="Release 2.4"
  headline="See every order the moment it ships"
  copy="One dashboard for stock, orders and returns, updated as they happen."
  primary={{ href: '/sign-up/', label: 'Start free' }}
  secondary={{ href: '/pricing/', label: 'Pricing' }}
  shot={{ src: dashboard, dark: dashboardDark, alt: 'The dashboard: orders this week, stock by warehouse and a returns table' }}
  url="https://app.example.com"
  video={{ src: ['/video/demo.webm', '/video/demo.mp4'], poster: '/video/demo.jpg', title: 'Product tour, 2 minutes', tracks: [{ src: '/video/demo.en.vtt', srclang: 'en', label: 'English', default: true }] }}
/>
```

## Reduced motion

The frame is upright and still, the headline appears whole without the word stagger, and the play chip does not breathe.

## With ClientRouter

Inherits SplitText, Reveal, ScrollScrub and VideoDialog; nothing to wire. The video pauses when a navigation removes it.

## Craft

- The frame leans back about its middle line with a 1200 px perspective, so the top recedes and the bottom comes forward; the section clips what passes its sides.
- The tilt reads the scroll of the page, not the frame's view timeline: at load the frame is already part way through any view range, so it would start half upright.
- The play chip is VideoDialog's own button drawn as a pill; its label is printed from the button's accessible name, so the words shown and the words read are one string.
- Headline first, copy at 300 ms, actions at 450 ms; the frame is still until the reader scrolls.

## Replaces

- ContainerScroll (Aceternity)
- Hero with app screenshot (Tailwind Plus)
- HeroVideoDialog (Magic UI)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/hero-product` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: split-text, reveal, arrow-button, scroll-scrub, browser-frame, video-dialog.
