{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hero-section",
  "type": "registry:block",
  "title": "Hero section",
  "description": "Three full screen SaaS heroes: a live dashboard rising from the bottom edge over a drifting mesh, a workflow graph that routes sample events node by node, and editorial type over a mesh gradient.",
  "categories": [
    "blocks"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "https://irisui.dev/r/foundation.json",
    "https://irisui.dev/r/utils.json",
    "https://irisui.dev/r/button.json",
    "https://irisui.dev/r/copy-button.json",
    "https://irisui.dev/r/line-chart.json",
    "https://irisui.dev/r/sparkline.json",
    "https://irisui.dev/r/segmented-control.json",
    "https://irisui.dev/r/avatar.json",
    "https://irisui.dev/r/motion-tokens.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/hero-section/hero-cadence.tsx",
      "content": "\"use client\";\n\nimport { MotionConfig, motion } from \"motion/react\";\nimport { cn } from \"../../lib/utils\";\nimport { ActionLink, heroDescription, heroScreen, heroShell, heroTitle } from \"./hero-content\";\nimport type { HeroAction } from \"./hero-content\";\nimport { HeroMesh } from \"./hero-mesh\";\nimport type { HeroMeshPoint } from \"./hero-mesh\";\nimport { heroGroup, heroRise } from \"./hero-motion\";\n\nexport interface HeroCadenceProps {\n  primaryAction?: HeroAction;\n  secondaryAction?: HeroAction;\n  animateIn?: boolean;\n  className?: string;\n}\n\n/** Six points, authored the way the Gradient mesh editor saves them. Colors live in the class list, one palette per theme. */\nconst MESH: HeroMeshPoint[] = [\n  { x: 0.1, y: 0.88, spread: 0.72 },\n  { x: 0.86, y: 0.12, spread: 0.66 },\n  { x: 0.16, y: 0.14, spread: 0.56 },\n  { x: 0.62, y: 0.62, spread: 0.5 },\n  { x: 0.94, y: 0.92, spread: 0.48 },\n  { x: 0.44, y: 0.3, spread: 0.34 },\n];\n/** The mesh is the whole screen, so its palette is authored per theme rather than derived from tokens. */\nconst MESH_COLORS =\n  \"-z-1 [--mesh-1:#bdd3ec] [--mesh-2:#c3c6ff] [--mesh-3:#ffd0b0] [--mesh-4:#f6bfd6] [--mesh-5:#cfe8dc] [--mesh-6:#fff3dc] [--mesh-base:#f2ede7] dark:[--mesh-1:#0d3a47] dark:[--mesh-2:#2a2f86] dark:[--mesh-3:#5a2a3c] dark:[--mesh-4:#3a1f5a] dark:[--mesh-5:#7a3a22] dark:[--mesh-6:#12142c] dark:[--mesh-base:#09090f]\";\n\n/** Sibling products, set as text wordmarks. */\nconst PRODUCTS = [\"CapyDB\", \"notdocs\", \"MyRoomie\", \"Meltemi\", \"Entrolytics\", \"Nami\", \"mpath\"];\n\n/** Minimal hero, one full screen: large type over a slowly drifting mesh gradient with grain, and sibling products along the bottom edge. Sample product: Anasa, a local-first notes app. */\nexport function HeroCadence({\n  primaryAction = { label: \"Download for Mac\", doneLabel: \"Download started\" },\n  secondaryAction = { label: \"Windows and Linux\", doneLabel: \"Opening downloads\" },\n  animateIn = true,\n  className,\n}: HeroCadenceProps) {\n  const item = heroRise;\n  const soft = \"text-[color-mix(in_oklab,var(--ink)_62%,transparent)]\";\n  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.\n  // The scrim under the copy keeps body text above 4.5:1 wherever the mesh drifts.\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section\n        className={cn(\n          heroShell,\n          heroScreen,\n          \"isolate grid after:pointer-events-none after:absolute after:inset-0 after:-z-1 after:bg-[linear-gradient(to_bottom,transparent_40%,color-mix(in_oklab,var(--page)_22%,transparent))] after:content-['']\",\n          className,\n        )}\n      >\n        <HeroMesh className={MESH_COLORS} points={MESH} grain={0.55} speed={0.45} />\n        <motion.div\n          className=\"mx-auto box-border grid w-full max-w-[1320px] grid-rows-[1fr_auto_auto_1fr_auto] gap-10 px-10 pt-[clamp(88px,12svh,128px)] pb-10 @max-[560px]/hero:gap-6 @max-[560px]/hero:px-4 @max-[560px]/hero:pt-20 @max-[560px]/hero:pb-6\"\n          variants={heroGroup}\n          initial={animateIn ? \"hidden\" : false}\n          animate=\"shown\"\n        >\n          <motion.h1\n            variants={item}\n            className={cn(\"text-[length:clamp(2.75rem,1rem+7cqi,7rem)]\", heroTitle, \"row-[2] max-w-[14ch]\")}\n          >\n            Your notes, in plain Markdown\n          </motion.h1>\n          <div className=\"row-[3] grid grid-cols-[minmax(0,1fr)_auto] items-end gap-x-16 gap-y-8 @max-[800px]/hero:grid-cols-[minmax(0,1fr)] @max-[800px]/hero:items-start\">\n            <motion.p\n              variants={item}\n              className={cn(heroDescription, \"max-w-[520px] text-[color-mix(in_oklab,var(--ink)_76%,transparent)]\")}\n            >\n              Anasa keeps every note as a Markdown file in a folder you choose, with links, backlinks, a knowledge graph\n              and tasks inside notes. On quieter days the Energy System shows less interface.\n            </motion.p>\n            <motion.div\n              variants={item}\n              className=\"grid justify-items-end gap-3 @max-[800px]/hero:justify-items-start @max-[560px]/hero:w-full\"\n            >\n              <div className=\"flex flex-wrap gap-3 @max-[560px]/hero:w-full @max-[560px]/hero:*:flex-auto\">\n                <ActionLink action={primaryAction} kind=\"primary\" />\n                <ActionLink action={secondaryAction} kind=\"secondary\" />\n              </div>\n              <p className={cn(\"m-0 text-(length:--text-sm)\", soft)}>\n                The local app is free. Hosted services are optional.\n              </p>\n            </motion.div>\n          </div>\n          <motion.div\n            variants={item}\n            className=\"row-[5] grid gap-5 border-t border-[color-mix(in_oklab,var(--ink)_14%,transparent)] pt-6\"\n          >\n            <p className={cn(\"m-0 text-(length:--text-sm)\", soft)}>More from the same studio</p>\n            <ul\n              className=\"m-0 flex list-none flex-wrap justify-between gap-x-8 gap-y-5 p-0 @max-[800px]/hero:grid @max-[800px]/hero:grid-cols-4 @max-[560px]/hero:grid-cols-3 @max-[560px]/hero:gap-x-3 @max-[560px]/hero:gap-y-4\"\n              aria-label=\"Products from the same studio\"\n            >\n              {PRODUCTS.map((name) => (\n                <li\n                  key={name}\n                  className=\"inline-flex items-center gap-[9px] text-(length:--text-lg) font-medium tracking-body text-[color-mix(in_oklab,var(--ink)_78%,transparent)] @max-[560px]/hero:text-(length:--text-base) @max-[560px]/hero:nth-[n+7]:hidden\"\n                >\n                  {name}\n                </li>\n              ))}\n            </ul>\n          </motion.div>\n        </motion.div>\n      </section>\n    </MotionConfig>\n  );\n}\n\nexport default HeroCadence;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-cadence.tsx"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-content.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { MotionConfig, motion } from \"motion/react\";\nimport { ArrowRight, ArrowUpRight, Check } from \"lucide-react\";\nimport { cn } from \"../../lib/utils\";\nimport { Button } from \"../../button/button\";\nimport { CopyButton } from \"../../copy-button/copy-button\";\nimport { heroGroup, heroRise } from \"./hero-motion\";\n\n/** The shell every hero shares: its own size container named `hero`, the page surface and body type. */\nexport const heroShell = \"relative @container/hero overflow-clip bg-page font-body tracking-body text-ink\";\n/** The designed variants are one full screen each; the small viewport unit keeps them clear of mobile browser bars. */\nexport const heroScreen = \"min-h-svh w-full\";\n/** Display headline. Each design passes its own size first, as `cn(size, heroTitle)`: a later font size would drop the leading. */\nexport const heroTitle = \"m-0 font-display font-medium leading-display tracking-display text-balance\";\nexport const heroDescription = \"m-0 text-lg leading-body text-pretty text-ink-2 @max-[560px]/hero:text-base\";\n\nexport interface HeroAction {\n  label: string;\n  href?: string;\n  onClick?: () => void;\n  /** Opens in a new tab and shows an outward arrow. */\n  external?: boolean;\n  /** Shown in place of the label for a moment after a press, such as \"Trial started\". Buttons only. */\n  doneLabel?: string;\n}\n\nexport interface HeroInstallCommand {\n  /** Short label, such as npm or pnpm. The first command is shown. */\n  label: string;\n  command: string;\n}\n\nexport interface HeroContentProps {\n  layout: \"centered\" | \"split\" | \"minimal\";\n  announcement?: HeroAction | null;\n  title: string;\n  description?: string;\n  primaryAction?: HeroAction | null;\n  secondaryAction?: HeroAction | null;\n  install?: HeroInstallCommand[] | null;\n  media?: ReactNode;\n  meta?: string[];\n  animateIn?: boolean;\n  className?: string;\n}\n\nconst arrow = \"flex-none transition-transform duration-fast ease-standard motion-reduce:transition-none\";\nconst linkKinds = {\n  primary: \"bg-ink text-page hover:opacity-90\",\n  secondary: \"border-line bg-surface text-ink hover:bg-hover\",\n};\n\n/** A link or button in the shape of a design-system button. Links keep native navigation; a button with `doneLabel` confirms in place. */\nexport function ActionLink({\n  action,\n  kind,\n  className,\n}: {\n  action: HeroAction;\n  kind: \"primary\" | \"secondary\";\n  className?: string;\n}) {\n  const [done, setDone] = useState(false);\n  useEffect(() => {\n    if (!done) return;\n    const timer = window.setTimeout(() => setDone(false), 2400);\n    return () => window.clearTimeout(timer);\n  }, [done]);\n  const icon = action.external ? (\n    <ArrowUpRight size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n  ) : kind === \"primary\" ? (\n    <ArrowRight\n      className={cn(arrow, action.href && \"group-hover/action:translate-x-0.5\")}\n      size={16}\n      strokeWidth={1.75}\n      aria-hidden=\"true\"\n    />\n  ) : null;\n  if (action.href) {\n    return (\n      <a\n        className={cn(\n          \"group/action inline-flex h-control-md cursor-pointer items-center justify-center gap-2 rounded-button border border-transparent px-5 text-(length:--text-sm) font-medium whitespace-nowrap no-underline [-webkit-tap-highlight-color:transparent]\",\n          \"transition-[scale,background-color,opacity] duration-fast ease-standard active:scale-97 motion-reduce:transition-none motion-reduce:active:scale-100 @max-[560px]/hero:flex-auto\",\n          linkKinds[kind],\n          className,\n        )}\n        href={action.href}\n        onClick={action.onClick}\n        {...(action.external ? { target: \"_blank\", rel: \"noopener noreferrer\" } : {})}\n      >\n        {action.label}\n        {icon}\n      </a>\n    );\n  }\n  return (\n    <Button\n      variant={kind}\n      size=\"lg\"\n      className={className}\n      onClick={() => {\n        action.onClick?.();\n        if (action.doneLabel) setDone(true);\n      }}\n    >\n      {done ? (\n        <>\n          <Check size={16} strokeWidth={1.75} aria-hidden=\"true\" />\n          {action.doneLabel}\n        </>\n      ) : (\n        <>\n          {action.label}\n          {icon}\n        </>\n      )}\n    </Button>\n  );\n}\n\nconst announcementClass =\n  \"group/action inline-flex min-h-8 max-w-full cursor-pointer items-center gap-1.5 rounded-button border border-line bg-surface px-3 py-1 text-left text-(length:--text-sm) text-ink-2 no-underline transition-[color,border-color] duration-fast ease-standard hover:border-line-strong hover:text-ink motion-reduce:transition-none\";\nconst announcementArrow = cn(arrow, \"group-hover/action:translate-x-0.5\");\n\n/**\n * The hero with your own content: announcement, headline, description, actions, an optional install line, and an\n * optional visual beside the copy. Use it when none of the three designed variants fits the product.\n */\nexport function HeroContent({\n  layout,\n  announcement,\n  title,\n  description,\n  primaryAction,\n  secondaryAction,\n  install,\n  media,\n  meta,\n  animateIn = true,\n  className,\n}: HeroContentProps) {\n  const item = heroRise;\n  const command = install?.[0];\n  const start = layout !== \"centered\";\n  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section className={cn(heroShell, className)} data-layout={layout}>\n        <div\n          className={cn(\n            \"mx-auto grid max-w-[1200px] px-10 py-24 @max-[560px]/hero:px-4 @max-[560px]/hero:py-16\",\n            layout === \"split\" &&\n              \"grid-cols-[minmax(0,1fr)_minmax(0,1.05fr)] items-center gap-12 @max-[800px]/hero:grid-cols-[minmax(0,1fr)] @max-[800px]/hero:gap-10\",\n          )}\n        >\n          <motion.div\n            className={cn(\n              \"mx-auto grid max-w-[760px] grid-cols-[minmax(0,1fr)] justify-items-center gap-5 text-center\",\n              start && \"m-0 justify-items-start text-left\",\n              layout === \"minimal\" && \"max-w-[860px]\",\n            )}\n            variants={heroGroup}\n            initial={animateIn ? \"hidden\" : false}\n            animate=\"shown\"\n          >\n            {announcement && (\n              <motion.div variants={item}>\n                {announcement.href ? (\n                  <a className={announcementClass} href={announcement.href} onClick={announcement.onClick}>\n                    {announcement.label}\n                    <ArrowRight className={announcementArrow} size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </a>\n                ) : (\n                  <button type=\"button\" className={announcementClass} onClick={announcement.onClick}>\n                    {announcement.label}\n                    <ArrowRight className={announcementArrow} size={14} strokeWidth={1.75} aria-hidden=\"true\" />\n                  </button>\n                )}\n              </motion.div>\n            )}\n            <motion.h1\n              variants={item}\n              className={cn(\"text-[length:clamp(2.25rem,1.3rem+4.4cqi,4.5rem)]\", heroTitle, \"max-w-[16ch]\")}\n            >\n              {title}\n            </motion.h1>\n            {description && (\n              <motion.p variants={item} className={cn(heroDescription, \"max-w-[560px]\")}>\n                {description}\n              </motion.p>\n            )}\n            {(primaryAction || secondaryAction) && (\n              <motion.div\n                variants={item}\n                className={cn(\n                  \"mt-3 flex flex-wrap justify-center gap-3 @max-[560px]/hero:w-full\",\n                  start && \"justify-start\",\n                )}\n              >\n                {primaryAction && <ActionLink action={primaryAction} kind=\"primary\" />}\n                {secondaryAction && <ActionLink action={secondaryAction} kind=\"secondary\" />}\n              </motion.div>\n            )}\n            {command && (\n              <motion.div\n                variants={item}\n                className=\"mt-2 flex h-control-md w-[min(100%,440px)] items-center gap-2 rounded-control border border-line bg-surface pr-1 pl-4 text-left font-[ui-monospace,'SF_Mono',Menlo,monospace] text-[13px]\"\n              >\n                <span className=\"text-ink-3\" aria-hidden=\"true\">\n                  $\n                </span>\n                <code className=\"flex-1 overflow-hidden [font:inherit] text-ellipsis whitespace-nowrap text-ink\">\n                  {command.command}\n                </code>\n                <CopyButton value={command.command} label=\"Copy install command\" iconOnly variant=\"plain\" />\n              </motion.div>\n            )}\n            {meta && meta.length > 0 && (\n              <motion.ul\n                variants={item}\n                className=\"mx-0 mt-4 mb-0 flex list-none flex-wrap gap-x-5 gap-y-2 border-t border-line-soft px-0 pt-5 pb-0 text-(length:--text-sm) text-ink-3\"\n              >\n                {meta.map((entry) => (\n                  <li key={entry}>{entry}</li>\n                ))}\n              </motion.ul>\n            )}\n          </motion.div>\n          {media && (\n            <motion.div className=\"min-w-0\" variants={item} initial={animateIn ? \"hidden\" : false} animate=\"shown\">\n              {media}\n            </motion.div>\n          )}\n        </div>\n      </section>\n    </MotionConfig>\n  );\n}\n\nexport default HeroContent;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-content.tsx"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-lumen-data.ts",
      "content": "import type { LineChartDatum } from \"@/registry/components/line-chart/line-chart\";\n\n/**\n * Sample data for the HeroLumen dashboard: Entrolytics, privacy-first web analytics, across three websites. Every number\n * is composed for the demo. Replace it with your own product's numbers.\n */\n\nexport type LumenRange = \"30d\" | \"90d\" | \"12m\";\n\nexport const lumenRanges: { value: LumenRange; label: string }[] = [\n  { value: \"30d\", label: \"30D\" },\n  { value: \"90d\", label: \"90D\" },\n  { value: \"12m\", label: \"12M\" },\n];\n\n/** A smooth, deterministic walk: a trend, one slow wave, and a little noise that repeats for the same seed. */\nfunction walk(count: number, start: number, end: number, wave: number, seed: number) {\n  let s = seed;\n  const rand = () => {\n    s = (s * 16807) % 2147483647;\n    return s / 2147483647 - 0.5;\n  };\n  return Array.from({ length: count }, (_, i) => {\n    const t = i / (count - 1);\n    return Math.round(\n      start + (end - start) * (t * t * 0.35 + t * 0.65) + Math.sin(t * Math.PI * 2.3) * wave + rand() * wave * 0.6,\n    );\n  });\n}\n\nconst months = [\"Oct\", \"Nov\", \"Dec\", \"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\"];\n\n/** Visitors per day (30D), per week (90D) or per month (12M), against the period before. */\nfunction series(range: LumenRange): LineChartDatum[] {\n  if (range === \"12m\") {\n    const now = walk(12, 38_000, 78_000, 2_600, 7),\n      before = walk(12, 24_000, 51_500, 2_200, 19);\n    return months.map((month, i) => ({\n      key: `m${i}`,\n      label: `${month} ${i < 3 ? 2025 : 2026}`,\n      axisLabel: i % 2 === 0 ? month : undefined,\n      values: { visitors: now[i], last: before[i] },\n    }));\n  }\n  const days = range === \"30d\" ? 30 : 13;\n  const step = range === \"30d\" ? 1 : 7;\n  const now = walk(\n    days,\n    range === \"30d\" ? 1_900 : 14_200,\n    range === \"30d\" ? 3_100 : 18_240,\n    range === \"30d\" ? 260 : 900,\n    range === \"30d\" ? 3 : 11,\n  );\n  const before = walk(\n    days,\n    range === \"30d\" ? 1_700 : 12_600,\n    range === \"30d\" ? 2_500 : 15_100,\n    range === \"30d\" ? 220 : 700,\n    29,\n  );\n  const end = new Date(Date.UTC(2026, 9, 22));\n  return now.map((value, i) => {\n    const date = new Date(end.getTime() - (days - 1 - i) * step * 86_400_000);\n    const label = date.toLocaleDateString(\"en-GB\", { day: \"numeric\", month: \"short\", timeZone: \"UTC\" });\n    const every = range === \"30d\" ? 7 : 3;\n    return {\n      key: date.toISOString().slice(0, 10),\n      label,\n      axisLabel: (days - 1 - i) % every === 0 ? label : undefined,\n      values: { visitors: value, last: before[i] },\n    };\n  });\n}\n\nexport const lumenSeries: Record<LumenRange, LineChartDatum[]> = {\n  \"30d\": series(\"30d\"),\n  \"90d\": series(\"90d\"),\n  \"12m\": series(\"12m\"),\n};\n\nexport interface LumenKpi {\n  id: string;\n  label: string;\n  tone: \"accent\" | \"success\" | \"warning\" | \"danger\";\n  byRange: Record<LumenRange, { value: string; change: string; data: number[] }>;\n}\n\nexport const lumenKpis: LumenKpi[] = [\n  {\n    id: \"visitors\",\n    label: \"Visitors\",\n    tone: \"accent\",\n    byRange: {\n      \"30d\": { value: \"74.3K\", change: \"+8.2%\", data: walk(12, 66, 74, 4, 5) },\n      \"90d\": { value: \"208K\", change: \"+19.9%\", data: walk(12, 52, 74, 6, 9) },\n      \"12m\": { value: \"694K\", change: \"+51.7%\", data: walk(12, 38, 78, 8, 13) },\n    },\n  },\n  {\n    id: \"pageviews\",\n    label: \"Pageviews\",\n    tone: \"success\",\n    byRange: {\n      \"30d\": { value: \"212K\", change: \"+6.4%\", data: walk(12, 196, 212, 6, 17) },\n      \"90d\": { value: \"598K\", change: \"+14%\", data: walk(12, 168, 212, 8, 21) },\n      \"12m\": { value: \"2.0M\", change: \"+38%\", data: walk(12, 110, 212, 12, 23) },\n    },\n  },\n  {\n    id: \"bounce\",\n    label: \"Bounce rate\",\n    tone: \"success\",\n    byRange: {\n      \"30d\": { value: \"41%\", change: \"−3 pts\", data: walk(12, 44, 41, 1.2, 31) },\n      \"90d\": { value: \"43%\", change: \"−2 pts\", data: walk(12, 45, 43, 1.4, 37) },\n      \"12m\": { value: \"44%\", change: \"−6 pts\", data: walk(12, 50, 44, 1.6, 41) },\n    },\n  },\n  {\n    id: \"visit\",\n    label: \"Median visit\",\n    tone: \"success\",\n    byRange: {\n      \"30d\": { value: \"1 min 48 s\", change: \"+9 s\", data: walk(12, 99, 108, 3, 43) },\n      \"90d\": { value: \"1 min 41 s\", change: \"+12 s\", data: walk(12, 89, 101, 3, 47) },\n      \"12m\": { value: \"1 min 36 s\", change: \"+21 s\", data: walk(12, 75, 96, 4, 53) },\n    },\n  },\n];\n\n/** A row in the top pages list: the page, its visitors this period, and the change against the period before. */\nexport interface LumenMover {\n  name: string;\n  change: string;\n  amount: number;\n}\n\nexport const lumenMovers: LumenMover[] = [\n  { name: \"capydb.dev/pricing\", change: \"4,210 visitors\", amount: 620 },\n  { name: \"capydb.dev/docs/getting-started\", change: \"2,980 visitors\", amount: 410 },\n  { name: \"notdocs.dev\", change: \"2,140 visitors\", amount: 380 },\n  { name: \"entrolytics.dev/blog\", change: \"1,140 visitors\", amount: -90 },\n  { name: \"notdocs.dev/docs/documents\", change: \"860 visitors\", amount: 120 },\n  { name: \"capydb.dev/changelog\", change: \"640 visitors\", amount: -60 },\n];\n\nexport const lumenInsight: Record<LumenRange, { lead: string; rest: string }> = {\n  \"30d\": {\n    lead: \"Visitors rose 8.2% this month.\",\n    rest: \"Most of the rise landed on capydb.dev/pricing, which drew 4,210 visitors.\",\n  },\n  \"90d\": {\n    lead: \"Visitors rose 19.9% this quarter.\",\n    rest: \"The getting started guide is now the second most visited page.\",\n  },\n  \"12m\": {\n    lead: \"Visitors rose 51.7% in twelve months.\",\n    rest: \"Desktop still brings 62% of visits; mobile grew faster.\",\n  },\n};\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-lumen-data.ts"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-lumen.tsx",
      "content": "\"use client\";\n\nimport { useLayoutEffect, useRef, useState } from \"react\";\nimport { MotionConfig, motion, useReducedMotion } from \"motion/react\";\nimport {\n  BarChart3,\n  BookOpen,\n  ChevronsUpDown,\n  Gauge,\n  LayoutDashboard,\n  LineChart as LineIcon,\n  Search,\n  Settings,\n  Sparkles,\n  Users,\n} from \"lucide-react\";\nimport { LineChart } from \"../../line-chart/line-chart\";\nimport { Sparkline } from \"../../sparkline/sparkline\";\nimport SegmentedControl from \"../../segmented-control/segmented-control\";\nimport { Avatar } from \"../../avatar/avatar\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport { ActionLink, heroDescription, heroScreen, heroShell, heroTitle } from \"./hero-content\";\nimport type { HeroAction } from \"./hero-content\";\nimport { HeroMesh } from \"./hero-mesh\";\nimport type { HeroMeshPoint } from \"./hero-mesh\";\nimport { heroGroup, heroRise } from \"./hero-motion\";\nimport { lumenInsight, lumenKpis, lumenMovers, lumenRanges, lumenSeries } from \"./hero-lumen-data\";\nimport type { LumenRange } from \"./hero-lumen-data\";\n\n/** A soft mesh across the whole screen: cool tints at the top, a warm one low behind the window, all a few steps off the page. */\nconst MESH: HeroMeshPoint[] = [\n  { x: 0.1, y: 0.92, spread: 0.7 },\n  { x: 0.92, y: 0.86, spread: 0.66 },\n  { x: 0.14, y: 0.08, spread: 0.64 },\n  { x: 0.86, y: 0.04, spread: 0.6 },\n  { x: 0.5, y: 0.38, spread: 0.46 },\n];\n\n/** The mesh is authored per theme: tints a few steps off the page, so it reads as light rather than color. */\nconst MESH_COLORS =\n  \"-z-1 [--mesh-1:#ffe6d8] [--mesh-2:#e3dcff] [--mesh-3:#dbe4ff] [--mesh-4:#f0e0ff] [--mesh-5:#e0eefc] [--mesh-base:var(--page)] dark:[--mesh-1:#2a1822] dark:[--mesh-2:#1c1a44] dark:[--mesh-3:#141c40] dark:[--mesh-4:#231842] dark:[--mesh-5:#0e2133]\";\n\nconst NAV = [\n  { label: \"Overview\", icon: Gauge, active: true },\n  { label: \"Pageviews\", icon: LineIcon },\n  { label: \"Visitors\", icon: Users },\n  { label: \"Funnels\", icon: BarChart3 },\n  { label: \"Boards\", icon: LayoutDashboard },\n  { label: \"Reports\", icon: BookOpen },\n];\nconst SEGMENTS = [\n  { label: \"Pricing page visitors\", tone: \"accent\" },\n  { label: \"Bounced sessions\", tone: \"danger\" },\n  { label: \"Visitors from GR\", tone: \"neutral\" },\n];\n\nconst count = new Intl.NumberFormat(\"en-US\");\nconst compact = (value: number) => (value >= 1000 ? `${+(value / 1000).toFixed(1)}K` : `${value}`);\n\n/** Width the dashboard is designed at, wide and narrow. It is drawn at that size and scaled to fit, like a screenshot. */\nconst WIDE = 1200,\n  NARROW = 600;\n\nexport interface HeroLumenProps {\n  primaryAction?: HeroAction;\n  secondaryAction?: HeroAction;\n  animateIn?: boolean;\n  className?: string;\n}\n\n/**\n * Centered hero, one full screen: a headline over a drifting mesh, and a live analytics dashboard rising from the bottom edge. The\n * window settles from a tilt as it enters and runs off the bottom of the screen into a fade. It is real markup built from the\n * system's own charts; its range control works.\n */\nexport function HeroLumen({\n  primaryAction = { label: \"Add your website\", doneLabel: \"Website added\" },\n  secondaryAction = { label: \"See the plans\", doneLabel: \"Opening plans\" },\n  animateIn = true,\n  className,\n}: HeroLumenProps) {\n  const calm = !!useReducedMotion();\n  const item = heroRise;\n  const stage = useRef<HTMLDivElement>(null);\n  const [box, setBox] = useState({ width: WIDE, scale: 1 });\n\n  useLayoutEffect(() => {\n    const node = stage.current;\n    if (!node) return;\n    const measure = () => {\n      const css = getComputedStyle(node);\n      const width = node.clientWidth - parseFloat(css.paddingLeft) - parseFloat(css.paddingRight);\n      const design = width < 720 ? NARROW : WIDE;\n      // On a phone the window keeps a legible size and crops evenly at both edges instead of shrinking to fit.\n      setBox({\n        width: design,\n        scale: design === NARROW ? Math.max(0.6, Math.min(1, width / design)) : Math.min(1, width / design),\n      });\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  const narrow = box.width === NARROW;\n  const height = narrow ? 700 : 760;\n\n  // Reduced motion: Motion skips every transform (rise, zoom, tilt entrance); opacity still fades briefly.\n  // The hero is exactly one screen tall (never shorter than 600px); the window takes whatever height the copy leaves.\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section\n        className={cn(\n          heroShell,\n          heroScreen,\n          \"isolate flex h-svh min-h-[600px] flex-col pt-[clamp(88px,14svh,144px)] text-center\",\n          className,\n        )}\n      >\n        <HeroMesh className={MESH_COLORS} points={MESH} grain={0.18} speed={0.55} />\n        <motion.div\n          className=\"relative mx-auto grid max-w-[900px] justify-items-center gap-5 px-10 @max-[560px]/hero:gap-4 @max-[560px]/hero:px-4\"\n          variants={heroGroup}\n          initial={animateIn ? \"hidden\" : false}\n          animate=\"shown\"\n        >\n          <motion.h1\n            variants={item}\n            className={cn(\"text-[length:clamp(2.5rem,1.2rem+5cqi,var(--text-5xl))]\", heroTitle, \"max-w-[14ch]\")}\n          >\n            Web analytics without cookies\n          </motion.h1>\n          <motion.p variants={item} className={cn(heroDescription, \"max-w-[590px]\")}>\n            Entrolytics counts visitors, events and funnels with a script of about 1&nbsp;KB. No cookies, no consent\n            banner, and a dashboard the whole team can read.\n          </motion.p>\n          <motion.div\n            variants={item}\n            className=\"mt-3 flex flex-wrap justify-center gap-3 @max-[560px]/hero:mt-2 @max-[560px]/hero:w-full @max-[560px]/hero:*:flex-auto\"\n          >\n            <ActionLink action={primaryAction} kind=\"primary\" />\n            <ActionLink action={secondaryAction} kind=\"secondary\" />\n          </motion.div>\n          <motion.p variants={item} className=\"m-0 text-(length:--text-sm) text-ink-3\">\n            Free for 25,000 pageviews a month. The script loads in under 50 ms.\n          </motion.p>\n        </motion.div>\n\n        <motion.div\n          ref={stage}\n          className=\"relative mx-auto mt-[calc(var(--space-12)-var(--halo))] box-border min-h-0 w-full max-w-[calc(1200px+2*var(--space-10))] flex-[1_1_0] overflow-hidden px-10 pt-(--halo) [--halo:96px] [mask-image:linear-gradient(to_bottom,#000_62%,transparent_100%)] @max-[560px]/hero:mt-[calc(var(--space-10)-var(--halo))] @max-[560px]/hero:px-0 @max-[560px]/hero:[--halo:56px]\"\n          initial={animateIn ? { opacity: 0, y: 56 } : false}\n          animate={{ opacity: 1, y: 0 }}\n          transition={\n            calm\n              ? { duration: motionTokens.duration.standard }\n              : { duration: 1.1, ease: [...motionTokens.ease.enter], delay: 0.32 }\n          }\n        >\n          <div\n            className=\"absolute top-[calc(var(--halo)-110px)] right-[10%] left-[10%] -z-1 h-[240px] bg-[radial-gradient(50%_50%_at_50%_50%,var(--hero-glow),transparent_72%)] blur-[36px] [--hero-glow:color-mix(in_oklab,color-mix(in_oklab,var(--accent)_35%,#7d8cff)_36%,transparent)] dark:[--hero-glow:color-mix(in_oklab,color-mix(in_oklab,var(--accent)_30%,#6e7cff)_58%,transparent)]\"\n            aria-hidden=\"true\"\n          />\n          <div className=\"flex justify-center perspective-[2200px] perspective-origin-top\">\n            <motion.div\n              className=\"relative flex-none origin-top will-change-transform motion-reduce:will-change-auto\"\n              style={{ width: box.width * box.scale, height: height * box.scale }}\n              initial={animateIn ? { rotateX: 22, scale: 0.94 } : false}\n              animate={{ rotateX: 0, scale: 1 }}\n              transition={{ duration: 1.5, ease: [...motionTokens.ease.enter], delay: 0.36 }}\n            >\n              {/* The scale is a Motion value rather than a CSS transform, so layout animations inside (the date range highlight)\n              are projected through it and slide exactly between options at any screen width. */}\n              <motion.div\n                className=\"absolute top-0 left-0 origin-top-left\"\n                style={{ width: box.width, height, scale: box.scale, originX: 0, originY: 0 }}\n              >\n                <LumenDashboard narrow={narrow} />\n              </motion.div>\n            </motion.div>\n          </div>\n        </motion.div>\n      </section>\n    </MotionConfig>\n  );\n}\n\n/** The Entrolytics app window. Everything is content except the range control, which changes the data the whole screen shows. */\nfunction LumenDashboard({ narrow }: { narrow: boolean }) {\n  const [range, setRange] = useState<LumenRange>(\"30d\");\n  const insight = lumenInsight[range];\n  const panel = \"grid min-w-0 content-start gap-3.5 rounded-[12px] border border-line-soft bg-surface px-[18px] py-4\";\n  const hint = \"text-(length:--text-xs) text-ink-3\";\n  return (\n    <figure\n      className={cn(\n        \"relative m-0 grid h-full grid-rows-[auto_minmax(0,1fr)] overflow-hidden rounded-[16px] border border-line bg-surface text-left text-(length:--text-sm) text-ink shadow-[0_1px_1px_rgb(20_24_40/.04),0_24px_48px_-16px_rgb(20_24_40/.16),0_60px_120px_-40px_rgb(40_48_110/.22)]\",\n        // In the dark a thin light catches the top edge, the way a real display does in a dark room.\n        \"dark:border-[color-mix(in_oklab,var(--ink)_14%,transparent)] dark:shadow-[inset_0_1px_0_color-mix(in_oklab,var(--ink)_9%,transparent),0_24px_48px_-16px_rgb(0_0_0/.6),0_60px_140px_-40px_rgb(40_50_140/.35)]\",\n        \"dark:after:pointer-events-none dark:after:absolute dark:after:top-0 dark:after:right-[12%] dark:after:left-[12%] dark:after:h-px dark:after:bg-[linear-gradient(to_right,transparent,color-mix(in_oklab,#b9c2ff_70%,transparent),transparent)] dark:after:content-['']\",\n      )}\n      data-narrow={narrow || undefined}\n      aria-label=\"Entrolytics traffic dashboard, sample data\"\n    >\n      <div\n        className=\"relative flex h-10 items-center justify-center border-b border-line-soft bg-[color-mix(in_oklab,var(--hover)_55%,var(--surface))]\"\n        aria-hidden=\"true\"\n      >\n        <span className=\"absolute left-4 flex gap-[7px] *:size-[11px] *:rounded-full *:bg-[color-mix(in_oklab,var(--ink)_14%,transparent)]\">\n          <i />\n          <i />\n          <i />\n        </span>\n        <span className=\"rounded-control bg-surface px-3.5 py-[3px] text-(length:--text-xs) text-ink-3 shadow-[inset_0_0_0_1px_var(--line-soft)]\">\n          entrolytics.dev/overview\n        </span>\n      </div>\n      <div className={cn(\"grid min-h-0 grid-cols-[224px_minmax(0,1fr)]\", narrow && \"grid-cols-[minmax(0,1fr)]\")}>\n        {!narrow && (\n          <aside className=\"flex flex-col gap-0.5 border-r border-line-soft bg-[color-mix(in_oklab,var(--hover)_40%,var(--surface))] px-3 py-3.5\">\n            <div className=\"flex items-center gap-2.5 px-2 pt-1.5 pb-2.5\">\n              <span className=\"grid size-[22px] place-items-center rounded-chip bg-ink text-(length:--text-xs) font-medium text-page\">\n                A\n              </span>\n              <span className=\"flex-1 font-medium\">All websites</span>\n              <ChevronsUpDown size={14} strokeWidth={1.75} className=\"text-ink-3\" />\n            </div>\n            <div className=\"mb-2.5 flex items-center gap-2 rounded-[9px] border border-line-soft bg-surface px-2 py-1.5 text-ink-3\">\n              <Search size={14} strokeWidth={1.75} />\n              Search<kbd className=\"ml-auto font-[inherit] text-(length:--text-xs)\">⌘K</kbd>\n            </div>\n            <div className=\"grid gap-px\">\n              {NAV.map(({ label, icon: Icon, active }) => (\n                <div\n                  key={label}\n                  className={cn(\n                    \"flex items-center gap-2.5 rounded-control px-2 py-1.5 text-ink-2 [&_svg]:text-ink-3\",\n                    active && \"bg-accent-tint font-medium text-ink [&_svg]:text-accent\",\n                  )}\n                  data-active={active || undefined}\n                >\n                  <Icon size={15} strokeWidth={1.75} />\n                  {label}\n                </div>\n              ))}\n            </div>\n            <div className={cn(\"mx-2 mt-[18px] mb-1.5\", hint)}>Segments</div>\n            <div className=\"grid gap-px\">\n              {SEGMENTS.map((view) => (\n                <div key={view.label} className=\"flex items-center gap-2.5 rounded-control px-2 py-1.5 text-ink-2\">\n                  <i\n                    className={cn(\n                      \"mx-[3px] size-2 rounded-full bg-ink-3\",\n                      view.tone === \"accent\" && \"bg-accent\",\n                      view.tone === \"danger\" && \"bg-red\",\n                    )}\n                  />\n                  {view.label}\n                </div>\n              ))}\n            </div>\n            <div className=\"mt-auto flex items-center gap-2.5 border-t border-line-soft px-1.5 pt-3 pb-0.5\">\n              <Avatar name=\"Clara Weiss\" size=\"sm\" />\n              <span className=\"grid flex-1 leading-[1.3]\">\n                <strong className=\"font-medium\">Clara Weiss</strong>\n                <small className={hint}>Data analyst</small>\n              </span>\n              <Settings size={15} strokeWidth={1.75} className=\"text-ink-3\" />\n            </div>\n          </aside>\n        )}\n        <div className=\"grid min-w-0 content-start gap-4 px-6 py-[22px]\">\n          <header className=\"flex items-center justify-between gap-4\">\n            <div>\n              <h2 className=\"m-0 text-(length:--text-lg) font-medium tracking-body\">Traffic overview</h2>\n              <p className={cn(\"mt-0.5 mb-0\", hint)}>Live · 847 visitors right now</p>\n            </div>\n            <SegmentedControl\n              label=\"Date range\"\n              options={lumenRanges}\n              value={range}\n              onValueChange={(value) => setRange(value as LumenRange)}\n              className=\"flex-none\"\n            />\n          </header>\n\n          {/* KPI tiles reuse Sparkline; its caption wraps so the label sits above a larger value. */}\n          <div className={cn(\"grid grid-cols-4 gap-3\", narrow && \"grid-cols-2\")}>\n            {lumenKpis.map((kpi) => {\n              const now = kpi.byRange[range];\n              return (\n                <div\n                  key={kpi.id}\n                  className=\"min-w-0 rounded-[12px] border border-line-soft bg-surface px-4 pt-3.5 pb-3 [&_figcaption]:flex-wrap [&_figcaption]:gap-y-1 [&_figcaption_small]:self-center [&_figcaption_strong]:ml-0 [&_figcaption_strong]:text-(length:--text-2xl) [&_figcaption_strong]:tracking-body [&_figcaption>:first-child]:basis-full [&_figcaption>:first-child]:text-(length:--text-xs) [&_figcaption>:first-child]:text-ink-3\"\n                >\n                  <Sparkline\n                    label={kpi.label}\n                    value={now.value}\n                    change={now.change}\n                    data={now.data}\n                    tone={kpi.tone}\n                    width={220}\n                    height={34}\n                    interactive={false}\n                  />\n                </div>\n              );\n            })}\n          </div>\n\n          <div\n            className={cn(\n              \"grid grid-cols-[minmax(0,1.7fr)_minmax(0,1fr)] gap-3\",\n              narrow && \"grid-cols-[minmax(0,1fr)]\",\n            )}\n          >\n            <div className={panel}>\n              <div className=\"flex items-start justify-between gap-4\">\n                <p className=\"m-0 flex max-w-[46ch] gap-2 leading-[1.45] text-ink-2\">\n                  <Sparkles size={15} strokeWidth={1.75} className=\"mt-0.5 flex-none text-accent\" />\n                  <span>\n                    <strong className=\"font-medium text-ink\">{insight.lead}</strong> {insight.rest}\n                  </span>\n                </p>\n                <div\n                  className={cn(\n                    \"flex flex-none gap-3.5 *:inline-flex *:items-center *:gap-1.5\",\n                    hint,\n                    narrow && \"hidden\",\n                  )}\n                  aria-hidden=\"true\"\n                >\n                  <span>\n                    <i className=\"h-0.5 w-3.5 rounded-[2px] bg-accent\" />\n                    This period\n                  </span>\n                  <span>\n                    <i className=\"h-0.5 w-3.5 rounded-[2px] bg-[repeating-linear-gradient(to_right,color-mix(in_oklab,var(--ink)_46%,var(--surface))_0_4px,transparent_4px_7px)]\" />\n                    Last period\n                  </span>\n                </div>\n              </div>\n              <LineChart\n                label=\"Visitors\"\n                data={lumenSeries[range]}\n                series={[\n                  { key: \"visitors\", label: \"This period\", area: true },\n                  { key: \"last\", label: \"Last period\", dashed: true },\n                ]}\n                height={narrow ? 210 : 232}\n                legend={false}\n                formatValue={(value) => count.format(value)}\n                formatTick={compact}\n              />\n            </div>\n            {!narrow && (\n              <div className={panel}>\n                <div className=\"flex justify-between font-medium\">\n                  Top pages<span className={cn(\"font-normal\", hint)}>Change in visitors</span>\n                </div>\n                <ul className=\"m-0 grid list-none p-0\">\n                  {lumenMovers.map((mover) => (\n                    <li\n                      key={mover.name}\n                      className=\"flex items-center gap-3 border-t border-line-soft py-2.5 first:border-t-0 first:pt-0.5\"\n                    >\n                      <span\n                        className=\"grid size-5 flex-none place-items-center rounded-chip bg-hover text-[11px]/none font-semibold text-ink-2\"\n                        aria-hidden=\"true\"\n                      >\n                        {mover.name.charAt(0).toUpperCase()}\n                      </span>\n                      <span className=\"grid min-w-0 flex-1 leading-[1.3]\">\n                        <strong className=\"truncate font-medium\">{mover.name}</strong>\n                        <small className={cn(\"truncate\", hint)}>{mover.change}</small>\n                      </span>\n                      <span\n                        className={cn(\"font-medium text-green tabular-nums\", mover.amount < 0 && \"text-red\")}\n                        data-down={mover.amount < 0 || undefined}\n                      >\n                        {mover.amount < 0 ? \"−\" : \"+\"}\n                        {count.format(Math.abs(mover.amount))}\n                      </span>\n                    </li>\n                  ))}\n                </ul>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </figure>\n  );\n}\n\nexport default HeroLumen;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-lumen.tsx"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-mesh.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { cn } from \"../../lib/utils\";\n\n/**\n * One point of a mesh, in the same model the Gradient mesh editor saves: a position as a fraction of the box (0 to 1 from the\n * top left) and a reach. The color is not given here: it is read from the CSS variable `--mesh-{n}` on the element, so light\n * and dark themes each author their own palette in CSS and the canvas follows a theme switch without a remount.\n */\nexport interface HeroMeshPoint {\n  x: number;\n  y: number;\n  /** Reach of the color as a fraction of the box, 0.15 to 1.2. */\n  spread: number;\n}\n\nexport interface HeroMeshProps {\n  points: readonly HeroMeshPoint[];\n  /** Film grain, 0 to 1. */\n  grain?: number;\n  /** Drift speed. 1 loops every 20 seconds; 0 holds the mesh still. */\n  speed?: number;\n  className?: string;\n  style?: CSSProperties;\n}\n\ntype Rgb = [number, number, number];\n\n/** Same loop as the Gradient mesh editor: integer frequencies, so the drift is seamless. */\nconst PERIOD = 20,\n  TRAVEL = 0.075,\n  BREATH = 0.08,\n  MAX = 8;\n/** The drift is slow, so thirty frames a second is indistinguishable from sixty and halves the cost. */\nconst FRAME = 1000 / 30;\n\nconst VS = `attribute vec2 a; void main() { gl_Position = vec4(a, 0.0, 1.0); }`;\nconst FS = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform vec3 uBase;\nuniform vec4 uP[${MAX}];\nuniform vec3 uC[${MAX}];\nuniform int uN;\nuniform float uGrain;\nfloat hash(vec2 p) { vec3 q = fract(vec3(p.xyx) * .1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); }\nvoid main() {\n  vec2 uv = vec2(gl_FragCoord.x / uRes.x, 1.0 - gl_FragCoord.y / uRes.y);\n  vec3 col = uBase;\n  for (int i = 0; i < ${MAX}; i++) {\n    if (i >= uN) break;\n    vec4 p = uP[i];\n    float d = length((uv - p.xy) / max(p.z, 1e-4));\n    col = mix(col, uC[i], 1.0 - smoothstep(0.0, 1.0, d));\n  }\n  float n = hash(floor(gl_FragCoord.xy)) + hash(floor(gl_FragCoord.xy) + 19.19) - 1.0;\n  float lum = dot(col, vec3(.2126, .7152, .0722));\n  col += n * (uGrain * .1 * (.55 + 1.8 * lum * (1.0 - lum)) + .6 / 255.0);\n  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);\n}`;\n\nfunction hashOf(text: string) {\n  let h = 2166136261;\n  for (let i = 0; i < text.length; i++) {\n    h ^= text.charCodeAt(i);\n    h = Math.imul(h, 16777619);\n  }\n  return h >>> 0;\n}\n\nconst UNIFORMS = [\"uRes\", \"uBase\", \"uP\", \"uC\", \"uN\", \"uGrain\"] as const;\n\n/** Resolves any CSS color (hex, oklch, color-mix) to sRGB through a one pixel 2D canvas. */\nfunction toRgb(color: string, scratch: CanvasRenderingContext2D): Rgb {\n  scratch.clearRect(0, 0, 1, 1);\n  scratch.fillStyle = \"#000\";\n  scratch.fillStyle = color;\n  scratch.fillRect(0, 0, 1, 1);\n  const [r = 0, g = 0, b = 0] = scratch.getImageData(0, 0, 1, 1).data;\n  return [r / 255, g / 255, b / 255];\n}\n\n/** The static CSS render of the same mesh: shown on the server, before the canvas paints, and when WebGL is missing. */\nfunction cssMesh(points: readonly HeroMeshPoint[]): CSSProperties {\n  const stops = [0, 0.2, 0.4, 0.6, 0.8, 1].map((t) => [t, Math.round((1 - t * t * (3 - 2 * t)) * 100)] as const);\n  const layers = points.map(\n    (p, i) =>\n      `radial-gradient(ellipse ${p.spread * 100}% ${p.spread * 100}% at ${p.x * 100}% ${p.y * 100}%, ${stops.map(([t, a]) => `color-mix(in srgb, var(--mesh-${i + 1}) ${a}%, transparent) ${t * 100}%`).join(\", \")})`,\n  );\n  return { backgroundColor: \"var(--mesh-base)\", backgroundImage: layers.reverse().join(\", \") };\n}\n\n/**\n * A lean, non-editing render of the Gradient mesh: a single WebGL triangle composites each point over the last with the\n * editor's smoothstep falloff and grain, drifting on the editor's seamless loop. It draws at thirty frames a second only while\n * it is on screen and the tab is visible, and draws one still frame under reduced motion.\n */\nexport function HeroMesh({ points, grain = 0.35, speed = 1, className, style }: HeroMeshProps) {\n  const root = useRef<HTMLDivElement>(null);\n  const canvas = useRef<HTMLCanvasElement>(null);\n  const reduced = !!useReducedMotion();\n  const key = JSON.stringify(points);\n\n  useEffect(() => {\n    const host = root.current,\n      node = canvas.current;\n    if (!host || !node) return;\n    const gl = node.getContext(\"webgl\", {\n      alpha: false,\n      antialias: false,\n      depth: false,\n      stencil: false,\n      powerPreference: \"low-power\",\n    });\n    const scratch = document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n    if (!gl || !scratch) return;\n    const compile = (type: number, src: string) => {\n      const s = gl.createShader(type)!;\n      gl.shaderSource(s, src);\n      gl.compileShader(s);\n      return s;\n    };\n    const prog = gl.createProgram()!;\n    gl.attachShader(prog, compile(gl.VERTEX_SHADER, VS));\n    gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FS));\n    gl.linkProgram(prog);\n    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return;\n    gl.useProgram(prog);\n    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const loc = gl.getAttribLocation(prog, \"a\");\n    gl.enableVertexAttribArray(loc);\n    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n    const u = Object.fromEntries(UNIFORMS.map((name) => [name, gl.getUniformLocation(prog, name)])) as Record<\n      (typeof UNIFORMS)[number],\n      WebGLUniformLocation | null\n    >;\n\n    const pts = (JSON.parse(key) as HeroMeshPoint[]).slice(0, MAX);\n    const drift = pts.map((_, i) => {\n      const h = hashOf(`mesh-${i}`);\n      return {\n        ph1: (h % 628) / 100,\n        ph2: ((h >>> 10) % 628) / 100,\n        k1: 1 + ((h >>> 20) % 2),\n        k2: 1 + ((h >>> 22) % 2),\n      };\n    });\n    const P = new Float32Array(MAX * 4),\n      C = new Float32Array(MAX * 3);\n    let base: Rgb = [1, 1, 1];\n\n    const readColors = () => {\n      const css = getComputedStyle(host);\n      base = toRgb(css.getPropertyValue(\"--mesh-base\").trim() || \"#fff\", scratch);\n      pts.forEach((_, i) => {\n        const c = toRgb(css.getPropertyValue(`--mesh-${i + 1}`).trim() || \"#fff\", scratch);\n        C.set(c, i * 3);\n      });\n    };\n\n    let width = 0,\n      height = 0;\n    const size = () => {\n      // The mesh has no detail finer than its grain, so one canvas pixel per CSS pixel is enough on any screen.\n      width = Math.max(1, Math.round(host.clientWidth));\n      height = Math.max(1, Math.round(host.clientHeight));\n      node.width = width;\n      node.height = height;\n    };\n\n    let t = hashOf(key) % PERIOD;\n    const draw = () => {\n      const a = (2 * Math.PI * t) / PERIOD;\n      pts.forEach((p, i) => {\n        const d = drift[i]!;\n        P[i * 4] = p.x + TRAVEL * Math.sin(a * d.k1 + d.ph1);\n        P[i * 4 + 1] = p.y + TRAVEL * Math.cos(a * d.k2 + d.ph2);\n        P[i * 4 + 2] = p.spread * (1 + BREATH * Math.sin(a + d.ph1 * 0.7));\n      });\n      gl.viewport(0, 0, width, height);\n      gl.uniform2f(u.uRes, width, height);\n      gl.uniform3f(u.uBase, base[0], base[1], base[2]);\n      gl.uniform4fv(u.uP, P);\n      gl.uniform3fv(u.uC, C);\n      gl.uniform1i(u.uN, pts.length);\n      gl.uniform1f(u.uGrain, grain);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n      host.dataset.painted = \"\";\n    };\n\n    readColors();\n    size();\n    draw();\n\n    const moving = !reduced && speed > 0;\n    let visible = false,\n      frame = 0,\n      last = 0;\n    const tick = (now: number) => {\n      frame = requestAnimationFrame(tick);\n      if (now - last < FRAME) return;\n      const dt = last ? Math.min(0.1, (now - last) / 1000) : 0;\n      last = now;\n      t = (t + dt * speed) % PERIOD;\n      draw();\n    };\n    const sync = () => {\n      const run = moving && visible && document.visibilityState === \"visible\";\n      if (run && !frame) {\n        last = 0;\n        frame = requestAnimationFrame(tick);\n      }\n      if (!run && frame) {\n        cancelAnimationFrame(frame);\n        frame = 0;\n      }\n    };\n    const io = new IntersectionObserver(([entry]) => {\n      if (!entry) return;\n      visible = entry.isIntersecting;\n      sync();\n    });\n    io.observe(host);\n    const ro = new ResizeObserver(() => {\n      size();\n      draw();\n    });\n    ro.observe(host);\n    const theme = () => {\n      readColors();\n      draw();\n    };\n    const mo = new MutationObserver(theme);\n    mo.observe(document.documentElement, { attributes: true, attributeFilter: [\"data-theme\", \"data-accent\", \"class\"] });\n    const scheme = matchMedia(\"(prefers-color-scheme: dark)\");\n    scheme.addEventListener(\"change\", theme);\n    document.addEventListener(\"visibilitychange\", sync);\n    return () => {\n      cancelAnimationFrame(frame);\n      io.disconnect();\n      ro.disconnect();\n      mo.disconnect();\n      scheme.removeEventListener(\"change\", theme);\n      document.removeEventListener(\"visibilitychange\", sync);\n      delete host.dataset.painted;\n    };\n  }, [key, grain, speed, reduced]);\n\n  // The canvas fades in over its own CSS render once it has painted, so the first frame never flashes.\n  return (\n    <div\n      ref={root}\n      className={cn(\"group/mesh pointer-events-none absolute inset-0 overflow-hidden\", className)}\n      style={{ ...cssMesh(points), ...style }}\n      aria-hidden=\"true\"\n    >\n      <canvas\n        ref={canvas}\n        className=\"block size-full opacity-0 transition-opacity duration-considered ease-standard group-data-painted/mesh:opacity-100 motion-reduce:transition-none\"\n      />\n    </div>\n  );\n}\n\nexport default HeroMesh;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-mesh.tsx"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-motion.ts",
      "content": "import { useSyncExternalStore } from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport type { Transition, Variants } from \"motion/react\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\n\nexport type Bezier = [number, number, number, number];\nexport const ease = {\n  enter: [...motionTokens.ease.enter] as Bezier,\n  standard: [...motionTokens.ease.standard] as Bezier,\n  inOut: [...motionTokens.ease.inOut] as Bezier,\n};\n\n/** The one entrance a hero plays: children rise in reading order, once. */\nexport const heroGroup: Variants = {\n  hidden: {},\n  shown: { transition: { staggerChildren: motionTokens.stagger.line, delayChildren: 0.04 } },\n};\nexport const heroRise: Variants = {\n  hidden: { opacity: 0, y: 12, filter: `blur(${motionTokens.blur.subtle}px)` },\n  shown: { opacity: 1, y: 0, filter: \"blur(0px)\", transition: { duration: 0.64, ease: ease.enter } },\n};\nexport const heroFade: Variants = {\n  hidden: { opacity: 0 },\n  shown: { opacity: 1, transition: { duration: motionTokens.duration.standard } },\n};\n\nexport const instant: Transition = { duration: 0 };\n\nconst noop = () => () => {};\n/**\n * Reduced motion, read only after hydration. The server cannot know the preference, so the first client render matches the\n * server's full motion markup and the reduced branch takes over on the next render, before anything has moved.\n */\nexport function useHeroReducedMotion() {\n  const hydrated = useSyncExternalStore(\n    noop,\n    () => true,\n    () => false,\n  );\n  return !!useReducedMotion() && hydrated;\n}\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-motion.ts"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-relay.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\nimport { Check, FolderOpen, Inbox, Minus, ScanFace, ShieldCheck, Split, Webhook } from \"lucide-react\";\nimport { Button } from \"../../button/button\";\nimport { motionTokens } from \"../../lib/motion-tokens\";\nimport { cn } from \"../../lib/utils\";\nimport { ActionLink, heroDescription, heroScreen, heroShell, heroTitle } from \"./hero-content\";\nimport type { HeroAction } from \"./hero-content\";\nimport { ease, heroGroup, heroRise, useHeroReducedMotion } from \"./hero-motion\";\n\ntype NodeId = \"trigger\" | \"checks\" | \"policy\" | \"queue\" | \"case\" | \"webhook\";\ntype Status = \"idle\" | \"running\" | \"done\" | \"skipped\";\n\n/** Sample OpsisID sessions. Each one runs the same flow; the policy table decides whether a person reviews it. */\nconst EVENTS = [\n  {\n    purpose: \"rental_lister\",\n    country: \"GR\",\n    checks: \"3 passed, 1 running\",\n    decision: \"Review: property link\",\n    routed: true,\n    caseId: \"Case 482\",\n    total: 412,\n    ms: { checks: 118, policy: 2, queue: 164, case: 231, webhook: 58 },\n  },\n  {\n    purpose: \"rental_seeker\",\n    country: \"PL\",\n    checks: \"Email and phone passed\",\n    decision: \"Approved at level 0\",\n    routed: false,\n    caseId: \"\",\n    total: 171,\n    ms: { checks: 96, policy: 1, queue: 0, case: 0, webhook: 61 },\n  },\n  {\n    purpose: \"aml_cdd\",\n    country: \"GR\",\n    checks: \"Sanctions check passed\",\n    decision: \"Review: decision case\",\n    routed: true,\n    caseId: \"Case 483\",\n    total: 436,\n    ms: { checks: 131, policy: 2, queue: 149, case: 244, webhook: 66 },\n  },\n] as const;\ntype RelayEvent = (typeof EVENTS)[number];\n\n/**\n * One run in phases. Odd phases run a step, even phases carry the data along the edges out of it; the last phase holds the\n * finished run on screen before the next event arrives. Durations in milliseconds.\n */\nconst PHASES = [900, 420, 380, 640, 380, 360, 440, 760, 2800];\nconst DONE = PHASES.length - 1;\nconst STEPS: Record<NodeId, [start: number, end: number]> = {\n  trigger: [1, 2],\n  checks: [3, 4],\n  policy: [5, 6],\n  queue: [7, 8],\n  case: [7, 8],\n  webhook: [7, 8],\n};\nconst ACTIONS = [\"queue\", \"case\", \"webhook\"] as const;\n\nfunction statusOf(id: NodeId, phase: number, event: RelayEvent): Status {\n  const [start, end] = STEPS[id];\n  if ((id === \"queue\" || id === \"case\") && !event.routed && phase >= 6) return \"skipped\";\n  return phase < start ? \"idle\" : phase < end ? \"running\" : \"done\";\n}\n\n/** The graph is drawn at a fixed size and scaled to its column, like a screenshot, so it never reflows mid run. */\ntype Box = { x: number; y: number; w: number; h: number };\nconst LAYOUTS = {\n  wide: {\n    width: 640,\n    height: 524,\n    summary: 480,\n    nodes: {\n      trigger: { x: 170, y: 0, w: 300, h: 60 },\n      checks: { x: 170, y: 112, w: 300, h: 60 },\n      policy: { x: 170, y: 224, w: 300, h: 60 },\n      queue: { x: 0, y: 356, w: 200, h: 84 },\n      case: { x: 220, y: 356, w: 200, h: 84 },\n      webhook: { x: 440, y: 356, w: 200, h: 84 },\n    } as Record<NodeId, Box>,\n  },\n  narrow: {\n    width: 344,\n    height: 316,\n    summary: 0,\n    nodes: {\n      trigger: { x: 0, y: 0, w: 344, h: 52 },\n      checks: { x: 0, y: 78, w: 344, h: 52 },\n      policy: { x: 0, y: 156, w: 344, h: 52 },\n      queue: { x: 0, y: 244, w: 108, h: 72 },\n      case: { x: 118, y: 244, w: 108, h: 72 },\n      webhook: { x: 236, y: 244, w: 108, h: 72 },\n    } as Record<NodeId, Box>,\n  },\n};\ntype Layout = (typeof LAYOUTS)[keyof typeof LAYOUTS];\n\nconst EDGES: { from: NodeId; to: NodeId; phase: number }[] = [\n  { from: \"trigger\", to: \"checks\", phase: 2 },\n  { from: \"checks\", to: \"policy\", phase: 4 },\n  { from: \"policy\", to: \"queue\", phase: 6 },\n  { from: \"policy\", to: \"case\", phase: 6 },\n  { from: \"policy\", to: \"webhook\", phase: 6 },\n];\n\nfunction edgePath(layout: Layout, from: NodeId, to: NodeId) {\n  const a = layout.nodes[from],\n    b = layout.nodes[to];\n  const x1 = a.x + a.w / 2,\n    y1 = a.y + a.h,\n    x2 = b.x + b.w / 2,\n    y2 = b.y;\n  const mid = (y2 - y1) / 2;\n  return `M ${x1} ${y1} C ${x1} ${y1 + mid} ${x2} ${y2 - mid} ${x2} ${y2}`;\n}\n\nconst RESULTS: Record<(typeof ACTIONS)[number], (event: RelayEvent) => string> = {\n  queue: (event) => `Queued in ${event.ms.queue} ms`,\n  case: (event) => `${event.caseId} in ${event.ms.case} ms`,\n  webhook: (event) => `Delivered in ${event.ms.webhook} ms`,\n};\n\nconst glyph = \"text-ink-2\";\nconst STATUS_LABEL: Record<Status, string> = { idle: \"Waiting\", running: \"Running\", done: \"Done\", skipped: \"Skipped\" };\nconst mono = \"font-code\";\nconst spinner =\n  \"block size-[13px] flex-none animate-[spin_.7s_linear_infinite] rounded-full border-[1.5px] border-[color-mix(in_oklab,var(--accent)_22%,transparent)] border-t-accent motion-reduce:animate-none\";\n\nexport interface HeroRelayProps {\n  primaryAction?: HeroAction;\n  secondaryAction?: HeroAction;\n  animateIn?: boolean;\n  className?: string;\n}\n\n/**\n * Split hero, one full screen: the copy beside a live flow graph on a dotted canvas. Sample OpsisID sessions arrive on their\n * own and run the flow node by node; the data draws along each edge, the policy table decides which actions run, and every step\n * reports its time. \"Send test session\" runs the next sample at once. The run pauses off screen, and under reduced motion each\n * event shows its finished run without playing.\n */\nexport function HeroRelay({\n  primaryAction = { label: \"Ask for a quote\", doneLabel: \"Request sent\" },\n  secondaryAction = { label: \"Read the docs\", doneLabel: \"Opening docs\" },\n  animateIn = true,\n  className,\n}: HeroRelayProps) {\n  const item = heroRise;\n  const reduced = useHeroReducedMotion();\n  const root = useRef<HTMLElement>(null);\n  const stage = useRef<HTMLDivElement>(null);\n  const [mode, setMode] = useState<keyof typeof LAYOUTS>(\"wide\");\n  const [scale, setScale] = useState(1);\n  const [run, setRun] = useState({ index: 0, count: 2418, phase: 0 });\n  const [visible, setVisible] = useState(false);\n\n  useLayoutEffect(() => {\n    const node = stage.current;\n    if (!node) return;\n    const measure = () => {\n      const width = node.clientWidth;\n      const next = width < 520 ? \"narrow\" : \"wide\";\n      setMode(next);\n      setScale(Math.min(1, width / LAYOUTS[next].width));\n    };\n    measure();\n    const observer = new ResizeObserver(measure);\n    observer.observe(node);\n    return () => observer.disconnect();\n  }, []);\n\n  // The run only plays while the hero is on screen and the tab is visible.\n  useEffect(() => {\n    const node = root.current;\n    if (!node) return;\n    let onScreen = false;\n    const sync = () => setVisible(onScreen && document.visibilityState === \"visible\");\n    const io = new IntersectionObserver(([entry]) => {\n      if (!entry) return;\n      onScreen = entry.isIntersecting;\n      sync();\n    });\n    io.observe(node);\n    document.addEventListener(\"visibilitychange\", sync);\n    return () => {\n      io.disconnect();\n      document.removeEventListener(\"visibilitychange\", sync);\n    };\n  }, []);\n\n  useEffect(() => {\n    if (reduced || !visible) return;\n    const timer = window.setTimeout(\n      () =>\n        setRun((current) =>\n          current.phase < DONE\n            ? { ...current, phase: current.phase + 1 }\n            : { index: (current.index + 1) % EVENTS.length, count: current.count + 1, phase: 0 },\n        ),\n      PHASES[run.phase],\n    );\n    return () => window.clearTimeout(timer);\n  }, [run.phase, run.index, reduced, visible]);\n\n  const sendTest = () =>\n    setRun((current) => ({ index: (current.index + 1) % EVENTS.length, count: current.count + 1, phase: 1 }));\n\n  const layout = LAYOUTS[mode];\n  const event = EVENTS[run.index]!;\n  // Reduced motion: every event is shown as its finished run; the button moves to the next one at once.\n  const phase = reduced ? DONE : run.phase;\n  const status = (id: NodeId) => statusOf(id, phase, event);\n  const narrow = mode === \"narrow\";\n\n  // Reduced motion: Motion skips every transform (rise, tilt entrance); opacity still fades briefly.\n  // The whole hero is a workflow canvas: a dot grid fills the screen, strongest behind the graph, and the copy sits on its quiet side.\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <section\n        ref={root}\n        className={cn(\n          heroShell,\n          heroScreen,\n          \"isolate grid items-center bg-[color-mix(in_oklab,var(--accent)_2%,var(--page))]\",\n          \"[--dot:color-mix(in_oklab,var(--ink)_13%,transparent)] [--node-shadow:0_1px_1px_rgb(20_24_40/.04),0_8px_24px_-12px_rgb(20_24_40/.14)] [--wire:color-mix(in_oklab,var(--ink)_17%,transparent)]\",\n          \"dark:[--dot:color-mix(in_oklab,var(--ink)_17%,transparent)] dark:[--node-shadow:inset_0_1px_0_color-mix(in_oklab,var(--ink)_7%,transparent),0_12px_32px_-14px_rgb(0_0_0/.7)] dark:[--wire:color-mix(in_oklab,var(--ink)_20%,transparent)]\",\n          \"before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:bg-[radial-gradient(circle_at_1px_1px,var(--dot)_1px,transparent_1.5px)] before:bg-[length:22px_22px] before:content-[''] before:[mask-image:radial-gradient(70%_80%_at_72%_52%,#000_30%,transparent_85%)] @max-[900px]/hero:before:[mask-image:radial-gradient(90%_60%_at_50%_78%,#000_30%,transparent_85%)]\",\n          className,\n        )}\n      >\n        <div className=\"mx-auto box-border grid w-full max-w-[1320px] grid-cols-[minmax(0,.92fr)_minmax(0,1fr)] items-center gap-16 px-10 pt-[clamp(88px,12svh,120px)] pb-12 @max-[900px]/hero:grid-cols-[minmax(0,1fr)] @max-[900px]/hero:gap-10 @max-[560px]/hero:gap-8 @max-[560px]/hero:px-4 @max-[560px]/hero:pt-20 @max-[560px]/hero:pb-6\">\n          <motion.div\n            className=\"grid justify-items-start gap-5 text-left @max-[560px]/hero:gap-4\"\n            variants={heroGroup}\n            initial={animateIn ? \"hidden\" : false}\n            animate=\"shown\"\n          >\n            <motion.h1\n              variants={item}\n              className={cn(\"text-[length:clamp(2.5rem,1.2rem+4.6cqi,4.5rem)]\", heroTitle, \"max-w-[12ch]\")}\n            >\n              Identity checks, decided by your policy\n            </motion.h1>\n            <motion.p variants={item} className={cn(heroDescription, \"max-w-[500px]\")}>\n              OpsisID verifies people by eID or by ID document and selfie, then your policy table decides. Cases that\n              need a person go to your review queue, and every decision returns by signed webhook.\n            </motion.p>\n            <motion.div\n              variants={item}\n              className=\"mt-3 flex flex-wrap gap-3 @max-[560px]/hero:mt-1 @max-[560px]/hero:w-full @max-[560px]/hero:*:flex-auto\"\n            >\n              <ActionLink action={primaryAction} kind=\"primary\" />\n              <ActionLink action={secondaryAction} kind=\"secondary\" />\n            </motion.div>\n            <motion.p variants={item} className=\"m-0 text-(length:--text-sm) text-ink-3\">\n              EU only. A model never decides a case.\n            </motion.p>\n          </motion.div>\n\n          <motion.div\n            className=\"grid min-w-0 justify-items-center gap-4 @max-[560px]/hero:gap-3\"\n            initial={animateIn ? { opacity: 0, y: 24 } : false}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 1, ease: ease.enter, delay: 0.28 }}\n          >\n            <div className=\"flex w-[min(100%,640px)] items-center justify-between gap-3\">\n              <span className={cn(\"inline-flex min-w-0 items-center gap-2 text-[13px] text-ink-2\", mono)}>\n                <i\n                  className=\"size-[7px] flex-none rounded-full bg-ink-3 transition-[background-color] duration-standard ease-standard data-on:bg-green motion-reduce:transition-none\"\n                  data-on={visible || reduced || undefined}\n                  aria-hidden=\"true\"\n                />\n                policy 2026-10-14.1\n              </span>\n              <Button variant=\"secondary\" size=\"sm\" onClick={sendTest}>\n                Send test session\n              </Button>\n            </div>\n            <div ref={stage} className=\"relative w-[min(100%,640px)]\" style={{ height: layout.height * scale }}>\n              <figure\n                className=\"absolute top-0 left-1/2 m-0 origin-top -translate-x-1/2\"\n                aria-label={`OpsisID flow, sample session for ${event.purpose}`}\n                style={{ width: layout.width, height: layout.height, transform: `scale(${scale})` }}\n              >\n                <svg\n                  className=\"absolute inset-0 overflow-visible\"\n                  width={layout.width}\n                  height={layout.height}\n                  viewBox={`0 0 ${layout.width} ${layout.height}`}\n                  aria-hidden=\"true\"\n                >\n                  {EDGES.map((edge) => {\n                    const d = edgePath(layout, edge.from, edge.to);\n                    const skipped = status(edge.to) === \"skipped\";\n                    const lit = phase >= edge.phase && !skipped;\n                    return (\n                      <g key={`${edge.from}-${edge.to}`}>\n                        <path\n                          d={d}\n                          className={cn(\n                            \"fill-none stroke-(--wire) stroke-[1.25]\",\n                            skipped && \"opacity-70 [stroke-dasharray:3_4]\",\n                          )}\n                          data-skipped={skipped || undefined}\n                        />\n                        <motion.path\n                          d={d}\n                          className=\"fill-none stroke-accent stroke-[1.75] [stroke-linecap:round]\"\n                          initial={false}\n                          animate={{ pathLength: lit ? 1 : 0, opacity: lit ? 1 : 0 }}\n                          transition={\n                            lit && !reduced\n                              ? {\n                                  pathLength: { duration: PHASES[edge.phase]! / 1000, ease: ease.inOut },\n                                  opacity: { duration: 0.08 },\n                                }\n                              : { duration: 0 }\n                          }\n                        />\n                      </g>\n                    );\n                  })}\n                </svg>\n\n                <RelayNode\n                  box={layout.nodes.trigger}\n                  status={status(\"trigger\")}\n                  icon={<ScanFace size={16} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />}\n                  title=\"Session submitted\"\n                  sub={`${event.purpose}, ${event.country}`}\n                />\n                <RelayNode\n                  box={layout.nodes.checks}\n                  status={status(\"checks\")}\n                  icon={<ShieldCheck size={16} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />}\n                  title=\"Run the checks\"\n                  sub={status(\"checks\") === \"done\" ? event.checks : \"Email, phone, name and ID\"}\n                  meta={`${event.ms.checks} ms`}\n                />\n                <RelayNode\n                  box={layout.nodes.policy}\n                  status={status(\"policy\")}\n                  icon={<Split size={17} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />}\n                  title=\"Policy table\"\n                  sub={status(\"policy\") === \"done\" ? event.decision : \"Decision\"}\n                  meta={`${event.ms.policy} ms`}\n                />\n                {ACTIONS.map((id) => {\n                  const s = status(id);\n                  const copy = {\n                    queue: {\n                      icon: <Inbox size={16} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />,\n                      title: narrow ? \"Queue\" : \"Add to review queue\",\n                      waiting: \"Rentals GR\",\n                    },\n                    case: {\n                      icon: <FolderOpen size={16} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />,\n                      title: narrow ? \"Case\" : \"Open a case\",\n                      waiting: \"Reviewer\",\n                    },\n                    webhook: {\n                      icon: <Webhook size={16} strokeWidth={1.75} className={glyph} aria-hidden=\"true\" />,\n                      title: narrow ? \"Webhook\" : \"Send signed webhook\",\n                      waiting: \"Opsisid-Signature\",\n                    },\n                  }[id];\n                  const sub =\n                    s === \"skipped\"\n                      ? \"Skipped\"\n                      : s === \"done\"\n                        ? narrow\n                          ? `${event.ms[id]} ms`\n                          : RESULTS[id](event)\n                        : s === \"running\"\n                          ? \"Running\"\n                          : narrow\n                            ? \"Waiting\"\n                            : copy.waiting;\n                  return (\n                    <RelayNode\n                      key={id}\n                      box={layout.nodes[id]}\n                      compact\n                      status={s}\n                      icon={copy.icon}\n                      title={copy.title}\n                      sub={sub}\n                    />\n                  );\n                })}\n\n                {!narrow && (\n                  <div\n                    className=\"absolute left-0 flex h-11 items-center justify-between gap-3 border-t border-line-soft px-0.5 text-[13px] text-ink-3\"\n                    style={{ top: layout.summary, width: layout.width }}\n                  >\n                    <span className=\"inline-flex items-center gap-3\">\n                      <code\n                        className={cn(\n                          \"rounded-chip border border-line-soft bg-surface px-[7px] py-px text-(length:--text-xs) text-ink-2\",\n                          mono,\n                        )}\n                      >\n                        {event.purpose}\n                      </code>\n                      <span>\n                        Session <span className=\"tabular-nums\">{run.count.toLocaleString(\"en-US\")}</span>\n                      </span>\n                    </span>\n                    <AnimatePresence mode=\"popLayout\" initial={false}>\n                      <motion.span\n                        key={phase === DONE ? `done-${run.count}` : phase === 0 ? \"waiting\" : \"running\"}\n                        className={cn(\n                          \"inline-flex items-center gap-[7px]\",\n                          phase === DONE && \"text-ink [&_svg]:text-green\",\n                        )}\n                        data-done={phase === DONE || undefined}\n                        initial={{ opacity: 0, y: 6 }}\n                        animate={{ opacity: 1, y: 0 }}\n                        exit={{ opacity: 0, y: -6 }}\n                        transition={{ duration: motionTokens.duration.fast, ease: ease.standard }}\n                      >\n                        {phase === DONE ? (\n                          <>\n                            <Check size={14} strokeWidth={2} aria-hidden=\"true\" />\n                            <span>\n                              Completed in <span className=\"tabular-nums\">{event.total} ms</span>\n                            </span>\n                          </>\n                        ) : phase === 0 ? (\n                          \"Waiting for the next event\"\n                        ) : (\n                          <>\n                            <i className={spinner} aria-hidden=\"true\" />\n                            Running\n                          </>\n                        )}\n                      </motion.span>\n                    </AnimatePresence>\n                  </div>\n                )}\n              </figure>\n            </div>\n          </motion.div>\n        </div>\n      </section>\n    </MotionConfig>\n  );\n}\n\nfunction RelayNode({\n  box,\n  status,\n  icon,\n  title,\n  sub,\n  meta,\n  compact = false,\n}: {\n  box: Box;\n  status: Status;\n  icon: ReactNode;\n  title: string;\n  sub: string;\n  meta?: string;\n  compact?: boolean;\n}) {\n  // A node: what the step is, what it found, and its state. The running step takes the accent; compact nodes (the three\n  // actions) put the icon and state on top and the name and its result under them.\n  return (\n    <div\n      className={cn(\n        \"absolute flex items-center gap-3 rounded-[14px] border border-line bg-surface px-3.5 text-[13px] leading-[1.3] shadow-(--node-shadow) transition-[border-color,opacity,background-color] duration-standard ease-standard motion-reduce:transition-none\",\n        status === \"running\" &&\n          \"border-[color-mix(in_oklab,var(--accent)_70%,var(--line))] bg-[color-mix(in_oklab,var(--accent)_4%,var(--surface))]\",\n        status === \"skipped\" && \"opacity-50 shadow-none\",\n        compact &&\n          \"grid grid-cols-[1fr_auto] grid-rows-[auto_auto] content-center gap-x-1 gap-y-2 px-3.5 @max-[560px]/hero:gap-y-1.5 @max-[560px]/hero:px-2.5\",\n      )}\n      data-status={status}\n      data-compact={compact || undefined}\n      style={{ left: box.x, top: box.y, width: box.w, height: box.h }}\n    >\n      <span className=\"grid size-5 flex-none place-items-center\">{icon}</span>\n      <span className={cn(\"grid min-w-0 flex-1\", compact && \"col-span-full\")}>\n        <strong className=\"truncate font-medium\">{title}</strong>\n        <small className=\"truncate text-(length:--text-xs) text-ink-3\">{sub}</small>\n      </span>\n      <span className={cn(\"grid flex-none justify-items-end\", compact && \"col-start-2 row-start-1\")}>\n        <AnimatePresence mode=\"popLayout\" initial={false}>\n          <motion.span\n            key={status}\n            className=\"inline-flex items-center gap-1.5\"\n            initial={{ opacity: 0, scale: 0.6 }}\n            animate={{ opacity: 1, scale: 1 }}\n            exit={{ opacity: 0, scale: 0.6 }}\n            transition={motionTokens.spring.snappy}\n          >\n            {status === \"running\" ? (\n              <i className={spinner} aria-hidden=\"true\" />\n            ) : status === \"done\" ? (\n              <>\n                {meta && <span className=\"text-(length:--text-xs) text-ink-3 tabular-nums\">{meta}</span>}\n                <Check size={14} strokeWidth={2} className=\"text-green\" aria-hidden=\"true\" />\n              </>\n            ) : status === \"skipped\" ? (\n              <Minus size={14} strokeWidth={2} className=\"text-ink-3\" aria-hidden=\"true\" />\n            ) : (\n              <i className=\"block size-2.5 rounded-full border-[1.5px] border-(--wire)\" aria-hidden=\"true\" />\n            )}\n            <span className=\"sr-only\">{STATUS_LABEL[status]}</span>\n          </motion.span>\n        </AnimatePresence>\n      </span>\n    </div>\n  );\n}\n\nexport default HeroRelay;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-relay.tsx"
    },
    {
      "path": "src/registry/blocks/hero-section/hero-section.tsx",
      "content": "\"use client\";\n\nimport { useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { cn } from \"../../lib/utils\";\nimport SegmentedControl from \"../../segmented-control/segmented-control\";\nimport { HeroLumen } from \"./hero-lumen\";\nimport { HeroRelay } from \"./hero-relay\";\nimport { HeroCadence } from \"./hero-cadence\";\nimport { HeroMesh } from \"./hero-mesh\";\nimport type { HeroMeshPoint } from \"./hero-mesh\";\nimport { HeroContent } from \"./hero-content\";\nimport type { HeroAction, HeroInstallCommand } from \"./hero-content\";\n\nexport { HeroCadence, HeroContent, HeroLumen, HeroMesh, HeroRelay };\nexport type { HeroAction, HeroInstallCommand, HeroMeshPoint };\n\nexport type HeroSectionVariant = \"centered\" | \"split\" | \"minimal\";\n\nexport interface HeroSectionProps {\n  /**\n   * `centered` is a headline over a drifting mesh with a live dashboard rising from the bottom edge, `split` sets the copy\n   * beside a live workflow graph that runs sample events node by node, and `minimal` is large type over a mesh gradient.\n   * Each design fills one full screen.\n   */\n  variant?: HeroSectionVariant;\n  /** Plays the entrance once on mount. Defaults to true. */\n  animateIn?: boolean;\n  /** The main call to action. With `doneLabel` and no `href`, the button confirms in place. */\n  primaryAction?: HeroAction | null;\n  /** The second call to action. */\n  secondaryAction?: HeroAction | null;\n  /**\n   * Your own headline. When set, the hero renders your content in the chosen layout (see `HeroContent`) instead of the\n   * designed sample, together with the props below.\n   */\n  title?: string;\n  description?: string;\n  announcement?: HeroAction | null;\n  /** Install commands for your own content; the first one is shown with a copy button. */\n  install?: HeroInstallCommand[] | null;\n  /** A visual beside your own content in the split layout. */\n  media?: ReactNode;\n  /** Small facts under your own content, such as the version and license. */\n  meta?: string[];\n  /** Kept for compatibility; highlighted phrases render as plain text. */\n  highlight?: string;\n  className?: string;\n}\n\n/**\n * A full screen landing page hero in three designs: a product screenshot in perspective over a drifting mesh, a live workflow\n * graph that routes sample events, and editorial type over a mesh gradient.\n */\nexport function HeroSection({\n  variant = \"centered\",\n  animateIn = true,\n  primaryAction,\n  secondaryAction,\n  title,\n  description,\n  announcement,\n  install,\n  media,\n  meta,\n  className,\n}: HeroSectionProps) {\n  if (title)\n    return (\n      <HeroContent\n        layout={variant}\n        title={title}\n        description={description}\n        announcement={announcement}\n        primaryAction={primaryAction}\n        secondaryAction={secondaryAction}\n        install={install}\n        media={media}\n        meta={meta}\n        animateIn={animateIn}\n        className={className}\n      />\n    );\n  const actions = { primaryAction: primaryAction ?? undefined, secondaryAction: secondaryAction ?? undefined };\n  if (variant === \"split\") return <HeroRelay animateIn={animateIn} className={className} {...actions} />;\n  if (variant === \"minimal\") return <HeroCadence animateIn={animateIn} className={className} {...actions} />;\n  return <HeroLumen animateIn={animateIn} className={className} {...actions} />;\n}\n\nconst variantOptions = [\n  { value: \"centered\", label: \"Screenshot\" },\n  { value: \"split\", label: \"Workflow\" },\n  { value: \"minimal\", label: \"Mesh\" },\n];\n\n/**\n * Preview: the hero, full screen, with a small glass switch floating over its top edge. The switch takes no space of its own,\n * so every design fills the screen exactly. Switching replays the entrance.\n */\nexport function HeroSectionBlock({ variant: initial = \"centered\" }: { variant?: HeroSectionVariant }) {\n  const [variant, setVariant] = useState<HeroSectionVariant>(initial);\n  return (\n    <div className=\"relative grid min-h-svh w-full *:first:min-h-[inherit]\">\n      <HeroSection key={variant} variant={variant} />\n      <div\n        className={cn(\n          \"absolute top-4 left-1/2 z-20 flex max-w-[calc(100%-2*var(--space-4))] -translate-x-1/2 rounded-button border border-[color-mix(in_oklab,var(--ink)_10%,transparent)] bg-[color-mix(in_oklab,var(--surface)_64%,transparent)] shadow-[0_1px_1px_rgb(20_24_40/.04),0_10px_30px_-12px_rgb(20_24_40/.22)] backdrop-blur-[18px] backdrop-saturate-150\",\n          \"dark:border-[color-mix(in_oklab,var(--ink)_14%,transparent)] dark:bg-[color-mix(in_oklab,var(--surface)_58%,transparent)] dark:shadow-[inset_0_1px_0_color-mix(in_oklab,var(--ink)_8%,transparent),0_10px_30px_-12px_rgb(0_0_0/.6)]\",\n        )}\n      >\n        {/* The segmented control drops its own well; the glass is its surface. */}\n        <SegmentedControl\n          label=\"Hero design\"\n          options={variantOptions}\n          value={variant}\n          onValueChange={(value) => setVariant(value as HeroSectionVariant)}\n          className=\"rounded-button border-0 bg-transparent [&_button]:rounded-button\"\n        />\n      </div>\n    </div>\n  );\n}\n\nexport default HeroSectionBlock;\n",
      "type": "registry:component",
      "target": "@components/iris/blocks/hero-section/hero-section.tsx"
    }
  ],
  "docs": "Docs and live preview: https://irisui.dev/components/hero-section",
  "meta": {
    "kind": "block",
    "group": "Blocks",
    "export": "HeroSection",
    "variants": [
      "HeroCadence",
      "HeroLumen",
      "HeroMesh",
      "HeroRelay"
    ],
    "docs": "https://irisui.dev/components/hero-section"
  }
}
