{
  "version": 1,
  "fps": 30,
  "generated": "build",
  "counts": {
    "items": 76,
    "categories": 11
  },
  "categories": [
    {
      "key": "typography",
      "label": "Kinetic type",
      "blurb": "Headlines, counters and text that carries the scene.",
      "count": 10,
      "featured": "impact-title-rules"
    },
    {
      "key": "titles",
      "label": "Titles & lower thirds",
      "blurb": "Naming things: cards, badges, captions, chapter marks.",
      "count": 7,
      "featured": "gridpaper-editorial"
    },
    {
      "key": "product",
      "label": "Product & UI",
      "blurb": "Devices, browser frames, cursors, notifications.",
      "count": 12,
      "featured": "depth-field-cards"
    },
    {
      "key": "data",
      "label": "Data",
      "blurb": "Charts and figures that animate to make a point.",
      "count": 14,
      "featured": "metric-odometer"
    },
    {
      "key": "brand",
      "label": "Logo & brand",
      "blurb": "Reveals, lockups and stamps.",
      "count": 6,
      "featured": "liquid-chrome-wordmark"
    },
    {
      "key": "background",
      "label": "Backgrounds",
      "blurb": "Atmosphere that sits behind everything else.",
      "count": 6,
      "featured": "hot-path-scan"
    },
    {
      "key": "transition",
      "label": "Transitions",
      "blurb": "The handoff between two scenes.",
      "count": 3,
      "featured": "zoom-punch-cut"
    },
    {
      "key": "primitive",
      "label": "Primitives",
      "blurb": "Draw-on paths, connectors, reveals — the building blocks.",
      "count": 4,
      "featured": "animate-in-presets"
    },
    {
      "key": "diagram",
      "label": "Diagrams",
      "blurb": "Explainer devices: flows, layers, packets, checklists.",
      "count": 6,
      "featured": "doodle-card-stack"
    },
    {
      "key": "social",
      "label": "Social formats",
      "blurb": "Vertical and square cuts with platform safe areas.",
      "count": 5,
      "featured": "gradient-card-collage"
    },
    {
      "key": "youtube",
      "label": "YouTube",
      "blurb": "The furniture of a YouTube video: subscribe nudges, comment cards, end screens.",
      "count": 3,
      "featured": "youtube-end-card"
    }
  ],
  "brands": [
    {
      "id": "slate",
      "name": "Slate",
      "description": "Cool dark editorial. The registry default — reads as an edit suite.",
      "swatch": [
        "#0B1116",
        "#151E26",
        "#E6EDF3",
        "#4FB9DC",
        "#E0A44E"
      ]
    },
    {
      "id": "paper",
      "name": "Paper",
      "description": "High-contrast light neutral. For documentation and explainer work.",
      "swatch": [
        "#F5F6F7",
        "#FFFFFF",
        "#15191C",
        "#0A6E8C",
        "#B0710F"
      ]
    },
    {
      "id": "signal",
      "name": "Signal",
      "description": "Near-black with one saturated blue. Product launches, high contrast.",
      "swatch": [
        "#07080A",
        "#13161A",
        "#FFFFFF",
        "#2F6BFF",
        "#00D2A8"
      ]
    },
    {
      "id": "press",
      "name": "Press",
      "description": "Newsprint. Off-white, true black, vermilion — documentary and data journalism.",
      "swatch": [
        "#F2F0EC",
        "#FFFFFF",
        "#111111",
        "#D2341E",
        "#1B4D8F"
      ]
    },
    {
      "id": "circuit",
      "name": "Circuit",
      "description": "Deep indigo with electric accents. Developer tooling and infrastructure.",
      "swatch": [
        "#0A0A18",
        "#14142B",
        "#E8E8F5",
        "#7C5CFF",
        "#38E0C0"
      ]
    },
    {
      "id": "field",
      "name": "Field",
      "description": "Muted warm light with an olive accent. Calm, considered, non-technical brands.",
      "swatch": [
        "#EFEDE6",
        "#FBFAF7",
        "#23241F",
        "#5A6B35",
        "#A8562A"
      ]
    },
    {
      "id": "ledger",
      "name": "Ledger",
      "description": "Mint on near-black with a hot red for the downside. Finance and startup explainers.",
      "swatch": [
        "#141716",
        "#1C201E",
        "#FFFFFF",
        "#00E8BF",
        "#FF292B"
      ]
    }
  ],
  "timing": {
    "budgets": {
      "beat": {
        "enter": 12,
        "hold": 36,
        "exit": 12
      },
      "statement": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "scene": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "feature": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      }
    },
    "springs": {
      "settle": {
        "damping": 18,
        "stiffness": 180,
        "mass": 0.9
      },
      "snap": {
        "damping": 22,
        "stiffness": 300,
        "mass": 0.7
      },
      "glide": {
        "damping": 26,
        "stiffness": 90,
        "mass": 1.4
      },
      "pop": {
        "damping": 11,
        "stiffness": 240,
        "mass": 0.8
      }
    },
    "easings": [
      "enter",
      "exit",
      "travel",
      "accent"
    ],
    "note": "Overlap consecutive scenes by the outgoing scene's exit budget. Every component multiplies its own presence by stage.presence, so the curves cross into a clean handoff."
  },
  "items": [
    {
      "id": "agent-chat-stream",
      "name": "Agent Chat Stream",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "An assistant answers word by word while tool-call chips spin, resolve to a check and settle under a blinking caret",
      "tags": [
        "ai",
        "chat",
        "streaming text",
        "tool call",
        "product ui",
        "caret"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/agent-chat-stream.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nconst BUDGET = BUDGETS.feature;\n\n/* -------------------------------------------------------------------- timing */\n\n/** The user's message is already sent by the time the frame has settled. */\nconst PROMPT_AT = 3;\n/** Thinking dots come up in the slot the answer will occupy, so nothing moves later. */\nconst THINK_AT = 16;\n/** First token. ~1s of thinking reads as considered; much less reads as fake. */\nconst ANSWER_AT = 46;\n/** Base frames per word before jitter: ~13 words a second, which is what fast streaming looks like. */\nconst WORD_BASE = 2.2;\n/** Fastest the auto-fit is allowed to stream. Below this it stops reading as words arriving. */\nconst WORD_FLOOR = 0.7;\n/** Beat between the answer's last word and the first tool chip. */\nconst ANSWER_TO_TOOLS = 10;\n/** Offset between successive tool chips. They overlap — real calls run concurrently. */\nconst TOOL_STRIDE = 15;\n/** Frames a chip spins before it resolves. Under ~20 a spinner reads as a glitch, not as work. */\nconst TOOL_SPIN = 27;\n/** Frames the result line takes to land after the check draws. */\nconst TOOL_RESOLVE = 12;\n/** Beat between the last resolve and the closing reply. */\nconst TOOLS_TO_REPLY = 9;\n/** Frames in one full caret blink. The caret holds solid while words are landing. */\nconst BLINK = 16;\n/**\n * Frames of finished, still frame before the exit window opens. The auto-fit\n * streams faster rather than letting the last word land on the way out.\n */\nconst REST = 16;\n/** Absolute frame the reply must have finished by. */\nconst DEADLINE = BUDGET.enter + BUDGET.hold - REST;\n\n/* ------------------------------------------------------------------- metrics */\n\n/**\n * Layout is expressed in ems of one body size, and that size is solved for\n * (below) so the whole transcript fits the frame in any aspect. Every block\n * reserves its final height from frame 0, so a chip landing never reflows the\n * text above it — the composition is identical on frame 60 and frame 200.\n */\nconst LINE_EM = 1.46;\n/** Inter's average advance for mixed-case prose, in em. Deliberately generous: wrap estimates only. */\nconst CHAR_EM = 0.52;\n/** Header: mark, name, status pill, hairline. Includes its own padding-bottom. */\nconst MARK_EM = 1.9;\nconst HEADER_EM = 2.95;\n/** Between the five blocks of the column. */\nconst GAP_EM = 1.25;\nconst PAD_X_EM = 1.15;\nconst CHIP_PAD_Y_EM = 0.8;\n/** User chip width ceiling, as a fraction of the column. A chip that spans it stops reading as a chip. */\nconst CHIP_MAX = 0.74;\n/** Tool chips are narrower than the prose, which keeps them subordinate to it. */\nconst TOOL_W = 0.88;\nconst TOOL_HEAD_EM = 2.35;\nconst TOOL_PAD_Y_EM = 0.6;\nconst TOOL_GAP_EM = 0.75;\nconst GLYPH_EM = 1.35;\nconst GLYPH_GAP_EM = 0.62;\n/* Type scale, relative to the body size. */\nconst NAME_EM = 0.86;\nconst ARGS_EM = 0.78;\nconst META_EM = 0.7;\nconst RESULT_EM = 0.8;\nconst LABEL_EM = 0.62;\n\n/* -------------------------------------------------------------------- schema */\n\nexport const agentToolCallSchema = z.object({\n  name: z\n    .string()\n    .describe(\n      \"Tool name, set in mono. Dotted namespaces ('registry.search', 'timeline.compose') read as a real tool surface; verbs alone read as invented.\",\n    ),\n  args: z\n    .string()\n    .describe(\n      \"The arguments, on one line beside the name, formatted exactly as you want them read. Kept to one line — truncated with an ellipsis, and dropped entirely on portrait and square frames, where the chip has no room for it.\",\n    ),\n  result: z\n    .string()\n    .describe(\n      \"One-line result, revealed in muted ink under the name once the call resolves. Concrete counts and figures sell it; 'success' does not.\",\n    ),\n  meta: z\n    .string()\n    .describe(\"Right-aligned metadata for the call, landing with the result. An elapsed time like '412ms' is the convention.\"),\n});\n\nexport const agentChatStreamSchema = z.object({\n  prompt: z\n    .string()\n    .describe(\"The user's message, shown as a chip at the top right of the transcript. One sentence — the answer is the subject here, not the question.\"),\n  answer: z\n    .string()\n    .describe(\n      \"The assistant's first reply, revealed word by word before the tools run. 20–30 words is the sweet spot for the 8s budget; longer is streamed faster to fit.\",\n    ),\n  reply: z\n    .string()\n    .describe(\n      \"The closing reply, revealed after the last call resolves. This is the line that sits on the held frame, so make it the payoff — 12–18 words.\",\n    ),\n  tools: z\n    .array(agentToolCallSchema)\n    .min(1)\n    .max(3)\n    .default([\n      {\n        name: \"registry.search\",\n        args: \"category: typography\",\n        result: \"12 items · kinetic-headline ranked first\",\n        meta: \"412ms\",\n      },\n      {\n        name: \"timeline.compose\",\n        args: \"scenes: 6, fps: 30\",\n        result: \"00:29.8 · 1920×1080 · handoffs clean\",\n        meta: \"1.9s\",\n      },\n    ])\n    .describe(\n      \"Tool-call chips, appearing between the two replies and overlapping by half a second. Two is the honest number for an 8s scene; three is the ceiling and squeezes the prose.\",\n    ),\n  agent: z\n    .string()\n    .default(\"Assistant\")\n    .describe(\"Name in the header, beside the accent mark. A product name works better here than a model name.\"),\n  model: z\n    .string()\n    .default(\"registry-1\")\n    .describe(\"Mono model tag in the metadata rail. Empty string drops the row.\"),\n  pace: z\n    .number()\n    .min(0.4)\n    .max(2.5)\n    .default(1)\n    .describe(\n      \"Streaming speed multiplier. 1 is ~13 words a second, which is what a fast stream looks like; 0.7 reads more deliberate. Long copy is sped up automatically to make the budget regardless.\",\n    ),\n  rail: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Show the metadata rail — model, token count, tool progress, elapsed clock. Ignored on portrait and square frames, where the transcript takes the full width instead.\",\n    ),\n  seed: z\n    .string()\n    .default(\"stream-01\")\n    .describe(\n      \"Seeds the per-word jitter that keeps the stream off the metronome. Change it for a differently-paced take of identical duration; the same seed always renders identical frames.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof agentChatStreamSchema>;\ntype ToolCall = z.infer<typeof agentToolCallSchema>;\n\n/* ------------------------------------------------------------------ schedule */\n\ntype Schedule = {\n  /** Frames per word, after the auto-fit. */\n  rate: number;\n  toolsAt: number;\n  replyAt: number;\n};\n\n/**\n * The whole exchange, resolved to absolute frames before anything renders.\n *\n * Everything except the stream is a fixed beat, so the one free variable is the\n * word rate: the fixed beats are subtracted from the budget and the remaining\n * room is divided by the words that have to fit in it. Arbitrary copy therefore\n * keeps the rhythm of the scene and just streams faster, rather than running\n * past the exit window.\n */\nconst buildSchedule = (answerWords: number, replyWords: number, toolCount: number, pace: number): Schedule => {\n  const toolWindow = (toolCount - 1) * TOOL_STRIDE + TOOL_SPIN + TOOL_RESOLVE;\n  const fixed = ANSWER_AT + ANSWER_TO_TOOLS + toolWindow + TOOLS_TO_REPLY;\n  const room = DEADLINE - fixed;\n  const words = Math.max(1, answerWords + replyWords);\n  const rate = Math.max(WORD_FLOOR, Math.min(WORD_BASE / pace, room / words));\n\n  const toolsAt = ANSWER_AT + answerWords * rate + ANSWER_TO_TOOLS;\n  return { rate, toolsAt, replyAt: toolsAt + toolWindow + TOOLS_TO_REPLY };\n};\n\n/* -------------------------------------------------------------------- reveal */\n\ntype Reveal = {\n  /** Words fully arrived. The paragraph is a hard slice of the string — tokens do not fade in. */\n  shown: number;\n  /** Absolute frame each word lands on, for the tint that trails the write head. */\n  starts: number[];\n  done: boolean;\n};\n\nconst reveal = (count: number, at: number, rate: number, seed: string, frame: number): Reveal => {\n  const starts: number[] = [];\n  let cursor = at;\n  for (let i = 0; i < count; i++) {\n    starts.push(cursor);\n    // Jitter averages to 1, so the schedule above still holds.\n    cursor += rate * (0.55 + random(`${seed}/${i}`) * 0.9);\n  }\n  let shown = 0;\n  while (shown < count && frame >= starts[shown]) shown += 1;\n  return { shown, starts, done: frame >= cursor };\n};\n\n/* --------------------------------------------------------------------- fit */\n\ntype Metrics = {\n  size: number;\n  promptLines: number;\n  answerLines: number;\n  replyLines: number;\n  resultLines: number[];\n  total: number;\n};\n\nconst lineCount = (text: string, size: number, boxW: number) =>\n  Math.max(1, Math.ceil((text.trim().length * size * CHAR_EM) / Math.max(1, boxW)));\n\ntype Copy = { prompt: string; answer: string; reply: string; tools: ToolCall[] };\n\n/** Reserved height of the whole transcript at one body size. */\nconst measure = (size: number, colW: number, copy: Copy): Metrics => {\n  const promptLines = lineCount(copy.prompt, size, colW * CHIP_MAX - 2 * PAD_X_EM * size);\n  const answerLines = lineCount(copy.answer, size, colW);\n  const replyLines = lineCount(copy.reply, size, colW);\n  const resultBox = colW * TOOL_W - 2 * PAD_X_EM * size - (GLYPH_EM + GLYPH_GAP_EM) * size;\n  const resultLines = copy.tools.map((tool) => lineCount(tool.result, size * RESULT_EM, resultBox));\n\n  const tools = copy.tools.reduce(\n    (acc, _tool, i) =>\n      acc +\n      TOOL_HEAD_EM * size +\n      2 * TOOL_PAD_Y_EM * size +\n      resultLines[i] * LINE_EM * RESULT_EM * size +\n      (i > 0 ? TOOL_GAP_EM * size : 0),\n    0,\n  );\n\n  const total =\n    HEADER_EM * size +\n    4 * GAP_EM * size +\n    (promptLines * LINE_EM + 2 * CHIP_PAD_Y_EM) * size +\n    answerLines * LINE_EM * size +\n    tools +\n    replyLines * LINE_EM * size;\n\n  return { size, promptLines, answerLines, replyLines, resultLines, total };\n};\n\n/**\n * Solve for the largest body size whose reserved transcript still clears the\n * frame. Wrapping depends on the size and the size depends on the wrapping, so\n * this is a bisection rather than a division — 20 passes over a cheap estimate.\n */\nconst fitMetrics = (avail: number, colW: number, copy: Copy, minSize: number, maxSize: number): Metrics => {\n  const at = (s: number) => measure(s, colW, copy);\n  const biggest = at(maxSize);\n  if (biggest.total <= avail) return biggest;\n  let lo = minSize;\n  let hi = maxSize;\n  for (let i = 0; i < 20; i += 1) {\n    const mid = (lo + hi) / 2;\n    if (at(mid).total <= avail) lo = mid;\n    else hi = mid;\n  }\n  return at(lo);\n};\n\n/* -------------------------------------------------------------------- helpers */\n\nconst mono = (tokens: BrandTokens, size: number, color: string): React.CSSProperties => ({\n  fontFamily: tokens.fontMono,\n  fontSize: size,\n  lineHeight: 1,\n  color,\n});\n\nconst label = (tokens: BrandTokens, size: number, color: string): React.CSSProperties => ({\n  ...mono(tokens, size, color),\n  textTransform: \"uppercase\",\n  letterSpacing: size * 0.16,\n});\n\nconst ease = (frame: number, from: number, span: number) =>\n  interpolate(frame, [from, from + span], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\nconst withCommas = (n: number) => n.toString().replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n\n/** m:ss.t — an elapsed clock, ticking at 10Hz in the rail. */\nconst clock = (frame: number, fps: number) => {\n  const seconds = Math.max(0, frame) / fps;\n  const s = Math.floor(seconds % 60);\n  return `${Math.floor(seconds / 60)}:${s < 10 ? \"0\" : \"\"}${s}.${Math.floor((seconds * 10) % 10)}`;\n};\n\n/* ----------------------------------------------------------------- component */\n\nconst AgentChatStream: React.FC<Props> = ({ prompt, answer, reply, tools, agent, model, pace, rail, seed, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const frame = stage.frame;\n\n  const answerWords = answer.split(/\\s+/).filter(Boolean);\n  const replyWords = reply.split(/\\s+/).filter(Boolean);\n  const script = buildSchedule(answerWords.length, replyWords.length, tools.length, pace);\n\n  const answerStream = reveal(answerWords.length, ANSWER_AT, script.rate, `${seed}/a`, frame);\n  const replyStream = reveal(replyWords.length, script.replyAt, script.rate, `${seed}/b`, frame);\n\n  // The rail is a landscape luxury: in 9:16 and 1:1 the transcript takes the\n  // whole width instead, and the type grows to match.\n  const wide = width / height > 1.2;\n  const showRail = rail && wide;\n  const padX = width * (wide ? 0.075 : 0.055);\n  const padY = height * (wide ? 0.085 : 0.06);\n  const innerW = width - 2 * padX;\n  const railGap = showRail ? innerW * 0.055 : 0;\n  const railW = showRail ? innerW * 0.2 : 0;\n  const colW = innerW - railW - railGap;\n\n  const m = fitMetrics(\n    height - 2 * padY,\n    colW,\n    { prompt, answer, reply, tools },\n    width * 0.008,\n    // 16:9 is width-rich and height-poor, so the cap there is a legibility\n    // ceiling; portrait and square are the reverse, and want the type as large\n    // as their height will carry.\n    width * (wide ? 0.021 : 0.045),\n  );\n  const size = m.size;\n  // Token radius is authored for UI at ~16px type. Scale it with the type so a\n  // 10px corner still reads as a 10px corner. 0 stays 0.\n  const radius = t.radius * (size / 16);\n\n  const resolved = tools.filter((_tool, i) => frame >= script.toolsAt + i * TOOL_STRIDE + TOOL_SPIN).length;\n  // Not a real tokenizer — a plausible one. ~1.4 tokens a word, plus what the\n  // tool results cost, so the rail ticks with the stream instead of on a timer.\n  const tokens = Math.round(96 + (answerStream.shown + replyStream.shown) * 5.4 + resolved * 84);\n  const status = frame < ANSWER_AT ? \"thinking\" : replyStream.done ? \"ready\" : \"streaming\";\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, alignItems: \"center\", justifyContent: \"center\" }}>\n      {/* One presence container: header, transcript and rail arrive and leave as a\n          single object, and every internal animation inherits this opacity\n          rather than fighting it. */}\n      <div\n        style={{\n          width: innerW,\n          display: \"flex\",\n          alignItems: \"stretch\",\n          gap: railGap,\n          opacity: stage.presence,\n          transform: `translateY(${(1 - stage.presence) * size * 0.55}px) scale(${0.994 + 0.006 * stage.presence})`,\n        }}\n      >\n        <div style={{ flex: 1, minWidth: 0, display: \"flex\", flexDirection: \"column\", gap: GAP_EM * size }}>\n          <Header agent={agent} status={status} steady={replyStream.done} tokens={t} size={size} radius={radius} frame={frame} />\n\n          <div style={{ display: \"flex\", justifyContent: \"flex-end\" }}>\n            <UserChip\n              text={prompt}\n              lines={m.promptLines}\n              maxWidth={colW * CHIP_MAX}\n              tokens={t}\n              size={size}\n              radius={radius}\n              frame={frame}\n            />\n          </div>\n\n          <Paragraph\n            words={answerWords}\n            stream={answerStream}\n            lines={m.answerLines}\n            caret={frame >= ANSWER_AT && frame < script.toolsAt}\n            tokens={t}\n            size={size}\n            frame={frame}\n          >\n            <ThinkingDots tokens={t} size={size} frame={frame} />\n          </Paragraph>\n\n          <div style={{ display: \"flex\", flexDirection: \"column\", gap: TOOL_GAP_EM * size }}>\n            {tools.map((tool, i) => (\n              <ToolChip\n                key={`${tool.name}-${i}`}\n                tool={tool}\n                at={script.toolsAt + i * TOOL_STRIDE}\n                resultLines={m.resultLines[i]}\n                width={colW * TOOL_W}\n                showArgs={wide}\n                tokens={t}\n                size={size}\n                radius={radius}\n                frame={frame}\n              />\n            ))}\n          </div>\n\n          <Paragraph\n            words={replyWords}\n            stream={replyStream}\n            lines={m.replyLines}\n            caret={frame >= script.replyAt}\n            tokens={t}\n            size={size}\n            frame={frame}\n          />\n        </div>\n\n        {showRail ? (\n          <div\n            style={{\n              width: railW,\n              borderLeft: `1px solid ${t.rule}`,\n              paddingLeft: innerW * 0.028,\n              display: \"flex\",\n              flexDirection: \"column\",\n            }}\n          >\n            {model ? <RailRow name=\"model\" value={model} tokens={t} size={size} /> : null}\n            <RailRow name=\"tokens\" value={withCommas(tokens)} tokens={t} size={size} />\n            <RailRow name=\"tools\" value={`${resolved}/${tools.length}`} tokens={t} size={size} />\n            <div style={{ flex: 1 }} />\n            <RailRow name=\"elapsed\" value={clock(frame, stage.fps)} tokens={t} size={size} last />\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------- pieces */\n\n/** Accent mark, agent name, and a status pill pushed to the far edge of the column. */\nconst Header: React.FC<{\n  agent: string;\n  status: string;\n  steady: boolean;\n  tokens: BrandTokens;\n  size: number;\n  radius: number;\n  frame: number;\n}> = ({ agent, status, steady, tokens, size, radius, frame }) => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      gap: size * 0.7,\n      height: MARK_EM * size,\n      paddingBottom: (HEADER_EM - MARK_EM) * size,\n      borderBottom: `1px solid ${tokens.rule}`,\n      boxSizing: \"content-box\",\n    }}\n  >\n    <div\n      style={{\n        width: MARK_EM * size,\n        height: MARK_EM * size,\n        borderRadius: Math.max(radius, size * 0.18),\n        backgroundColor: tokens.accent,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexShrink: 0,\n      }}\n    >\n      <svg width={size * 1.1} height={size * 1.1} viewBox=\"0 0 24 24\">\n        {/* A four-point spark, drawn rather than shipped — no asset leaves this file. */}\n        <path d=\"M12 2.6 Q13.1 10.9 21.4 12 Q13.1 13.1 12 21.4 Q10.9 13.1 2.6 12 Q10.9 10.9 12 2.6 Z\" fill={tokens.accentInk} />\n      </svg>\n    </div>\n\n    <span\n      style={{\n        fontFamily: tokens.fontDisplay,\n        fontSize: size * 1.12,\n        fontWeight: 600,\n        letterSpacing: size * -0.012,\n        color: tokens.ink,\n      }}\n    >\n      {agent}\n    </span>\n\n    <div\n      style={{\n        marginLeft: \"auto\",\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.38,\n        padding: `${size * 0.2}px ${size * 0.5}px`,\n        border: `1px solid ${tokens.rule}`,\n        borderRadius: radius,\n      }}\n    >\n      <div\n        style={{\n          width: size * 0.34,\n          height: size * 0.34,\n          borderRadius: \"50%\",\n          backgroundColor: tokens.accent,\n          // A held pulse while work is in flight; dead steady once it is not.\n          opacity: steady ? 1 : 0.45 + 0.55 * (0.5 + 0.5 * Math.sin(frame * 0.26)),\n        }}\n      />\n      <span style={label(tokens, size * LABEL_EM, tokens.inkMuted)}>{status}</span>\n    </div>\n  </div>\n);\n\n/** The user's message: a surface chip, its inner corner clipped towards the sender. */\nconst UserChip: React.FC<{\n  text: string;\n  lines: number;\n  maxWidth: number;\n  tokens: BrandTokens;\n  size: number;\n  radius: number;\n  frame: number;\n}> = ({ text, lines, maxWidth, tokens, size, radius, frame }) => {\n  const arrive = ease(frame, PROMPT_AT, 15);\n  return (\n    <div\n      style={{\n        maxWidth,\n        minHeight: lines * LINE_EM * size,\n        padding: `${CHIP_PAD_Y_EM * size}px ${PAD_X_EM * size}px`,\n        backgroundColor: tokens.surface,\n        border: `1px solid ${tokens.rule}`,\n        borderRadius: `${radius}px ${radius}px ${radius * 0.3}px ${radius}px`,\n        boxShadow: `0 ${size * 0.22}px ${size * 0.75}px rgba(0,0,0,0.16)`,\n        fontFamily: tokens.fontBody,\n        fontSize: size,\n        lineHeight: LINE_EM,\n        color: tokens.ink,\n        opacity: arrive,\n        // It came from the composer, below the frame.\n        transform: `translateY(${(1 - arrive) * size * 1.1}px)`,\n      }}\n    >\n      {text}\n    </div>\n  );\n};\n\n/**\n * A streamed paragraph. The reveal is a hard slice of the word list — tokens\n * arrive, they do not fade — and the newest words carry an accent tint that\n * decays to ink, which is the only thing marking the write head besides the\n * caret. The block reserves its finished height, so the chips below it never\n * move as it fills.\n */\nconst Paragraph: React.FC<{\n  words: string[];\n  stream: Reveal;\n  lines: number;\n  caret: boolean;\n  tokens: BrandTokens;\n  size: number;\n  frame: number;\n  children?: React.ReactNode;\n}> = ({ words, stream, lines, caret, tokens, size, frame, children }) => (\n  <div\n    style={{\n      position: \"relative\",\n      minHeight: lines * LINE_EM * size,\n      fontFamily: tokens.fontBody,\n      fontSize: size,\n      lineHeight: LINE_EM,\n      color: tokens.ink,\n    }}\n  >\n    {children}\n    {words.slice(0, stream.shown).map((word, i) => (\n      <React.Fragment key={`${word}-${i}`}>\n        {i > 0 ? \" \" : null}\n        <span\n          style={{\n            color: interpolateColors(\n              interpolate(frame - stream.starts[i], [0, 7], [0, 1], {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n              }),\n              [0, 1],\n              [tokens.accent, tokens.ink],\n            ),\n          }}\n        >\n          {word}\n        </span>\n      </React.Fragment>\n    ))}\n    {caret ? <Caret solid={!stream.done} tokens={tokens} size={size} frame={frame} /> : null}\n  </div>\n);\n\n/** Solid while words are landing, hard blink once they have stopped. */\nconst Caret: React.FC<{ solid: boolean; tokens: BrandTokens; size: number; frame: number }> = ({\n  solid,\n  tokens,\n  size,\n  frame,\n}) => (\n  <span\n    style={{\n      display: \"inline-block\",\n      width: Math.max(2, size * 0.075),\n      height: size * 0.92,\n      marginLeft: size * 0.18,\n      verticalAlign: \"-0.13em\",\n      backgroundColor: tokens.accent,\n      opacity: solid || frame % BLINK < BLINK * 0.55 ? 1 : 0,\n    }}\n  />\n);\n\n/**\n * Thinking dots, sitting in the answer's own first line and crossfading out as\n * the first word lands — so the answer starts exactly where the waiting was.\n */\nconst ThinkingDots: React.FC<{ tokens: BrandTokens; size: number; frame: number }> = ({ tokens, size, frame }) => {\n  const show = ease(frame, THINK_AT, 9) * (1 - ease(frame, ANSWER_AT - 7, 8));\n  if (show <= 0.001) return null;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        height: LINE_EM * size,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.38,\n        opacity: show,\n      }}\n    >\n      {[0, 1, 2].map((i) => {\n        const wave = 0.5 + 0.5 * Math.sin((frame - THINK_AT) * 0.3 - i * 0.85);\n        return (\n          <div\n            key={i}\n            style={{\n              width: size * 0.33,\n              height: size * 0.33,\n              borderRadius: \"50%\",\n              backgroundColor: tokens.inkMuted,\n              opacity: 0.35 + 0.65 * wave,\n              transform: `translateY(${-wave * size * 0.12}px)`,\n            }}\n          />\n        );\n      })}\n    </div>\n  );\n};\n\n/**\n * One tool call. Mono name, muted arguments, a spinner that resolves into a\n * check drawing itself on, and a result line whose space is reserved from the\n * moment the chip lands so the chip never grows.\n */\nconst ToolChip: React.FC<{\n  tool: ToolCall;\n  at: number;\n  resultLines: number;\n  width: number;\n  showArgs: boolean;\n  tokens: BrandTokens;\n  size: number;\n  radius: number;\n  frame: number;\n}> = ({ tool, at, resultLines, width, showArgs, tokens, size, radius, frame }) => {\n  const arrive = ease(frame, at, 13);\n  const resolveAt = at + TOOL_SPIN;\n  const draw = ease(frame, resolveAt, 9);\n  const result = ease(frame, resolveAt + 3, 11);\n  const glyph = GLYPH_EM * size;\n\n  return (\n    <div\n      style={{\n        width,\n        backgroundColor: tokens.surface,\n        border: `1px solid ${tokens.rule}`,\n        borderRadius: radius,\n        overflow: \"hidden\",\n        boxShadow: `0 ${size * 0.22}px ${size * 0.75}px rgba(0,0,0,0.16)`,\n        opacity: arrive,\n        transform: `translateY(${(1 - arrive) * size * 0.5}px)`,\n      }}\n    >\n      <div\n        style={{\n          height: TOOL_HEAD_EM * size,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: GLYPH_GAP_EM * size,\n          padding: `0 ${PAD_X_EM * size}px`,\n        }}\n      >\n        <svg width={glyph} height={glyph} viewBox=\"0 0 24 24\" style={{ flexShrink: 0 }}>\n          <circle cx={12} cy={12} r={8.6} fill=\"none\" stroke={tokens.rule} strokeWidth={2} />\n          {/* pathLength normalises the arc to 1, so the dash maths is unit-free\n              and needs no path-length measurement at render time. */}\n          <circle\n            cx={12}\n            cy={12}\n            r={8.6}\n            fill=\"none\"\n            stroke={tokens.inkMuted}\n            strokeWidth={2}\n            strokeLinecap=\"round\"\n            pathLength={1}\n            strokeDasharray=\"0.27 0.73\"\n            opacity={1 - draw}\n            transform={`rotate(${(frame - at) * 7.5} 12 12)`}\n          />\n          <path\n            d=\"M6.2 12.6 L10.2 16.6 L18 8.2\"\n            fill=\"none\"\n            stroke={tokens.accent}\n            strokeWidth={2.2}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            pathLength={1}\n            strokeDasharray=\"1\"\n            strokeDashoffset={1 - draw}\n          />\n        </svg>\n\n        <span style={{ ...mono(tokens, size * NAME_EM, tokens.ink), fontWeight: 500, flexShrink: 0 }}>{tool.name}</span>\n        {showArgs ? (\n          <span\n            style={{\n              ...mono(tokens, size * ARGS_EM, tokens.inkMuted),\n              flex: 1,\n              minWidth: 0,\n              whiteSpace: \"nowrap\",\n              overflow: \"hidden\",\n              textOverflow: \"ellipsis\",\n            }}\n          >\n            {tool.args}\n          </span>\n        ) : (\n          <div style={{ flex: 1 }} />\n        )}\n        <span style={{ ...mono(tokens, size * META_EM, tokens.inkMuted), flexShrink: 0, opacity: result }}>\n          {tool.meta}\n        </span>\n      </div>\n\n      <div\n        style={{\n          borderTop: `1px solid ${tokens.rule}`,\n          padding: `${TOOL_PAD_Y_EM * size}px ${PAD_X_EM * size}px`,\n          paddingLeft: PAD_X_EM * size + glyph + GLYPH_GAP_EM * size,\n          minHeight: resultLines * LINE_EM * RESULT_EM * size,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: tokens.fontBody,\n            fontSize: size * RESULT_EM,\n            lineHeight: LINE_EM,\n            color: tokens.inkMuted,\n            opacity: result,\n            transform: `translateY(${(1 - result) * size * 0.16}px)`,\n          }}\n        >\n          {tool.result}\n        </div>\n      </div>\n    </div>\n  );\n};\n\n/** One ledger row in the metadata rail. */\nconst RailRow: React.FC<{ name: string; value: string; tokens: BrandTokens; size: number; last?: boolean }> = ({\n  name,\n  value,\n  tokens,\n  size,\n  last,\n}) => (\n  <div\n    style={{\n      borderTop: `1px solid ${tokens.rule}`,\n      paddingTop: size * 0.5,\n      marginBottom: last ? 0 : size * 0.95,\n    }}\n  >\n    <div style={label(tokens, size * LABEL_EM, tokens.inkMuted)}>{name}</div>\n    <div\n      style={{\n        ...mono(tokens, size * 0.95, tokens.ink),\n        marginTop: size * 0.32,\n        fontVariantNumeric: \"tabular-nums\",\n        whiteSpace: \"nowrap\",\n        overflow: \"hidden\",\n        textOverflow: \"ellipsis\",\n      }}\n    >\n      {value}\n    </div>\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"agent-chat-stream\",\n  name: \"Agent Chat Stream\",\n  category: \"product\",\n  summary:\n    \"An assistant answers word by word while tool-call chips spin, resolve to a check and settle under a blinking caret\",\n  tags: [\"ai\", \"chat\", \"streaming text\", \"tool call\", \"product ui\", \"caret\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: agentChatStreamSchema,\n  defaults: {\n    prompt: \"Turn the launch notes into a 30-second film.\",\n    answer:\n      \"Reading the notes now. I'll pull the headline, three feature beats and a closing card, then compose them on the shared timing grammar.\",\n    reply: \"Six scenes, twenty-nine point eight seconds. Preview is ready whenever you want to look.\",\n    tools: [\n      {\n        name: \"registry.search\",\n        args: \"category: typography\",\n        result: \"12 items · kinetic-headline ranked first\",\n        meta: \"412ms\",\n      },\n      {\n        name: \"timeline.compose\",\n        args: \"scenes: 6, fps: 30\",\n        result: \"00:29.8 · 1920×1080 · handoffs clean\",\n        meta: \"1.9s\",\n      },\n    ],\n    agent: \"Assistant\",\n    model: \"registry-1\",\n    pace: 1,\n    rail: true,\n    seed: \"stream-01\",\n    brand: \"slate\",\n  },\n  component: AgentChatStream,\n  file: \"src/registry/items/agent-chat-stream.tsx\",\n  origin: {\n    prompt:\n      \"An AI chat panel where the assistant's answer streams in word by word, a couple of tool calls appear inline as small chips with a spinner that resolves into a check and a muted result line, and the caret is still blinking on the last frame. Precise and quiet, not flashy.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An AI chat panel where the assistant's answer streams in word by word, a couple of tool calls appear inline as small chips with a spinner that resolves into a check and a muted result line, and the caret is still blinking on the last frame. Precise and quiet, not flashy.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4197,
      "props": [
        {
          "name": "prompt",
          "type": "string",
          "description": "The user's message, shown as a chip at the top right of the transcript. One sentence — the answer is the subject here, not the question.",
          "required": true,
          "defaultValue": "Turn the launch notes into a 30-second film.",
          "options": null
        },
        {
          "name": "answer",
          "type": "string",
          "description": "The assistant's first reply, revealed word by word before the tools run. 20–30 words is the sweet spot for the 8s budget; longer is streamed faster to fit.",
          "required": true,
          "defaultValue": "Reading the notes now. I'll pull the headline, three feat…",
          "options": null
        },
        {
          "name": "reply",
          "type": "string",
          "description": "The closing reply, revealed after the last call resolves. This is the line that sits on the held frame, so make it the payoff — 12–18 words.",
          "required": true,
          "defaultValue": "Six scenes, twenty-nine point eight seconds. Preview is r…",
          "options": null
        },
        {
          "name": "tools",
          "type": "array",
          "description": "Tool-call chips, appearing between the two replies and overlapping by half a second. Two is the honest number for an 8s scene; three is the ceiling and squeezes the prose.",
          "required": false,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "agent",
          "type": "string",
          "description": "Name in the header, beside the accent mark. A product name works better here than a model name.",
          "required": false,
          "defaultValue": "Assistant",
          "options": null
        },
        {
          "name": "model",
          "type": "string",
          "description": "Mono model tag in the metadata rail. Empty string drops the row.",
          "required": false,
          "defaultValue": "registry-1",
          "options": null
        },
        {
          "name": "pace",
          "type": "number",
          "description": "Streaming speed multiplier. 1 is ~13 words a second, which is what a fast stream looks like; 0.7 reads more deliberate. Long copy is sped up automatically to make the budget regardless.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "rail",
          "type": "boolean",
          "description": "Show the metadata rail — model, token count, tool progress, elapsed clock. Ignored on portrait and square frames, where the transcript takes the full width instead.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seeds the per-word jitter that keeps the stream off the metronome. Change it for a differently-paced take of identical duration; the same seed always renders identical frames.",
          "required": false,
          "defaultValue": "stream-01",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "agent-composer",
      "name": "Agent Composer",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A prompt types into a composer, the submit button takes a real press, and the agent's work reports itself step by step",
      "tags": [
        "agent",
        "prompt",
        "ui",
        "typing",
        "spinner",
        "product",
        "dark"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/agent-composer.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const agentComposerSchema = z.object({\n  prompt: z\n    .string()\n    .describe(\"The prompt that types into the composer. Written as a person would actually type it.\"),\n  chip: z\n    .string()\n    .default(\"Plan\")\n    .describe(\"Label on the mode chip beside the submit button.\"),\n  steps: z\n    .array(z.string())\n    .min(1)\n    .max(5)\n    .describe(\"Work the agent reports after submit, in order. Each completes before the next begins; the last one stays running.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS. The submit button uses accentAlt, so brands with a warm second accent read closest to the reference.\"),\n});\n\ntype Props = z.infer<typeof agentComposerSchema>;\n\nconst BUDGET = BUDGETS.feature;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.86\" numOctaves={3} seed={Math.floor(frame / 2) % 64} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst AgentComposer: React.FC<Props> = ({ prompt, chip, steps, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const TYPE_START = 10;\n  const CPF = 1.35;\n  const typed = Math.max(0, Math.min(prompt.length, Math.floor((frame - TYPE_START) * CPF)));\n  const typingDone = TYPE_START + prompt.length / CPF;\n  const SUBMIT = typingDone + 10;\n\n  const submitted = interpolate(frame, [SUBMIT, SUBMIT + 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // The caret blinks on the standard 530ms cadence, and stops once submitted.\n  const caretOn = frame < SUBMIT && Math.floor(time / 0.53) % 2 === 0;\n\n  // A press on the button, not a colour change: the button dips and rebounds.\n  const press = interpolate(frame, [SUBMIT - 3, SUBMIT, SUBMIT + 7], [0, 1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const barW = width * 0.62;\n  const fs = width * 0.019;\n  const barH = fs * 3.4;\n\n  const STEP_GAP = 26;\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.03]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(70% 44% at 50% 34%, ${alpha(t.surface, 0.75)} 0%, ${alpha(\n            t.surface,\n            0.2,\n          )} 38%, ${alpha(t.bg, 0.95)} 72%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(40% 26% at 50% 62%, ${alpha(t.accentAlt, 0.16 * submitted)} 0%, ${alpha(\n            t.accentAlt,\n            0,\n          )} 70%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 45%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* -------------------------------------------------- the composer */}\n        <div\n          style={{\n            width: barW,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: fs * 0.7,\n            padding: `${fs * 0.55}px ${fs * 0.55}px ${fs * 0.55}px ${fs * 1.4}px`,\n            borderRadius: barH,\n            backgroundColor: alpha(t.surface, 0.9),\n            border: `1px solid ${alpha(t.ink, 0.09)}`,\n            boxShadow: `inset 0 1px 0 ${alpha(\"#fff\", 0.05)}, 0 ${fs * 1.2}px ${fs * 3}px ${alpha(\"#000\", 0.42)}`,\n            // Settles back a touch on submit, as if handing off.\n            transform: `translateY(${interpolate(submitted, [0, 1], [0, -fs * 0.6])}px) scale(${interpolate(\n              submitted,\n              [0, 1],\n              [1, 0.985],\n            )})`,\n          }}\n        >\n          {/* A real input scrolls its own content once the caret reaches the\n              right edge — it does not clip the text and leave it sitting there.\n              Char width is estimated from the size rather than measured, which\n              is close enough at one decimal and keeps the component pure. */}\n          <div style={{ flex: 1, minWidth: 0, overflow: \"hidden\", display: \"flex\", alignItems: \"center\", height: barH }}>\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                transform: `translateX(${-Math.max(0, typed * fs * 0.53 - fs * 21)}px)`,\n              }}\n            >\n            <span\n              style={{\n                fontFamily: t.fontBody,\n                fontSize: fs * 1.05,\n                color: t.ink,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {prompt.slice(0, typed)}\n            </span>\n            <span\n              style={{\n                display: \"inline-block\",\n                width: Math.max(2, fs * 0.09),\n                height: fs * 1.3,\n                marginLeft: fs * 0.1,\n                backgroundColor: t.ink,\n                opacity: caretOn ? 1 : 0,\n              }}\n            />\n            </div>\n          </div>\n\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: fs * 0.35,\n              padding: `${fs * 0.42}px ${fs * 0.8}px`,\n              borderRadius: 999,\n              backgroundColor: alpha(t.ink, 0.08),\n              fontFamily: t.fontBody,\n              fontSize: fs * 0.92,\n              color: t.inkMuted,\n              flexShrink: 0,\n            }}\n          >\n            <svg width={fs * 0.95} height={fs * 0.95} viewBox=\"0 0 24 24\" aria-hidden>\n              <path d=\"M12 3 22 8 12 13 2 8z\" fill=\"none\" stroke={t.inkMuted} strokeWidth=\"1.8\" strokeLinejoin=\"round\" />\n              <path d=\"M2 13l10 5 10-5\" fill=\"none\" stroke={t.inkMuted} strokeWidth=\"1.8\" strokeLinejoin=\"round\" />\n            </svg>\n            {chip}\n          </div>\n\n          <div\n            style={{\n              width: barH,\n              height: barH,\n              flexShrink: 0,\n              borderRadius: \"50%\",\n              backgroundColor: t.accentAlt,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              transform: `scale(${1 - press * 0.12})`,\n              boxShadow: `0 0 ${fs * 2.4 * press}px ${alpha(t.accentAlt, 0.6 * press)}`,\n            }}\n          >\n            <svg width={fs * 1.5} height={fs * 1.5} viewBox=\"0 0 24 24\" aria-hidden>\n              <path d=\"M5 12h13M12 5l7 7-7 7\" fill=\"none\" stroke={t.bg} strokeWidth=\"2.4\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n            </svg>\n          </div>\n        </div>\n\n        {/* ------------------------------------------------- the task stack */}\n        <div\n          style={{\n            width: barW,\n            marginTop: fs * 1.6,\n            borderRadius: fs * 1.4,\n            backgroundColor: alpha(t.surface, 0.7),\n            border: `1px solid ${alpha(t.ink, 0.08)}`,\n            padding: fs * 1.3,\n            opacity: submitted,\n            transform: `translateY(${interpolate(submitted, [0, 1], [fs * 1.4, 0])}px)`,\n          }}\n        >\n          {steps.map((s, i) => {\n            const at = SUBMIT + 14 + i * STEP_GAP;\n            const appear = interpolate(frame, [at, at + 10], [0, 1], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n              easing: EASING.enter,\n            });\n            const isLast = i === steps.length - 1;\n            const done = !isLast && frame > at + STEP_GAP - 4;\n\n            return (\n              <div\n                key={s}\n                style={{\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  gap: fs * 0.8,\n                  padding: `${fs * 0.42}px 0`,\n                  opacity: appear * (done ? 0.55 : 1),\n                  transform: `translateY(${interpolate(appear, [0, 1], [fs * 0.5, 0])}px)`,\n                }}\n              >\n                {done ? (\n                  <svg width={fs * 1.1} height={fs * 1.1} viewBox=\"0 0 24 24\" aria-hidden>\n                    <path d=\"M4 12.5l5.2 5.2L20 7\" fill=\"none\" stroke={t.accentAlt} strokeWidth=\"3\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n                  </svg>\n                ) : (\n                  /* A real arc sweeping a circle, not a dot that fades — a\n                     spinner is the one place a loop is honest. */\n                  <svg width={fs * 1.1} height={fs * 1.1} viewBox=\"0 0 24 24\" aria-hidden>\n                    <circle cx=\"12\" cy=\"12\" r=\"9\" fill=\"none\" stroke={alpha(t.ink, 0.18)} strokeWidth=\"3\" />\n                    <path\n                      d=\"M12 3a9 9 0 019 9\"\n                      fill=\"none\"\n                      stroke={t.accentAlt}\n                      strokeWidth=\"3\"\n                      strokeLinecap=\"round\"\n                      transform={`rotate(${(time * 300) % 360} 12 12)`}\n                    />\n                  </svg>\n                )}\n                <span style={{ fontFamily: t.fontBody, fontSize: fs * 1.02, color: done ? t.inkMuted : t.ink }}>{s}</span>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(115% 80% at 50% 46%, ${alpha(\"#000\", 0)} 32%, ${alpha(\"#000\", 0.16)} 62%, ${alpha(\n            \"#000\",\n            0.5,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"agent-composer-grain\" frame={frame} opacity={0.07} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"agent-composer\",\n  name: \"Agent Composer\",\n  category: \"product\",\n  summary:\n    \"A prompt types into a composer, the submit button takes a real press, and the agent's work reports itself step by step\",\n  tags: [\"agent\", \"prompt\", \"ui\", \"typing\", \"spinner\", \"product\", \"dark\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: agentComposerSchema,\n  defaults: {\n    prompt: \"Make a 30-second launch video for Motion.\",\n    chip: \"Plan\",\n    steps: [\"Thinking through the brief\", \"Tuning the look\", \"Designing scene 2 of 3\"],\n    brand: \"slate\",\n  },\n  component: AgentComposer,\n  file: \"src/registry/items/agent-composer.tsx\",\n  origin: {\n    prompt:\n      \"A dark product frame showing someone prompting an agent. Type the prompt into a pill-shaped composer with a blinking caret at the real 530ms cadence, then press the circular accent submit button — an actual dip and rebound, not a colour change. After submit the composer settles back and a task card rises underneath, reporting each step in turn with a sweeping spinner that becomes a check as the next step starts.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A dark product frame showing someone prompting an agent. Type the prompt into a pill-shaped composer with a blinking caret at the real 530ms cadence, then press the circular accent submit button — an actual dip and rebound, not a colour change. After submit the composer settles back and a task card rises underneath, reporting each step in turn with a sweeping spinner that becomes a check as the next step starts.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4006,
      "props": [
        {
          "name": "prompt",
          "type": "string",
          "description": "The prompt that types into the composer. Written as a person would actually type it.",
          "required": true,
          "defaultValue": "Make a 30-second launch video for Motion.",
          "options": null
        },
        {
          "name": "chip",
          "type": "string",
          "description": "Label on the mode chip beside the submit button.",
          "required": false,
          "defaultValue": "Plan",
          "options": null
        },
        {
          "name": "steps",
          "type": "array",
          "description": "Work the agent reports after submit, in order. Each completes before the next begins; the last one stays running.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. The submit button uses accentAlt, so brands with a warm second accent read closest to the reference.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "animate-in-presets",
      "name": "Animate In Presets",
      "category": "primitive",
      "categoryLabel": "Primitives",
      "summary": "Six entrance presets on six otherwise identical tiles, replayed so they can be compared directly rather than remembered",
      "tags": [
        "primitive",
        "entrance",
        "presets",
        "documentation",
        "comparison"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/animate-in-presets.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const animateInPresetsSchema = z.object({\n  cycle: z\n    .number()\n    .min(60)\n    .max(160)\n    .default(110)\n    .describe(\"Frames per demonstration pass. The grid replays so presets can be compared twice.\"),\n  perTile: z\n    .number()\n    .min(0)\n    .max(10)\n    .default(4)\n    .describe(\"Stagger between tiles, in frames. Zero fires them simultaneously.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof animateInPresetsSchema>;\n\nconst PRESETS = [\"fadeUp\", \"slideLeft\", \"scalePop\", \"blurIn\", \"maskUp\", \"flipY\"] as const;\ntype Preset = (typeof PRESETS)[number];\n\n/**\n * Documentation as a component.\n *\n * Every tile is identical apart from its entrance, which is the whole point: the\n * comparison is only useful if nothing else varies. The grid replays so a viewer\n * can watch one preset on the first pass and a different one on the second\n * without scrubbing.\n */\nconst presetStyle = (preset: Preset, t: number, spr: number, unit: number): React.CSSProperties => {\n  switch (preset) {\n    case \"fadeUp\":\n      return { opacity: t, transform: `translateY(${(1 - t) * unit * 0.5}px)` };\n    case \"slideLeft\":\n      return { opacity: t, transform: `translateX(${(1 - t) * unit * 0.9}px)` };\n    case \"scalePop\":\n      return { opacity: Math.min(1, t * 2), transform: `scale(${spr})` };\n    case \"blurIn\":\n      return { opacity: t, filter: `blur(${(1 - t) * unit * 0.14}px)` };\n    case \"maskUp\":\n      return { clipPath: `inset(${(1 - t) * 100}% 0% 0% 0%)`, opacity: 1 };\n    case \"flipY\":\n      return {\n        opacity: Math.min(1, t * 1.6),\n        transform: `perspective(${unit * 8}px) rotateY(${(1 - t) * -82}deg)`,\n      };\n    default:\n      return {};\n  }\n};\n\nconst AnimateInPresets: React.FC<Props> = ({ cycle, perTile, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const gap = width * 0.024;\n  const cols = 3;\n  const tileW = (width * 0.82 - gap * (cols - 1)) / cols;\n  const tileH = height * 0.26;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        opacity: stage.presence,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          marginBottom: height * 0.045,\n        }}\n      >\n        Entrance presets · pass {Math.floor(stage.frame / cycle) + 1}\n      </div>\n\n      <div style={{ display: \"flex\", flexWrap: \"wrap\", gap, width: width * 0.82 }}>\n        {PRESETS.map((preset, i) => (\n          <Tile\n            key={preset}\n            preset={preset}\n            index={i}\n            cycle={cycle}\n            perTile={perTile}\n            w={tileW}\n            h={tileH}\n            tokens={t}\n          />\n        ))}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Tile: React.FC<{\n  preset: Preset;\n  index: number;\n  cycle: number;\n  perTile: number;\n  w: number;\n  h: number;\n  tokens: BrandTokens;\n}> = ({ preset, index, cycle, perTile, w, h, tokens }) => {\n  const stage = useStage(BUDGETS.feature);\n  const { fps } = useVideoConfig();\n\n  // Local time inside the current pass, offset by this tile's place in the stagger.\n  const local = (stage.frame % cycle) - index * perTile;\n  const t = interpolate(local, [0, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const spr = spring({ frame: Math.max(0, local), fps, config: SPRINGS.pop, durationInFrames: 30 });\n\n  return (\n    <div style={{ width: w }}>\n      <div\n        style={{\n          height: h,\n          borderRadius: tokens.radius,\n          border: `1px solid ${tokens.rule}`,\n          backgroundColor: tokens.bg,\n          overflow: \"hidden\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div\n          style={{\n            width: w * 0.44,\n            height: h * 0.4,\n            borderRadius: tokens.radius * 0.6,\n            backgroundColor: index === 2 ? tokens.accent : tokens.surface,\n            border: `1px solid ${index === 2 ? tokens.accent : tokens.rule}`,\n            ...presetStyle(preset, t, spr, w * 0.2),\n          }}\n        />\n      </div>\n      <div\n        style={{\n          marginTop: h * 0.09,\n          fontFamily: tokens.fontMono,\n          fontSize: w * 0.052,\n          letterSpacing: w * 0.004,\n          color: tokens.ink,\n        }}\n      >\n        {preset}\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"animate-in-presets\",\n  name: \"Animate In Presets\",\n  category: \"primitive\",\n  summary:\n    \"Six entrance presets on six otherwise identical tiles, replayed so they can be compared directly rather than remembered\",\n  tags: [\"primitive\", \"entrance\", \"presets\", \"documentation\", \"comparison\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: animateInPresetsSchema,\n  defaults: { cycle: 110, perTile: 4, brand: \"slate\" },\n  component: AnimateInPresets,\n  file: \"src/registry/items/animate-in-presets.tsx\",\n  origin: {\n    prompt:\n      \"A showcase grid demonstrating six entrance presets side by side — fadeUp, slideLeft, scalePop, blurIn, maskUp and flipY — with everything identical except the motion, so they can be compared.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A showcase grid demonstrating six entrance presets side by side — fadeUp, slideLeft, scalePop, blurIn, maskUp and flipY — with everything identical except the motion, so they can be compared.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3867,
      "props": [
        {
          "name": "cycle",
          "type": "number",
          "description": "Frames per demonstration pass. The grid replays so presets can be compared twice.",
          "required": false,
          "defaultValue": "110",
          "options": null
        },
        {
          "name": "perTile",
          "type": "number",
          "description": "Stagger between tiles, in frames. Zero fires them simultaneously.",
          "required": false,
          "defaultValue": "4",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "architecture-flow",
      "name": "Architecture Flow",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "A system diagram assembles column by column, with orthogonal connectors that only draw once both of their endpoints exist",
      "tags": [
        "diagram",
        "architecture",
        "graph",
        "explainer",
        "evolvepath"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/architecture-flow.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { evolvePath } from \"@remotion/paths\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nconst nodeSchema = z.object({\n  id: z.string().describe(\"Unique node id, referenced by edges.\"),\n  label: z.string().describe(\"Node label.\"),\n  column: z.number().describe(\"Zero-based column. Nodes arrive column by column, left to right.\"),\n  row: z.number().describe(\"Zero-based row within the column.\"),\n  accent: z.boolean().optional().describe(\"Render this node in the accent colour.\"),\n});\n\nconst edgeSchema = z.object({\n  from: z.string().describe(\"Source node id.\"),\n  to: z.string().describe(\"Target node id.\"),\n  label: z.string().optional().describe(\"Short label rendered on the connector.\"),\n});\n\nexport const architectureFlowSchema = z.object({\n  title: z.string().default(\"Request path\").describe(\"Mono label above the diagram.\"),\n  nodes: z.array(nodeSchema).min(2).describe(\"The graph's nodes, positioned on a column grid.\"),\n  edges: z.array(edgeSchema).describe(\"Connectors. Each draws only once both endpoints exist.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof architectureFlowSchema>;\n\n/**\n * A system diagram that assembles itself from a graph.\n *\n * Two choices do the work. Edges route orthogonally — out, along, in — because\n * right-angle elbows read as a diagram while diagonals read as a sketch. And an\n * edge only starts drawing once both of its endpoints have arrived, so the\n * viewer never sees a connector reaching into empty space; that ordering is what\n * makes the assembly legible rather than busy.\n */\nconst ARRIVE = 12;\n\nconst ArchitectureFlow: React.FC<Props> = ({ title, nodes, edges, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const cols = Math.max(...nodes.map((n) => n.column)) + 1;\n  const boxW = width * 0.16;\n  const boxH = height * 0.115;\n  const colGap = (width * 0.84 - boxW * cols) / Math.max(1, cols - 1);\n  const rowGap = height * 0.055;\n\n  const place = (n: Props[\"nodes\"][number]) => {\n    const rowsInCol = nodes.filter((m) => m.column === n.column).length;\n    const stackH = rowsInCol * boxH + (rowsInCol - 1) * rowGap;\n    return {\n      x: width * 0.08 + n.column * (boxW + colGap),\n      y: height * 0.53 - stackH / 2 + n.row * (boxH + rowGap),\n    };\n  };\n\n  const byId = new Map(nodes.map((n) => [n.id, n]));\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: width * 0.08,\n          top: height * 0.13,\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n        }}\n      >\n        {title}\n      </div>\n\n      <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }}>\n        {edges.map((e, i) => {\n          const a = byId.get(e.from);\n          const b = byId.get(e.to);\n          if (!a || !b) return null;\n          const pa = place(a);\n          const pb = place(b);\n          const x1 = pa.x + boxW;\n          const y1 = pa.y + boxH / 2;\n          const x2 = pb.x;\n          const y2 = pb.y + boxH / 2;\n          const mid = x1 + (x2 - x1) / 2;\n          const d = `M ${x1} ${y1} H ${mid} V ${y2} H ${x2}`;\n\n          // Wait for the later of the two endpoints to have landed.\n          const after = Math.max(a.column, b.column) * ARRIVE + 18;\n          const draw = interpolate(stage.frame, [after, after + 16], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: EASING.travel,\n          });\n          const ev = evolvePath(draw, d);\n          const hair = Math.max(1.5, width * 0.0014);\n\n          return (\n            <g key={i}>\n              <path\n                d={d}\n                fill=\"none\"\n                stroke={t.rule}\n                strokeWidth={hair}\n                strokeDasharray={ev.strokeDasharray}\n                strokeDashoffset={ev.strokeDashoffset}\n              />\n              {draw > 0.98 ? (\n                <polygon\n                  points={`${x2},${y2} ${x2 - hair * 5},${y2 - hair * 3.2} ${x2 - hair * 5},${y2 + hair * 3.2}`}\n                  fill={t.inkMuted}\n                />\n              ) : null}\n              {e.label ? (\n                <text\n                  x={mid}\n                  y={y1 === y2 ? y1 - hair * 6 : (y1 + y2) / 2}\n                  textAnchor=\"middle\"\n                  fill={t.inkMuted}\n                  opacity={draw}\n                  style={{ fontFamily: t.fontMono, fontSize: width * 0.0085 }}\n                >\n                  {e.label}\n                </text>\n              ) : null}\n            </g>\n          );\n        })}\n      </svg>\n\n      {nodes.map((n) => {\n        const p = place(n);\n        return <Node key={n.id} node={n} x={p.x} y={p.y} w={boxW} h={boxH} tokens={t} />;\n      })}\n    </AbsoluteFill>\n  );\n};\n\nconst Node: React.FC<{\n  node: Props[\"nodes\"][number];\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  tokens: BrandTokens;\n}> = ({ node, x, y, w, h, tokens }) => {\n  const stage = useStaggeredStage(BUDGETS.feature, node.column, ARRIVE);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: y,\n        width: w,\n        height: h,\n        borderRadius: tokens.radius,\n        backgroundColor: tokens.surface,\n        border: `1px solid ${node.accent ? tokens.accent : tokens.rule}`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        padding: w * 0.06,\n        boxSizing: \"border-box\",\n        opacity: stage.enter,\n        transform: `translateY(${(1 - stage.enter) * h * 0.3}px)`,\n      }}\n    >\n      <span\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: w * 0.088,\n          letterSpacing: w * 0.002,\n          color: node.accent ? tokens.accent : tokens.ink,\n          textAlign: \"center\",\n        }}\n      >\n        {node.label}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"architecture-flow\",\n  name: \"Architecture Flow\",\n  category: \"diagram\",\n  summary:\n    \"A system diagram assembles column by column, with orthogonal connectors that only draw once both of their endpoints exist\",\n  tags: [\"diagram\", \"architecture\", \"graph\", \"explainer\", \"evolvepath\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: architectureFlowSchema,\n  defaults: {\n    title: \"Request path\",\n    nodes: [\n      { id: \"client\", label: \"Client\", column: 0, row: 0 },\n      { id: \"edge\", label: \"Edge\", column: 1, row: 0 },\n      { id: \"api\", label: \"API\", column: 2, row: 0, accent: true },\n      { id: \"cache\", label: \"Cache\", column: 3, row: 0 },\n      { id: \"db\", label: \"Postgres\", column: 3, row: 1 },\n    ],\n    edges: [\n      { from: \"client\", to: \"edge\", label: \"https\" },\n      { from: \"edge\", to: \"api\", label: \"rpc\" },\n      { from: \"api\", to: \"cache\", label: \"hit\" },\n      { from: \"api\", to: \"db\", label: \"miss\" },\n    ],\n    brand: \"slate\",\n  },\n  component: ArchitectureFlow,\n  file: \"src/registry/items/architecture-flow.tsx\",\n  origin: {\n    prompt:\n      \"A system architecture diagram that assembles from a JSON graph — node boxes appearing column by column, then orthogonal right-angle connectors drawing between them with small arrowheads and edge labels.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A system architecture diagram that assembles from a JSON graph — node boxes appearing column by column, then orthogonal right-angle connectors drawing between them with small arrowheads and edge labels.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.1583,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Mono label above the diagram.",
          "required": false,
          "defaultValue": "Request path",
          "options": null
        },
        {
          "name": "nodes",
          "type": "array",
          "description": "The graph's nodes, positioned on a column grid.",
          "required": true,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "edges",
          "type": "array",
          "description": "Connectors. Each draws only once both endpoints exist.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "aurora-statement",
      "name": "Aurora Statement",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "A headline resolves out of defocus over a drifting volumetric light field, with one specular pass across the glyphs",
      "tags": [
        "hero",
        "headline",
        "light field",
        "focus pull",
        "specular",
        "premium",
        "grain"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/aurora-statement.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\n/* ------------------------------------------------------------------ colour */\n\n/** `#rrggbb` → `\"r,g,b\"`, so a component can build rgba() ramps from tokens. */\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\n\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\n/* ------------------------------------------------------------------ schema */\n\nexport const auroraStatementSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"The statement. Three to six words; this is a hero frame, not a paragraph.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small tracked label above the headline. Omit for a purer frame.\"),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\"One quiet line under the headline. Arrives last, after the sweep.\"),\n  align: z\n    .enum([\"left\", \"center\"])\n    .default(\"left\")\n    .describe(\"Composition. 'left' with the light mass off-centre is the stronger frame; 'center' for symmetrical lockups.\"),\n  intensity: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.62)\n    .describe(\"How present the light field is. Below 0.4 reads as near-black; above 0.8 starts to compete with the type.\"),\n  sweep: z\n    .boolean()\n    .default(true)\n    .describe(\"Run a specular highlight across the glyphs once they have resolved.\"),\n  brand: z\n    .string()\n    .default(\"signal\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof auroraStatementSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\n/* ------------------------------------------------------------------- light */\n\n/**\n * One lobe of the light field.\n *\n * Drifts on two sines of different periods so no two lobes ever share a path —\n * the thing that makes procedural light read as canned is a visible loop. The\n * lobe carries no hard edge of its own: the whole field is blurred as a group by\n * the parent, which is cheaper than blurring each lobe and, more importantly,\n * lets the lobes bleed into each other the way real light does.\n */\nconst Lobe: React.FC<{\n  colour: string;\n  size: number;\n  x: number;\n  y: number;\n  drift: number;\n  phase: number;\n  frame: number;\n  fps: number;\n  opacity: number;\n}> = ({ colour, size, x, y, drift, phase, frame, fps, opacity }) => {\n  const t = frame / fps;\n  const dx = Math.sin(t * 0.34 + phase) * drift;\n  const dy = Math.cos(t * 0.21 + phase * 1.7) * drift * 0.55;\n  // Breathing scale keeps the field alive during the hold, where nothing else moves.\n  const breathe = 1 + Math.sin(t * 0.29 + phase * 2.3) * 0.07;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x - size / 2,\n        top: y - size / 2,\n        width: size,\n        height: size,\n        transform: `translate(${dx}px, ${dy}px) scale(${breathe})`,\n        opacity,\n        // Many stops rather than two: a two-stop radial gradient bands visibly\n        // across 1000px of dark frame, and banding is the tell of cheap CG.\n        background: `radial-gradient(circle at 50% 50%, ${alpha(colour, 0.95)} 0%, ${alpha(\n          colour,\n          0.72,\n        )} 18%, ${alpha(colour, 0.44)} 34%, ${alpha(colour, 0.22)} 50%, ${alpha(\n          colour,\n          0.09,\n        )} 64%, ${alpha(colour, 0.03)} 78%, ${alpha(colour, 0)} 92%)`,\n      }}\n    />\n  );\n};\n\n/* -------------------------------------------------------------------- type */\n\n/**\n * One glyph, resolving out of defocus.\n *\n * Blur-to-focus rather than a slide or a fade, because it is what a lens\n * actually does — and because it survives being scaled: at 4K a fade is a fade,\n * but focus carries a sense of a physical camera finding the subject. The glyph\n * also rises and contracts very slightly, so the motion has a direction without\n * announcing itself.\n */\nconst Glyph: React.FC<{\n  char: string;\n  index: number;\n  size: number;\n  colour: string;\n  font: string;\n}> = ({ char, index, size, colour, font }) => {\n  const stage = useStaggeredStage(BUDGET, index, 1.15);\n  const e = stage.enter;\n\n  const blur = interpolate(e, [0, 1], [size * 0.09, 0], { extrapolateRight: \"clamp\" });\n  const y = interpolate(e, [0, 1], [size * 0.13, 0], { extrapolateRight: \"clamp\" });\n  const scale = interpolate(e, [0, 1], [1.045, 1]);\n\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        whiteSpace: \"pre\",\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 700,\n        lineHeight: 1.0,\n        // Display type at this size needs negative tracking or it reads as a\n        // default; -0.035em is where large Archivo stops looking loose.\n        letterSpacing: size * -0.035,\n        color: colour,\n        opacity: e,\n        filter: `blur(${blur}px)`,\n        transform: `translateY(${y}px) scale(${scale})`,\n        transformOrigin: \"50% 60%\",\n      }}\n    >\n      {char}\n    </span>\n  );\n};\n\n/* ------------------------------------------------------------------- grain */\n\n/**\n * Film grain, as a real noise field rather than an image.\n *\n * `feTurbulence` is generated at render time, so this costs no asset and stays\n * deterministic. The seed steps every second frame, which lands the grain on a\n * 15fps cadence — the rate at which grain reads as film rather than as video\n * noise. It is also what finally kills the last of the gradient banding.\n */\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.82\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/* --------------------------------------------------------------- component */\n\nconst AuroraStatement: React.FC<Props> = ({\n  headline,\n  eyebrow,\n  kicker,\n  align,\n  intensity,\n  sweep,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const chars = [...headline];\n  const size = width * (chars.length > 18 ? 0.058 : 0.079);\n\n  /**\n   * The camera.\n   *\n   * A locked-off frame is the single clearest signal that a shot was assembled\n   * rather than photographed, so the whole composition sits on a slow push that\n   * never stops — 4.5% over five seconds, which is below the threshold where a\n   * viewer notices it as movement and above the threshold where the frame feels\n   * dead.\n   */\n  const push = interpolate(stage.progress, [0, 1], [1, 1.045]);\n  const rise = interpolate(stage.progress, [0, 1], [0, -height * 0.012]);\n\n  // The light field lands before the type and settles behind it.\n  const fieldIn = interpolate(frame, [0, BUDGET.enter * 1.15], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  /**\n   * The specular sweep.\n   *\n   * Deliberately after the glyphs have resolved, and only once: a highlight that\n   * loops turns a hero frame into a banner ad. The band is wide and low-contrast\n   * — it is light moving over the letterforms, not a shine effect on top of them.\n   */\n  const sweepStart = BUDGET.enter + chars.length * 1.15 + 6;\n  const sweepPos = interpolate(frame, [sweepStart, sweepStart + 26], [-115, 215], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const sweepLive = sweep && frame > sweepStart && frame < sweepStart + 30;\n\n  const kickerStage = useStage(BUDGET);\n  const kickerIn = interpolate(kickerStage.frame, [sweepStart + 4, sweepStart + 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const left = align === \"left\";\n  const pad = width * 0.085;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* ---------------------------------------------------- the light field */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: `-${height * 0.25}px`,\n          opacity: fieldIn * stage.presence,\n          // The group blur is what turns four coloured circles into light.\n          filter: `blur(${Math.round(width * 0.055)}px)`,\n          mixBlendMode: \"screen\",\n        }}\n      >\n        <Lobe\n          colour={t.accent}\n          size={width * 0.78}\n          x={left ? width * 0.74 : width * 0.5}\n          y={height * 0.34}\n          drift={width * 0.03}\n          phase={0}\n          frame={frame}\n          fps={fps}\n          opacity={0.5 + intensity * 0.5}\n        />\n        <Lobe\n          colour={t.accentAlt}\n          size={width * 0.58}\n          x={left ? width * 0.9 : width * 0.78}\n          y={height * 0.72}\n          drift={width * 0.026}\n          phase={2.1}\n          frame={frame}\n          fps={fps}\n          opacity={0.3 + intensity * 0.42}\n        />\n        <Lobe\n          colour={t.accent}\n          size={width * 0.46}\n          x={left ? width * 0.5 : width * 0.24}\n          y={height * 0.86}\n          drift={width * 0.02}\n          phase={4.4}\n          frame={frame}\n          fps={fps}\n          opacity={0.18 + intensity * 0.3}\n        />\n        <Lobe\n          colour={t.ink}\n          size={width * 0.3}\n          x={left ? width * 0.82 : width * 0.6}\n          y={height * 0.18}\n          drift={width * 0.014}\n          phase={5.9}\n          frame={frame}\n          fps={fps}\n          opacity={0.08 + intensity * 0.14}\n        />\n      </div>\n\n      {/* ------------------------------------------------------------ content */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"center\",\n          alignItems: left ? \"flex-start\" : \"center\",\n          padding: `0 ${pad}px`,\n          transform: `scale(${push}) translateY(${rise}px)`,\n          transformOrigin: left ? \"30% 50%\" : \"50% 50%\",\n        }}\n      >\n        {eyebrow ? (\n          <Eyebrow label={eyebrow} colour={t.accent} font={t.fontMono} size={size * 0.145} />\n        ) : null}\n\n        <div\n          style={{\n            position: \"relative\",\n            maxWidth: left ? \"72%\" : \"84%\",\n            textAlign: left ? \"left\" : \"center\",\n          }}\n        >\n          <div>\n            {chars.map((char, i) => (\n              <Glyph\n                key={`${char}-${i}`}\n                char={char}\n                index={i}\n                size={size}\n                colour={t.ink}\n                font={t.fontDisplay}\n              />\n            ))}\n          </div>\n\n          {/* The sweep is a second copy of the headline, clipped to its own\n              glyphs and blended in — so the highlight rides the letterforms\n              instead of sitting in a box over them. */}\n          {sweepLive ? (\n            <div\n              aria-hidden\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                pointerEvents: \"none\",\n                mixBlendMode: \"overlay\",\n              }}\n            >\n              <div\n                style={{\n                  fontFamily: t.fontDisplay,\n                  fontSize: size,\n                  fontWeight: 700,\n                  lineHeight: 1.0,\n                  letterSpacing: size * -0.035,\n                  color: \"transparent\",\n                  backgroundImage: `linear-gradient(96deg, ${alpha(t.ink, 0)} 34%, ${alpha(\n                    \"#fff\",\n                    0.5,\n                  )} 47%, ${alpha(\"#fff\", 0.85)} 50%, ${alpha(\"#fff\", 0.5)} 53%, ${alpha(\n                    t.ink,\n                    0,\n                  )} 66%)`,\n                  backgroundSize: \"260% 100%\",\n                  backgroundPosition: `${sweepPos}% 0`,\n                  WebkitBackgroundClip: \"text\",\n                  backgroundClip: \"text\",\n                }}\n              >\n                {headline}\n              </div>\n            </div>\n          ) : null}\n        </div>\n\n        {kicker ? (\n          <div\n            style={{\n              marginTop: size * 0.42,\n              maxWidth: left ? \"48%\" : \"58%\",\n              fontFamily: t.fontBody,\n              fontSize: size * 0.19,\n              fontWeight: 400,\n              lineHeight: 1.5,\n              letterSpacing: size * -0.002,\n              color: t.inkMuted,\n              textAlign: left ? \"left\" : \"center\",\n              opacity: kickerIn * stage.presence,\n              transform: `translateY(${interpolate(kickerIn, [0, 1], [10, 0])}px)`,\n            }}\n          >\n            {kicker}\n          </div>\n        ) : null}\n      </div>\n\n      {/* -------------------------------------------------------------- grade */}\n      {/* Vignette, then grain, in that order: grain over the vignette keeps the\n          darkened corners from banding, which is exactly where banding shows. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(125% 95% at ${left ? 62 : 50}% 44%, ${alpha(\n            \"#000\",\n            0,\n          )} 26%, ${alpha(\"#000\", 0.1)} 52%, ${alpha(\"#000\", 0.28)} 74%, ${alpha(\n            \"#000\",\n            0.52,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"aurora-statement-grain\" frame={frame} opacity={0.085} />\n    </AbsoluteFill>\n  );\n};\n\nconst Eyebrow: React.FC<{ label: string; colour: string; font: string; size: number }> = ({\n  label,\n  colour,\n  font,\n  size,\n}) => {\n  const stage = useStage(BUDGET);\n  const e = interpolate(stage.frame, [0, 16], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.8,\n        marginBottom: size * 2.1,\n        opacity: e * stage.presence,\n      }}\n    >\n      <div\n        style={{\n          height: Math.max(1, size * 0.07),\n          width: interpolate(e, [0, 1], [0, size * 2.4]),\n          backgroundColor: colour,\n        }}\n      />\n      <span\n        style={{\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 500,\n          letterSpacing: size * 0.19,\n          textTransform: \"uppercase\",\n          color: colour,\n          transform: `translateX(${interpolate(e, [0, 1], [-6, 0])}px)`,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"aurora-statement\",\n  name: \"Aurora Statement\",\n  category: \"typography\",\n  summary:\n    \"A headline resolves out of defocus over a drifting volumetric light field, with one specular pass across the glyphs\",\n  tags: [\"hero\", \"headline\", \"light field\", \"focus pull\", \"specular\", \"premium\", \"grain\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: auroraStatementSchema,\n  defaults: {\n    headline: \"Built to be rendered\",\n    eyebrow: \"Motionstash\",\n    kicker: \"Components that compose in time, not in space.\",\n    align: \"left\",\n    intensity: 0.62,\n    sweep: true,\n    brand: \"signal\",\n  },\n  component: AuroraStatement,\n  file: \"src/registry/items/aurora-statement.tsx\",\n  origin: {\n    prompt:\n      \"An opening hero frame for a product film. A large headline should resolve out of defocus letter by letter, as if a camera were finding focus, over a slow-drifting field of coloured light. Once the words are sharp, run a single soft specular highlight across them. Add a real vignette and film grain, and keep the whole frame on a slow camera push so it never feels locked off.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An opening hero frame for a product film. A large headline should resolve out of defocus letter by letter, as if a camera were finding focus, over a slow-drifting field of coloured light. Once the words are sharp, run a single soft specular highlight across them. Add a real vignette and film grain, and keep the whole frame on a slow camera push so it never feels locked off.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6876,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "The statement. Three to six words; this is a hero frame, not a paragraph.",
          "required": true,
          "defaultValue": "Built to be rendered",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small tracked label above the headline. Omit for a purer frame.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "One quiet line under the headline. Arrives last, after the sweep.",
          "required": false,
          "defaultValue": "Components that compose in time, not in space.",
          "options": null
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "description": "Composition. 'left' with the light mass off-centre is the stronger frame; 'center' for symmetrical lockups.",
          "required": false,
          "defaultValue": "left",
          "options": [
            "left",
            "center"
          ]
        },
        {
          "name": "intensity",
          "type": "number",
          "description": "How present the light field is. Below 0.4 reads as near-black; above 0.8 starts to compete with the type.",
          "required": false,
          "defaultValue": "0.62",
          "options": null
        },
        {
          "name": "sweep",
          "type": "boolean",
          "description": "Run a specular highlight across the glyphs once they have resolved.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "signal",
          "options": null
        }
      ]
    },
    {
      "id": "badge-row",
      "name": "Badge Row",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "Credibility chips set into a full-bleed hairline, springing up from 0.92 in a tight stagger with one filled accent pill landing last",
      "tags": [
        "badges",
        "chips",
        "pills",
        "credibility",
        "lower third",
        "stagger",
        "spring"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 60,
      "durationSeconds": 2,
      "budget": {
        "enter": 12,
        "hold": 36,
        "exit": 12
      },
      "file": "src/registry/items/badge-row.tsx",
      "source": "import React from \"react\";\nimport { makeStar } from \"@remotion/shapes\";\nimport { AbsoluteFill, interpolate, spring, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nconst MARKS = [\"none\", \"dot\", \"check\", \"star\", \"shield\", \"bolt\"] as const;\ntype MarkKind = (typeof MARKS)[number];\n\nexport const badgeChipSchema = z.object({\n  label: z\n    .string()\n    .describe(\n      \"The chip text. One to three short words: it is set in mono and uppercased, so 'Product Hunt #1' renders as 'PRODUCT HUNT #1'.\",\n    ),\n  mark: z\n    .enum(MARKS)\n    .default(\"none\")\n    .describe(\n      \"Leading glyph, drawn inline as a path — no icon font. 'dot' is a status pip, 'check' reads as verified or compliant, 'star' as an award, 'shield' as security, 'bolt' as speed or a fresh release. 'none' leaves a bare label.\",\n    ),\n});\n\nexport const badgeRowSchema = z.object({\n  badges: z\n    .array(badgeChipSchema)\n    .min(1)\n    .max(6)\n    .describe(\n      \"The chips, in reading order. Three to five is the sweet spot; past that the row scales itself down to stay inside the frame.\",\n    ),\n  accentIndex: z\n    .number()\n    .int()\n    .min(0)\n    .max(5)\n    .optional()\n    .describe(\n      \"Which chip (0-based) gets the filled accent treatment and lands last with a slight overshoot. Exactly one chip carries the emphasis — omit for an all-outline row.\",\n    ),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\n      \"Small letterspaced mono label hung off the row's left shoulder, e.g. 'Now generally available'. Two to four words; omit for a barer strip.\",\n    ),\n  rule: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Draw a hairline across the full frame width on the row's centre line. The chips knock a gutter out of it, so the row reads as sitting on the line rather than floating over it.\",\n    ),\n  shape: z\n    .enum([\"pill\", \"cut\"])\n    .default(\"pill\")\n    .describe(\n      \"'pill' rounds the chips to a full half-height radius. 'cut' uses the brand's own corner radius instead, which is what square-cornered brands like Press want.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof badgeRowSchema>;\ntype Badge = z.infer<typeof badgeChipSchema>;\n\n/* ------------------------------------------------------------------ metrics */\n\ntype ChipMetrics = {\n  height: number;\n  font: number;\n  tracking: number;\n  padX: number;\n  /** Floor on chip width, so a three-letter label is a chip and not a nub. */\n  minWidth: number;\n  markSize: number;\n  markGap: number;\n  gap: number;\n  border: number;\n  /** Width of the background gutter each chip knocks out of the rule. */\n  gutter: number;\n};\n\n/**\n * Every dimension in the row is a ratio of one number: the chip height. That\n * makes the whole strip linear in that number, which is what lets the fit\n * calculation below scale the row by a single factor and land exactly.\n */\nconst chipMetrics = (height: number): ChipMetrics => {\n  const font = height * 0.32;\n  return {\n    height,\n    font,\n    tracking: font * 0.13,\n    padX: height * 0.44,\n    minWidth: height * 2.5,\n    markSize: height * 0.3,\n    markGap: height * 0.24,\n    gap: height * 0.32,\n    border: Math.max(1, height * 0.017),\n    gutter: Math.max(2, height * 0.1),\n  };\n};\n\n/**\n * The row's width, computed rather than measured.\n *\n * Mono advances every glyph by the same amount — 0.6em, plus the tracking we\n * add — so a row of uppercase mono labels has an arithmetic width. No layout\n * pass, no measurement, and the same answer during preview as during render,\n * which is the whole reason the labels are mono in the first place. The 0.62\n * leaves a little slack for the frame where the webfont has not landed yet.\n */\nconst estimateRowWidth = (badges: Badge[], m: ChipMetrics) => {\n  const advance = m.font * 0.62 + m.tracking;\n  const chips = badges.reduce((acc, b) => {\n    const ink = b.label.length * advance + (b.mark === \"none\" ? 0 : m.markSize + m.markGap);\n    // The chips are border-box, so the two hairlines eat into the padding.\n    return acc + Math.max(m.minWidth, m.border * 2 + m.padX * 2 + ink);\n  }, 0);\n  return chips + m.gap * Math.max(0, badges.length - 1);\n};\n\n/* -------------------------------------------------------------- component */\n\n/**\n * Badge Row — the strip of credibility chips that closes a product video.\n *\n * The composition is one hairline and one row. The rule runs the full width of\n * the frame on the vertical centre line and the chips sit *on* it, each one\n * knocking a small gutter out of the line with a background-coloured ring, so\n * the strip reads as set into the frame rather than dropped on top of it. The\n * kicker is hung off the row's left shoulder and taken out of flow, which\n * keeps the row itself dead centre and stops the frame reading as a stack of\n * centred things.\n *\n * Motion is one gesture: the rule draws out from the centre while the chips\n * scale up from 0.92 on a spring in a tight left-to-right stagger. The one\n * filled chip is pulled to the back of that queue and lands on `SPRINGS.pop`,\n * about two percent past its size — the single accent beat in the component.\n */\nconst BadgeRow: React.FC<Props> = ({ badges, accentIndex, kicker, rule, shape, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.beat);\n\n  // Chips are glanced at, not read, so they size off the frame's short edge —\n  // 1080 in every aspect the registry ships — and a chip is then the same\n  // physical size in a 16:9 cut as in a 9:16 one.\n  const base = chipMetrics(Math.min(width, height) * 0.075);\n\n  // Text stops well short of the frame edge. If the row would break that\n  // margin, shrink the single governing number until it doesn't: everything\n  // downstream is linear in it, so one factor fits the whole strip.\n  const safeWidth = width * 0.84;\n  const fit = Math.min(1, safeWidth / estimateRowWidth(badges, base));\n  const m = chipMetrics(base.height * fit);\n\n  const accentAt = accentIndex !== undefined && accentIndex < badges.length ? accentIndex : -1;\n\n  // The accent chip is pulled to the back of the stagger queue; every other\n  // chip keeps reading order and closes the gap it left. Visual order and\n  // temporal order are deliberately different — the row builds left to right,\n  // then the badge lands.\n  const order = badges.map((_, i) => {\n    if (i === accentAt) return badges.length - 1;\n    return accentAt >= 0 && i > accentAt ? i - 1 : i;\n  });\n\n  // Two frames between chips is ~67ms, the tight end of the readable range.\n  // Longer rows compress so the last chip still lands near the enter window.\n  const perItem = badges.length > 4 ? 8 / badges.length : 2;\n\n  const radius = shape === \"pill\" ? m.height / 2 : Math.min(m.height / 2, t.radius);\n\n  const kickerSize = m.height * 0.29;\n  const kickerGap = m.height * 0.58;\n\n  // Optical centring. The kicker hangs above the row and carries real mass, so\n  // the whole band drops by half of it: that sits the *pair* on the frame's\n  // centre line instead of sitting the row there and letting the label tip the\n  // balance upward. The rule and the row read off the same number, so the line\n  // always runs through the middle of the chips.\n  const kickerBlock = kicker ? kickerSize * 1.15 + kickerGap : 0;\n  const bandY = height / 2 + kickerBlock / 2;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      {rule ? <RuleLine centreY={bandY} weight={m.border} color={t.rule} /> : null}\n\n      <AbsoluteFill\n        style={{ alignItems: \"center\", justifyContent: \"center\", paddingTop: kickerBlock }}\n      >\n        <div\n          style={{\n            position: \"relative\",\n            opacity: stage.presence,\n            // The row settles back a hair as it leaves, so the exit has weight\n            // instead of being a straight dissolve.\n            transform: `scale(${1 - stage.exit * 0.02})`,\n          }}\n        >\n          {kicker ? (\n            <Kicker\n              label={kicker}\n              color={t.inkMuted}\n              font={t.fontMono}\n              size={kickerSize}\n              gap={kickerGap}\n            />\n          ) : null}\n\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: m.gap }}>\n            {badges.map((badge, i) => (\n              <Chip\n                key={`${badge.label}-${i}`}\n                label={badge.label}\n                mark={badge.mark}\n                order={order[i]}\n                perItem={perItem}\n                filled={i === accentAt}\n                radius={radius}\n                knockout={rule ? t.bg : null}\n                m={m}\n                t={t}\n              />\n            ))}\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * The hairline the row sits on. Full bleed, and it draws out from the centre\n * in both directions — the same place the row is anchored, so the line reads\n * as the row's own ground rather than a separate event.\n */\nconst RuleLine: React.FC<{ centreY: number; weight: number; color: string }> = ({\n  centreY,\n  weight,\n  color,\n}) => {\n  const stage = useStage(BUDGETS.beat);\n  const run = interpolate(stage.frame, [0, 11], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        right: 0,\n        top: centreY - weight / 2,\n        height: weight,\n        backgroundColor: color,\n        transform: `scaleX(${run})`,\n        transformOrigin: \"center\",\n        opacity: stage.presence,\n      }}\n    />\n  );\n};\n\n/**\n * The kicker. Absolutely positioned above the row and therefore out of flow,\n * which is the point: however long the label runs, it can never push the row\n * off the frame's centre line, and it sets flush with the row's left edge.\n */\nconst Kicker: React.FC<{\n  label: string;\n  color: string;\n  font: string;\n  size: number;\n  gap: number;\n}> = ({ label, color, font, size, gap }) => {\n  const stage = useStage(BUDGETS.beat);\n  // Rises out from behind the row it introduces.\n  const y = interpolate(stage.enter, [0, 1], [size * 0.55, 0]);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        bottom: \"100%\",\n        left: 0,\n        marginBottom: gap,\n        whiteSpace: \"nowrap\",\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 600,\n        letterSpacing: size * 0.2,\n        textTransform: \"uppercase\",\n        color,\n        transform: `translateY(${y}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      {label}\n    </div>\n  );\n};\n\n/**\n * One chip. A hairline pill, a drawn mark, a mono label — or, for the one\n * designated accent chip, the same geometry filled solid.\n *\n * The `boxShadow` is not a shadow: it is a hard ring of canvas colour, which\n * is how the chip cuts its own gutter out of the rule behind it. Doing it in\n * the box model rather than with a mask means it survives the entrance scale\n * for free.\n */\nconst Chip: React.FC<{\n  label: string;\n  mark: MarkKind;\n  order: number;\n  perItem: number;\n  filled: boolean;\n  radius: number;\n  knockout: string | null;\n  m: ChipMetrics;\n  t: BrandTokens;\n}> = ({ label, mark, order, perItem, filled, radius, knockout, m, t }) => {\n  const stage = useStaggeredStage(BUDGETS.beat, order, perItem);\n\n  // Scale is a spring, not a curve: these are small physical objects. `snap`\n  // brings the outlined chips in with no visible bounce; the filled chip gets\n  // `pop`, whose ~2% overshoot is the one accent gesture in the composition.\n  const rise = spring({\n    frame: stage.frame,\n    fps: stage.fps,\n    config: filled ? SPRINGS.pop : SPRINGS.snap,\n    durationInFrames: BUDGETS.beat.enter,\n  });\n\n  // Opacity is done by 55% of the enter curve, so the chip is solid ink while\n  // the spring is still settling. A half-transparent pill reads as a mistake.\n  const opacity = interpolate(stage.enter, [0, 0.55], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        flexShrink: 0,\n        boxSizing: \"border-box\",\n        height: m.height,\n        minWidth: m.minWidth,\n        padding: `0 ${m.padX}px`,\n        gap: m.markGap,\n        borderRadius: radius,\n        backgroundColor: filled ? t.accent : t.surface,\n        border: `${m.border}px solid ${filled ? t.accent : t.rule}`,\n        boxShadow: knockout ? `0 0 0 ${m.gutter}px ${knockout}` : undefined,\n        transform: `scale(${0.92 + rise * 0.08})`,\n        opacity,\n      }}\n    >\n      <Mark kind={mark} size={m.markSize} color={filled ? t.accentInk : t.accent} />\n      <span\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: m.font,\n          fontWeight: 600,\n          lineHeight: 1,\n          letterSpacing: m.tracking,\n          textTransform: \"uppercase\",\n          whiteSpace: \"nowrap\",\n          color: filled ? t.accentInk : t.ink,\n          // Tracking is added after the last glyph too, which shoves centred\n          // text left by one step; pulling it back re-centres the label. The\n          // translate is cap-height centring — uppercase ink sits high in a\n          // line box because the descender space below it is empty.\n          marginRight: -m.tracking,\n          transform: `translateY(${m.font * 0.05}px)`,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\n/* ----------------------------------------------------------------- glyphs */\n\n/** A real five-pointed star path, so the award mark is geometry, not a glyph. */\nconst STAR = makeStar({ points: 5, innerRadius: 5.4, outerRadius: 12 });\n\n/**\n * The drawn marks. Each declares its own tight viewBox and a height factor,\n * because equal boxes do not read as equal marks: a check is wide and short, a\n * shield is tall and narrow, and a star's bounding box is filled corner to\n * corner. The factors are tuned by eye so the ink of every mark carries about\n * the same weight beside the label, whatever size the frame scales it to.\n */\nconst GLYPHS: Record<\n  Exclude<MarkKind, \"none\" | \"dot\">,\n  { vw: number; vh: number; d: string; heightFactor: number; solid: boolean }\n> = {\n  check: {\n    vw: 24,\n    vh: 17,\n    d: \"M1.8 9.4 L8.6 15.6 L22.2 1.4\",\n    heightFactor: 0.66,\n    solid: false,\n  },\n  star: {\n    vw: STAR.width,\n    vh: STAR.height,\n    d: STAR.path,\n    heightFactor: 0.88,\n    solid: true,\n  },\n  shield: {\n    vw: 20,\n    vh: 24,\n    d: \"M10 1.6 L18.2 4.8 L18.2 12.2 C18.2 17.6 10 22.4 10 22.4 C10 22.4 1.8 17.6 1.8 12.2 L1.8 4.8 Z\",\n    heightFactor: 0.96,\n    solid: false,\n  },\n  bolt: {\n    vw: 24,\n    vh: 24,\n    d: \"M13.4 1.4 L3.4 13.8 L11.4 13.8 L10.6 22.6 L20.6 10.2 L12.6 10.2 Z\",\n    heightFactor: 0.94,\n    solid: true,\n  },\n};\n\n/** Rendered stroke weight of an outlined mark, as a fraction of `size`. */\nconst MARK_STROKE = 0.11;\n\nconst Mark: React.FC<{ kind: MarkKind; size: number; color: string }> = ({ kind, size, color }) => {\n  if (kind === \"none\") return null;\n\n  if (kind === \"dot\") {\n    return (\n      <div\n        style={{\n          flexShrink: 0,\n          width: size * 0.42,\n          height: size * 0.42,\n          borderRadius: size,\n          backgroundColor: color,\n        }}\n      />\n    );\n  }\n\n  const g = GLYPHS[kind];\n  const h = size * g.heightFactor;\n  const w = h * (g.vw / g.vh);\n  // Stroke is declared in viewBox units, so it has to be divided back out by\n  // this glyph's own scale to land at the same rendered weight as every other.\n  const strokeWidth = (size * MARK_STROKE * g.vh) / h;\n\n  return (\n    <svg\n      width={w}\n      height={h}\n      viewBox={`0 0 ${g.vw} ${g.vh}`}\n      style={{ display: \"block\", flexShrink: 0, overflow: \"visible\" }}\n    >\n      <path\n        d={g.d}\n        fill={g.solid ? color : \"none\"}\n        stroke={g.solid ? \"none\" : color}\n        strokeWidth={g.solid ? undefined : strokeWidth}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"badge-row\",\n  name: \"Badge Row\",\n  category: \"titles\",\n  summary:\n    \"Credibility chips set into a full-bleed hairline, springing up from 0.92 in a tight stagger with one filled accent pill landing last\",\n  tags: [\"badges\", \"chips\", \"pills\", \"credibility\", \"lower third\", \"stagger\", \"spring\"],\n  budget: BUDGETS.beat,\n  aspect: \"16:9\",\n  schema: badgeRowSchema,\n  defaults: {\n    badges: [\n      { label: \"SOC 2 Type II\", mark: \"shield\" },\n      { label: \"Product Hunt #1\", mark: \"star\" },\n      { label: \"GDPR ready\", mark: \"check\" },\n      { label: \"v2.4\", mark: \"none\" },\n    ],\n    accentIndex: 1,\n    kicker: \"Now generally available\",\n    rule: true,\n    shape: \"pill\",\n    brand: \"slate\",\n  },\n  component: BadgeRow,\n  file: \"src/registry/items/badge-row.tsx\",\n  origin: {\n    prompt:\n      \"A row of small credibility badges to close a product video — SOC 2, a Product Hunt win, GDPR, the version number. Hairline pills with mono uppercase labels and little drawn icons, sitting on a hairline that runs the full width of the frame and that each pill knocks a gap out of. Tight stagger in, scaling up slightly, and one pill filled in the accent colour that arrives last with a small overshoot. Small letterspaced label above the left end of the row.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A row of small credibility badges to close a product video — SOC 2, a Product Hunt win, GDPR, the version number. Hairline pills with mono uppercase labels and little drawn icons, sitting on a hairline that runs the full width of the frame and that each pill knocks a gap out of. Tight stagger in, scaling up slightly, and one pill filled in the accent colour that arrives last with a small overshoot. Small letterspaced label above the left end of the row.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2702,
      "props": [
        {
          "name": "badges",
          "type": "array",
          "description": "The chips, in reading order. Three to five is the sweet spot; past that the row scales itself down to stay inside the frame.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "accentIndex",
          "type": "number",
          "description": "Which chip (0-based) gets the filled accent treatment and lands last with a slight overshoot. Exactly one chip carries the emphasis — omit for an all-outline row.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "Small letterspaced mono label hung off the row's left shoulder, e.g. 'Now generally available'. Two to four words; omit for a barer strip.",
          "required": false,
          "defaultValue": "Now generally available",
          "options": null
        },
        {
          "name": "rule",
          "type": "boolean",
          "description": "Draw a hairline across the full frame width on the row's centre line. The chips knock a gutter out of it, so the row reads as sitting on the line rather than floating over it.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "shape",
          "type": "\"pill\" | \"cut\"",
          "description": "'pill' rounds the chips to a full half-height radius. 'cut' uses the brand's own corner radius instead, which is what square-cornered brands like Press want.",
          "required": false,
          "defaultValue": "pill",
          "options": [
            "pill",
            "cut"
          ]
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "bar-chart-reveal",
      "name": "Bar Chart Reveal",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Gridlines draw on as hairlines, bars climb from the baseline in reading order, and each value counts up as its bar lands",
      "tags": [
        "bar chart",
        "chart reveal",
        "gridlines",
        "counter",
        "stagger",
        "tabular figures"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/bar-chart-reveal.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/* ------------------------------------------------------------------ schema */\n\nconst barSchema = z.object({\n  label: z\n    .string()\n    .describe(\n      \"Category label under the bar. Short wins: 'Mar', 'Q3', 'EU'. Anything longer than the column is truncated rather than wrapped, because a two-line axis breaks the baseline.\",\n    ),\n  value: z\n    .number()\n    .min(0)\n    .describe(\n      \"The measured value, in whatever unit `valuePrefix`/`valueSuffix` declare. Bars grow up from a zero baseline, so values are non-negative; the y domain is rounded up to a round number above the largest of them.\",\n    ),\n});\n\nexport const barChartRevealSchema = z.object({\n  data: z\n    .array(barSchema)\n    .min(2)\n    .max(12)\n    .describe(\n      \"The series, in reading order — the reveal staggers left to right, so order the array the way you want the chart read. Five to eight bars is the sweet spot; twelve is the ceiling before the columns thin out.\",\n    ),\n  title: z\n    .string()\n    .optional()\n    .describe(\n      \"Headline above the plot, set in the display face. Say what is measured, not what the shape does ('Scenes rendered per month', not 'Growth'). One line reads best.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono label in the accent colour above the title. Omit for a barer chart.\"),\n  highlight: z\n    .number()\n    .int()\n    .min(0)\n    .optional()\n    .describe(\n      \"Zero-based index of the one bar carried in the accent colour; every other bar is muted. Omit to highlight the largest value automatically. This is the argument of the chart, so pick the bar the scene is about.\",\n    ),\n  valuePrefix: z\n    .string()\n    .optional()\n    .describe(\"Set immediately before every figure, on the bars and the axis alike — a currency mark such as '$' or '€'.\"),\n  valueSuffix: z\n    .string()\n    .optional()\n    .describe(\n      \"Set immediately after every figure — '%', 'k', 'ms'. Rendered exactly as typed, so include a leading thin space yourself if the unit needs air.\",\n    ),\n  compact: z\n    .boolean()\n    .default(false)\n    .describe(\n      \"Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Turn it on once the figures pass four digits.\",\n    ),\n  ticks: z\n    .number()\n    .int()\n    .min(2)\n    .max(6)\n    .default(4)\n    .describe(\n      \"Number of gridlines above the baseline. It also sets the y domain: the top line is always a round number, so 4 over a peak of 137 gives 0/40/80/120/160.\",\n    ),\n  yAxis: z\n    .boolean()\n    .default(true)\n    .describe(\"Show the y-axis tick figures at the left, in mono with tabular figures. Off gives a barer chart that leans on the value labels.\"),\n  valueLabels: z\n    .boolean()\n    .default(true)\n    .describe(\"Count each bar's value up above its column as the bar lands. Off when the axis alone should carry the numbers.\"),\n  barWidth: z\n    .number()\n    .min(0.3)\n    .max(0.95)\n    .default(0.62)\n    .describe(\n      \"Bar thickness as a fraction of its column slot. 0.62 is the editorial default; above 0.8 the chart reads as a block, below 0.4 as a set of ticks.\",\n    ),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono caption under a hairline at the foot of the frame: the source, the period, the qualifier. One short sentence.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof barChartRevealSchema>;\ntype Datum = z.infer<typeof barSchema>;\n\n/* ------------------------------------------------------------------ timing */\n\nconst BUDGET = BUDGETS.scene;\n\n/*\n * Three beats, in frames from this component's own frame 0. They are declared\n * rather than derived so the reveal reads in the same order the eye does:\n * the frame gets its ground (baseline, then gridlines climbing), the bars grow\n * out of that ground, and only then do the numbers arrive. The last counter\n * lands around frame 75 of a 150-frame budget, i.e. well inside the hold —\n * which is the contract: presence is spent in `enter`, but a counter is the\n * canonical piece of internal motion the hold is allowed to carry.\n */\nconst AXIS_START = 3;\n/** Frames one rule takes to sweep across the plot. */\nconst AXIS_DRAW = 15;\n/** The first gridline waits for the baseline to commit before it follows. */\nconst GRID_LAG = 5;\n/** Each gridline lags the one below it, so the grid builds upward. */\nconst GRID_STAGGER = 2;\nconst BARS_START = 19;\n/** Frames one bar takes to climb. */\nconst BAR_GROW = 22;\n/** Fraction of a bar's climb that passes before its counter starts. */\nconst COUNT_LEAD = 0.42;\n/** Frames the counter runs on past its own bar, so the figure settles last. */\nconst COUNT_TAIL = 7;\nconst CAT_DELAY = 5;\nconst CAT_FADE = 11;\n\n/**\n * Per-bar delay. Held inside the readable stagger range (roughly 80–170ms at\n * 30fps) and tightened as the series grows, so twelve bars still read as one\n * gesture sweeping across the plot rather than twelve separate events.\n */\nconst barStagger = (count: number) => Math.min(5, Math.max(2.4, 26 / count));\n\n/* ------------------------------------------------------------------ domain */\n\n/**\n * The ladder of step sizes a human would choose for an axis. Finer than the\n * usual 1/2/5 ladder: the coarse version turns a peak of 137 into a domain of\n * 200, which wastes a third of the plot and makes every bar look smaller than\n * it is.\n */\nconst NICE_STEPS = [1, 1.5, 2, 2.5, 3, 4, 5, 7.5, 10];\n\nconst niceStep = (rough: number): number => {\n  if (!(rough > 0)) return 1;\n  const magnitude = 10 ** Math.floor(Math.log10(rough));\n  const normalised = rough / magnitude;\n  const pick = NICE_STEPS.find((s) => normalised <= s + 1e-9) ?? 10;\n  return pick * magnitude;\n};\n\n/**\n * A hair of headroom before rounding, so a peak that happens to sit exactly on\n * a round number still gets a domain above it. A bar pressed flat against the\n * top gridline reads as clipped data.\n */\nconst HEADROOM = 1.04;\n\n/** Round step, round top line, and the top line always above the peak. */\nconst buildDomain = (values: number[], ticks: number): { max: number; step: number } => {\n  const peak = Math.max(0, ...values);\n  if (peak <= 0) return { max: ticks, step: 1 };\n  const step = niceStep((peak * HEADROOM) / ticks);\n  return { max: step * ticks, step };\n};\n\n/* --------------------------------------------------------------- numbers */\n\n/** Typographic thousands separator. A comma is punctuation; this is spacing. */\nconst THIN_SPACE = \"\\u2009\";\n\n/**\n * Decimal places implied by a number — 0 for integers, 1 for tenths, 2 beyond.\n * Derived rather than configured so the axis and the bars agree, and so a\n * series of whole numbers never grows a trailing \".0\".\n */\nconst decimalsFor = (n: number): number => {\n  if (Math.abs(n - Math.round(n)) < 1e-9) return 0;\n  if (Math.abs(n * 10 - Math.round(n * 10)) < 1e-9) return 1;\n  return 2;\n};\n\nconst groupDigits = (digits: string) => digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, THIN_SPACE);\n\nconst trimZero = (n: number, places: number) => n.toFixed(places).replace(/\\.0$/, \"\");\n\nconst formatFigure = (value: number, decimals: number, compact: boolean): string => {\n  const abs = Math.abs(value);\n  if (compact && abs >= 1e6) return `${trimZero(value / 1e6, 1)}M`;\n  if (compact && abs >= 1e3) return `${trimZero(value / 1e3, abs >= 1e4 ? 0 : 1)}k`;\n\n  const fixed = abs.toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const whole = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fraction = dot === -1 ? \"\" : fixed.slice(dot);\n  return `${value < 0 ? \"\\u2212\" : \"\"}${groupDigits(whole)}${fraction}`;\n};\n\n/* ------------------------------------------------------------------ layout */\n\n/** Plot geometry in frame pixels. Computed once, passed down. */\ntype Plot = {\n  left: number;\n  right: number;\n  /** y of the baseline. */\n  bottom: number;\n  /** Pixels from the baseline to the top gridline. */\n  height: number;\n  /** Width of one column, bar plus its share of the gaps. */\n  slot: number;\n  barW: number;\n  barRadius: number;\n  /** x the y-axis figures are right-aligned to. */\n  axisRight: number;\n  /** Space between the axis figures and the plot. The baseline reaches into it. */\n  gutter: number;\n  /** Gridline weight. */\n  hair: number;\n  /** Baseline weight — one step heavier, because it is the datum. */\n  spine: number;\n};\n\n/** Type sizes in frame pixels. */\ntype Scale = {\n  eyebrow: number;\n  title: number;\n  tick: number;\n  cat: number;\n  value: number;\n  foot: number;\n};\n\nconst CLAMP = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\n\n/* --------------------------------------------------------------- component */\n\nconst BarChartReveal: React.FC<Props> = ({\n  data,\n  title,\n  eyebrow,\n  highlight,\n  valuePrefix,\n  valueSuffix,\n  compact,\n  ticks,\n  yAxis,\n  valueLabels,\n  barWidth,\n  footnote,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const f = stage.frame;\n\n  /*\n   * Everything is sized off `base`, one length that survives every aspect: in\n   * landscape the height is the binding constraint, in portrait the width is.\n   * Sizing type off `width` alone would give a 9:16 cut 33px axis figures.\n   */\n  const base = Math.min(width, height * 1.6);\n\n  const scale: Scale = {\n    eyebrow: base * 0.0125,\n    title: base * 0.0345,\n    tick: base * 0.0115,\n    cat: base * 0.0145,\n    value: base * 0.0195,\n    foot: base * 0.0118,\n  };\n\n  const padX = width * 0.072;\n  const padTop = height * 0.082;\n  const padBottom = height * 0.072;\n  const contentW = width - padX * 2;\n\n  // The plot takes what the type leaves it: header above, axis labels and an\n  // optional footnote below, and headroom for the value labels riding the bars.\n  const headerH = (eyebrow ? scale.eyebrow * 2.15 : 0) + (title ? scale.title * 1.16 : 0);\n  const footH = footnote ? scale.foot * 2.7 : 0;\n  const catRowH = scale.cat * 2.5;\n  const valueRoom = valueLabels ? scale.value * 1.95 : scale.value * 0.5;\n\n  const axisW = yAxis ? base * 0.055 : 0;\n  const gutter = yAxis ? width * 0.014 : width * 0.004;\n  const plotLeft = padX + axisW + gutter;\n  const plotRight = width - padX;\n  const bandTop = padTop + headerH + height * 0.05 + valueRoom;\n  const bandBottom = height - padBottom - footH - catRowH;\n\n  /*\n   * A bar chart has a natural aspect. Handed a square or portrait cut it would\n   * otherwise stretch the plot to fill the frame, turning every bar into a\n   * column of spaghetti — so the plot is capped against its own width, and the\n   * leftover band becomes air, weighted upward so the chart still sits low.\n   */\n  const plotW = plotRight - plotLeft;\n  const plotHeight = Math.max(base * 0.12, Math.min(bandBottom - bandTop, plotW * 0.55));\n  const slack = Math.max(0, bandBottom - bandTop - plotHeight);\n  const plotBottom = bandBottom - slack * 0.45;\n  const slot = plotW / Math.max(1, data.length);\n  const barW = slot * barWidth;\n\n  const plot: Plot = {\n    left: plotLeft,\n    right: plotRight,\n    bottom: plotBottom,\n    height: plotHeight,\n    slot,\n    barW,\n    // Radius comes from the brand, not from the bar: a token set with square\n    // corners (Press) gets square bars.\n    barRadius: Math.min(barW * 0.16, Math.max(2, t.radius * 0.55)),\n    axisRight: padX + axisW,\n    gutter,\n    hair: Math.max(1, base * 0.0007),\n    spine: Math.max(1.8, base * 0.0013),\n  };\n\n  const { max: domainMax, step: tickStep } = buildDomain(\n    data.map((d) => d.value),\n    ticks,\n  );\n  const tickDecimals = decimalsFor(tickStep);\n  const valueDecimals = data.reduce((acc, d) => Math.max(acc, decimalsFor(d.value)), 0);\n\n  const unit = (figure: string) => `${valuePrefix ?? \"\"}${figure}${valueSuffix ?? \"\"}`;\n  const fmtTick = (v: number) => unit(formatFigure(v, tickDecimals, compact));\n  const fmtValue = (v: number) => unit(formatFigure(v, valueDecimals, compact));\n\n  // Which bar carries the accent. Out-of-range indices fall back to the peak\n  // rather than throwing away the highlight.\n  const peakIndex = data.reduce((best, d, i) => (d.value > data[best].value ? i : best), 0);\n  const highlighted =\n    highlight !== undefined && highlight >= 0 && highlight < data.length ? highlight : peakIndex;\n\n  // Muted bars sit between the gridline colour and the muted ink: `rule` alone\n  // reads as grid rather than data, `inkMuted` alone competes with the accent.\n  const mutedFill = interpolateColors(0.42, [0, 1], [t.rule, t.inkMuted]);\n\n  const step = barStagger(data.length);\n  const lastLanding = BARS_START + step * (data.length - 1) + BAR_GROW;\n\n  const headIn = interpolate(f, [0, 12], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const titleIn = interpolate(f, [2, 17], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const footRule = interpolate(f, [lastLanding - 10, lastLanding + 8], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const footIn = interpolate(f, [lastLanding - 2, lastLanding + 12], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n\n  // The one accent gesture: once the highlighted bar lands, the baseline under\n  // it thickens into accent — a tick that pins the bar to its own datum.\n  const hiStart = BARS_START + step * highlighted + BAR_GROW;\n  const hiTick = interpolate(f, [hiStart - 3, hiStart + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASING.accent,\n  });\n\n  const lift = (1 - stage.enter) * height * 0.016;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, fontFamily: t.fontBody }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: stage.presence,\n          transform: `translateY(${lift}px)`,\n        }}\n      >\n        {/* Header, hard left. The chart is the composition; the title labels it. */}\n        <div style={{ position: \"absolute\", left: padX, top: padTop, width: contentW }}>\n          {eyebrow ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: scale.eyebrow,\n                fontWeight: 600,\n                letterSpacing: scale.eyebrow * 0.18,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                opacity: headIn,\n                transform: `translateY(${(1 - headIn) * scale.eyebrow * 0.7}px)`,\n              }}\n            >\n              {eyebrow}\n            </div>\n          ) : null}\n\n          {title ? (\n            <span\n              style={{\n                display: \"block\",\n                overflow: \"hidden\",\n                marginTop: eyebrow ? scale.eyebrow * 1.1 : 0,\n                paddingBottom: scale.title * 0.08,\n              }}\n            >\n              <span\n                style={{\n                  display: \"block\",\n                  fontFamily: t.fontDisplay,\n                  fontSize: scale.title,\n                  fontWeight: 600,\n                  lineHeight: 1.06,\n                  letterSpacing: scale.title * -0.02,\n                  color: t.ink,\n                  transform: `translateY(${(1 - titleIn) * 108}%)`,\n                }}\n              >\n                {title}\n              </span>\n            </span>\n          ) : null}\n        </div>\n\n        <Gridlines\n          frame={f}\n          frameW={width}\n          frameH={height}\n          plot={plot}\n          scale={scale}\n          ticks={ticks}\n          tickStep={tickStep}\n          showAxis={yAxis}\n          fmt={fmtTick}\n          t={t}\n        />\n\n        {data.map((d, i) => (\n          <Column\n            key={`${d.label}-${i}`}\n            datum={d}\n            x={plot.left + plot.slot * (i + 0.5)}\n            start={BARS_START + step * i}\n            frame={f}\n            frameH={height}\n            plot={plot}\n            scale={scale}\n            domainMax={domainMax}\n            highlighted={i === highlighted}\n            showValue={valueLabels}\n            fmt={fmtValue}\n            fill={i === highlighted ? t.accent : mutedFill}\n            t={t}\n          />\n        ))}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: plot.left + plot.slot * (highlighted + 0.5) - plot.barW / 2,\n            top: plot.bottom - plot.spine / 2,\n            width: plot.barW,\n            height: plot.spine,\n            backgroundColor: t.accent,\n            transform: `scaleX(${hiTick})`,\n            transformOrigin: \"center\",\n          }}\n        />\n\n        {footnote ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: padX,\n              top: height - padBottom - footH,\n              width: contentW,\n            }}\n          >\n            <div\n              style={{\n                height: plot.hair,\n                backgroundColor: t.rule,\n                transform: `scaleX(${footRule})`,\n                transformOrigin: \"left\",\n              }}\n            />\n            <div\n              style={{\n                marginTop: scale.foot * 0.85,\n                fontFamily: t.fontMono,\n                fontSize: scale.foot,\n                fontWeight: 500,\n                letterSpacing: scale.foot * 0.02,\n                color: t.inkMuted,\n                opacity: footIn,\n              }}\n            >\n              {footnote}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------ pieces */\n\n/**\n * Baseline and gridlines, each genuinely drawn on — a dash the length of the\n * rule, offset back to nothing — rather than scaled: a stroke that grows by\n * transform grows its own weight with it, and a 1px hairline passing through\n * 1.4px looks like a mistake at 4K. The axis figures fade in behind their own\n * line, sliding the last few pixels toward it.\n */\nconst Gridlines: React.FC<{\n  frame: number;\n  frameW: number;\n  frameH: number;\n  plot: Plot;\n  scale: Scale;\n  ticks: number;\n  tickStep: number;\n  showAxis: boolean;\n  fmt: (v: number) => string;\n  t: BrandTokens;\n}> = ({ frame, frameW, frameH, plot, scale, ticks, tickStep, showAxis, fmt, t }) => {\n  const lines = Array.from({ length: ticks + 1 }, (_, k) => {\n    const y = plot.bottom - (plot.height * k) / ticks;\n    const isBaseline = k === 0;\n    // The baseline starts back in the axis gutter, so it reads as attached to\n    // the figures it measures; the gridlines start clean at the plot edge.\n    const x0 = isBaseline ? plot.left - plot.gutter * 0.55 : plot.left;\n    const from = isBaseline ? AXIS_START : AXIS_START + GRID_LAG + k * GRID_STAGGER;\n    return {\n      k,\n      y,\n      x0,\n      length: Math.max(1, plot.right - x0),\n      isBaseline,\n      draw: interpolate(frame, [from, from + AXIS_DRAW], [0, 1], {\n        ...CLAMP,\n        easing: EASING.travel,\n      }),\n      label: interpolate(frame, [from + 4, from + 15], [0, 1], { ...CLAMP, easing: EASING.enter }),\n    };\n  });\n\n  return (\n    <>\n      <svg\n        width={frameW}\n        height={frameH}\n        viewBox={`0 0 ${frameW} ${frameH}`}\n        style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }}\n      >\n        {lines.map((line) => (\n          <line\n            key={line.k}\n            x1={line.x0}\n            y1={line.y}\n            x2={plot.right}\n            y2={line.y}\n            stroke={t.rule}\n            strokeWidth={line.isBaseline ? plot.spine : plot.hair}\n            opacity={line.isBaseline ? 1 : 0.75}\n            strokeDasharray={line.length}\n            strokeDashoffset={line.length * (1 - line.draw)}\n          />\n        ))}\n      </svg>\n\n      {showAxis\n        ? lines.map((line) => (\n            <div\n              key={line.k}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: line.y - scale.tick * 0.7,\n                width: plot.axisRight,\n                textAlign: \"right\",\n                fontFamily: t.fontMono,\n                fontSize: scale.tick,\n                fontWeight: 500,\n                lineHeight: 1.4,\n                // Tabular figures: the column of numbers must not shuffle\n                // sideways as the digit count changes up the axis.\n                fontVariantNumeric: \"tabular-nums\",\n                letterSpacing: scale.tick * 0.01,\n                color: t.inkMuted,\n                opacity: line.label * (line.isBaseline ? 0.85 : 1),\n                transform: `translateX(${(1 - line.label) * scale.tick * -0.5}px)`,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {fmt(tickStep * line.k)}\n            </div>\n          ))\n        : null}\n    </>\n  );\n};\n\n/**\n * One column: the bar, the counting figure that rides its top edge, and the\n * category label under the baseline.\n *\n * The bar grows by animating its height rather than `scaleY`, which keeps the\n * top corner radius circular instead of stretching it into an ellipse — and\n * keeps the growth reading as a measurement rather than a squash.\n */\nconst Column: React.FC<{\n  datum: Datum;\n  x: number;\n  start: number;\n  frame: number;\n  frameH: number;\n  plot: Plot;\n  scale: Scale;\n  domainMax: number;\n  highlighted: boolean;\n  showValue: boolean;\n  fmt: (v: number) => string;\n  fill: string;\n  t: BrandTokens;\n}> = ({\n  datum,\n  x,\n  start,\n  frame,\n  frameH,\n  plot,\n  scale,\n  domainMax,\n  highlighted,\n  showValue,\n  fmt,\n  fill,\n  t,\n}) => {\n  const grow = interpolate(frame, [start, start + BAR_GROW], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  const full = (plot.height * Math.max(0, datum.value)) / domainMax;\n  const h = full * grow;\n\n  const countFrom = start + BAR_GROW * COUNT_LEAD;\n  const count = interpolate(frame, [countFrom, start + BAR_GROW + COUNT_TAIL], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const valueIn = interpolate(frame, [countFrom, countFrom + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  const catIn = interpolate(frame, [start + CAT_DELAY, start + CAT_DELAY + CAT_FADE], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n\n  const radius = Math.min(plot.barRadius, h);\n\n  return (\n    <>\n      <div\n        style={{\n          position: \"absolute\",\n          left: x - plot.barW / 2,\n          top: plot.bottom - h,\n          width: plot.barW,\n          height: h,\n          backgroundColor: fill,\n          borderTopLeftRadius: radius,\n          borderTopRightRadius: radius,\n        }}\n      />\n\n      {showValue ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: x - plot.slot / 2,\n            // Anchored to the bar's top edge, so the figure rides up with it.\n            bottom: frameH - (plot.bottom - h) + scale.value * 0.5,\n            width: plot.slot,\n            textAlign: \"center\",\n            fontFamily: t.fontMono,\n            fontSize: scale.value,\n            fontWeight: highlighted ? 600 : 500,\n            fontVariantNumeric: \"tabular-nums\",\n            letterSpacing: scale.value * -0.005,\n            color: highlighted ? t.ink : t.inkMuted,\n            opacity: valueIn,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {fmt(datum.value * count)}\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: x - plot.slot / 2,\n          top: plot.bottom + scale.cat * 0.95,\n          width: plot.slot,\n          textAlign: \"center\",\n          fontFamily: t.fontBody,\n          fontSize: scale.cat,\n          fontWeight: highlighted ? 600 : 500,\n          letterSpacing: scale.cat * 0.005,\n          color: highlighted ? t.ink : t.inkMuted,\n          opacity: catIn,\n          transform: `translateY(${(1 - catIn) * scale.cat * 0.5}px)`,\n          whiteSpace: \"nowrap\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n        }}\n      >\n        {datum.label}\n      </div>\n    </>\n  );\n};\n\n/* ---------------------------------------------------------------- registry */\n\nexport default defineMotionItem({\n  id: \"bar-chart-reveal\",\n  name: \"Bar Chart Reveal\",\n  category: \"data\",\n  summary:\n    \"Gridlines draw on as hairlines, bars climb from the baseline in reading order, and each value counts up as its bar lands\",\n  tags: [\"bar chart\", \"chart reveal\", \"gridlines\", \"counter\", \"stagger\", \"tabular figures\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: barChartRevealSchema,\n  defaults: {\n    data: [\n      { label: \"Mar\", value: 42 },\n      { label: \"Apr\", value: 58 },\n      { label: \"May\", value: 51 },\n      { label: \"Jun\", value: 76 },\n      { label: \"Jul\", value: 94 },\n      { label: \"Aug\", value: 88 },\n      { label: \"Sep\", value: 137 },\n    ],\n    title: \"Scenes rendered per month\",\n    eyebrow: \"Throughput\",\n    highlight: 6,\n    valuePrefix: undefined,\n    valueSuffix: undefined,\n    compact: false,\n    ticks: 4,\n    yAxis: true,\n    valueLabels: true,\n    barWidth: 0.62,\n    footnote: \"Every scene composed from registry components · September is the launch month\",\n    brand: \"slate\",\n  },\n  component: BarChartReveal,\n  file: \"src/registry/items/bar-chart-reveal.tsx\",\n  origin: {\n    prompt:\n      \"A bar chart that reveals in reading order: the baseline and gridlines draw themselves on as hairlines first, then the bars grow up out of the baseline with a stagger left to right, then the values count up above each bar and the category labels fade in underneath. One bar in the accent colour, the rest muted grey. Y-axis ticks in mono with tabular figures, and round the domain up to a sensible number above the data rather than using the raw maximum.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A bar chart that reveals in reading order: the baseline and gridlines draw themselves on as hairlines first, then the bars grow up out of the baseline with a stagger left to right, then the values count up above each bar and the category labels fade in underneath. One bar in the accent colour, the rest muted grey. Y-axis ticks in mono with tabular figures, and round the domain up to a sensible number above the data rather than using the raw maximum.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2626,
      "props": [
        {
          "name": "data",
          "type": "array",
          "description": "The series, in reading order — the reveal staggers left to right, so order the array the way you want the chart read. Five to eight bars is the sweet spot; twelve is the ceiling before the columns thin out.",
          "required": true,
          "defaultValue": "7 items",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "Headline above the plot, set in the display face. Say what is measured, not what the shape does ('Scenes rendered per month', not 'Growth'). One line reads best.",
          "required": false,
          "defaultValue": "Scenes rendered per month",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label in the accent colour above the title. Omit for a barer chart.",
          "required": false,
          "defaultValue": "Throughput",
          "options": null
        },
        {
          "name": "highlight",
          "type": "number",
          "description": "Zero-based index of the one bar carried in the accent colour; every other bar is muted. Omit to highlight the largest value automatically. This is the argument of the chart, so pick the bar the scene is about.",
          "required": false,
          "defaultValue": "6",
          "options": null
        },
        {
          "name": "valuePrefix",
          "type": "string",
          "description": "Set immediately before every figure, on the bars and the axis alike — a currency mark such as '$' or '€'.",
          "required": false,
          "defaultValue": null,
          "options": null
        },
        {
          "name": "valueSuffix",
          "type": "string",
          "description": "Set immediately after every figure — '%', 'k', 'ms'. Rendered exactly as typed, so include a leading thin space yourself if the unit needs air.",
          "required": false,
          "defaultValue": null,
          "options": null
        },
        {
          "name": "compact",
          "type": "boolean",
          "description": "Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Turn it on once the figures pass four digits.",
          "required": false,
          "defaultValue": "false",
          "options": null
        },
        {
          "name": "ticks",
          "type": "number",
          "description": "Number of gridlines above the baseline. It also sets the y domain: the top line is always a round number, so 4 over a peak of 137 gives 0/40/80/120/160.",
          "required": false,
          "defaultValue": "4",
          "options": null
        },
        {
          "name": "yAxis",
          "type": "boolean",
          "description": "Show the y-axis tick figures at the left, in mono with tabular figures. Off gives a barer chart that leans on the value labels.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "valueLabels",
          "type": "boolean",
          "description": "Count each bar's value up above its column as the bar lands. Off when the axis alone should carry the numbers.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "barWidth",
          "type": "number",
          "description": "Bar thickness as a fraction of its column slot. 0.62 is the editorial default; above 0.8 the chart reads as a block, below 0.4 as a set of ticks.",
          "required": false,
          "defaultValue": "0.62",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Quiet mono caption under a hairline at the foot of the frame: the source, the period, the qualifier. One short sentence.",
          "required": false,
          "defaultValue": "Every scene composed from registry components · September…",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "browser-chrome",
      "name": "Browser Chrome",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A browser window assembles piece by piece, types a URL into the address bar, then resolves a shimmering skeleton into a finished page",
      "tags": [
        "browser",
        "window",
        "chrome",
        "url",
        "typing",
        "skeleton",
        "product shot",
        "ui mock"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/browser-chrome.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nconst BUDGET = BUDGETS.scene;\n\n/* ---------------------------------------------------------------- schedule */\n\n/**\n * Assembly order. Every piece of chrome rides `useStaggeredStage` at two frames\n * per step, so the window builds itself in one gesture: dots, tab, omnibox,\n * nav glyphs, then the loading document underneath.\n */\nconst PIECE = { dots: 0, tab: 3, tabLabel: 4, omnibox: 4, nav: 5, page: 6 } as const;\nconst PIECE_STEP = 2;\n\n/** The frame by which nothing in the chrome is moving any more. */\nconst ASSEMBLED = BUDGET.enter + PIECE.page * PIECE_STEP;\n\n/**\n * Typing owns the first half of the hold. The schedule stretches or compresses\n * to the length of the URL rather than running at a fixed rate, so the page\n * commits on the same frame whatever address you pass — a component whose\n * beats move when you change a string is a component you cannot sequence.\n */\nconst TYPE_START = ASSEMBLED + 2;\nconst TYPE_END = BUDGET.enter + Math.round(BUDGET.hold * 0.55);\n/** Return is pressed here: the load bar sweeps and the skeleton resolves. */\nconst COMMIT = TYPE_END + 6;\nconst LOAD: [number, number] = [COMMIT, COMMIT + 20];\nconst BAR: [number, number] = [COMMIT, COMMIT + 17];\n/** One full caret blink cycle. 20 frames is ~1.5 blinks a second at 30fps. */\nconst BLINK = 20;\n\n/** Text rows per column in the page body. */\nconst ROWS = 5;\n\n/* ---------------------------------------------------------------- geometry */\n\n/** Height / width of the window. A shade taller than 16:10, which is where a real browser lands once its chrome is counted. */\nconst WINDOW_RATIO = 0.615;\n\nexport const browserChromeSchema = z.object({\n  url: z\n    .string()\n    .default(\"motionregistry.dev/browser-chrome\")\n    .describe(\n      \"The address typed into the omnibox, one character at a time. Everything before the first '/' is read as the host and set in full-strength ink; the path dims behind it, the way a real browser weights it. Leave the scheme off — browsers hide it. 20–45 characters reads best; the keystroke schedule fits itself to the length.\",\n    ),\n  tabTitle: z\n    .string()\n    .default(\"Motionstash\")\n    .describe(\n      \"Label in the active tab, beside a favicon square carrying its first letter. The tab is roughly a quarter of the window wide, so anything past ~22 characters ellipses.\",\n    ),\n  pageTitle: z\n    .string()\n    .default(\"Draw the chrome, not the screenshot\")\n    .describe(\n      \"Display heading of the page that resolves out of the skeleton. Four to seven words; it sets across three-quarters of the page and wraps to a second line.\",\n    ),\n  buttonLabel: z\n    .string()\n    .default(\"Copy component\")\n    .describe(\"Label of the single accent-filled button on the synthesised page. Two or three words — the pill sizes itself to the text.\"),\n  secondaryLabel: z\n    .string()\n    .default(\"View source\")\n    .describe(\"Label of the hairline ghost button beside it. Empty string for a single button.\"),\n  eyebrow: z\n    .string()\n    .default(\"Product & UI\")\n    .describe(\"Small mono label above the window, led by an accent dash and aligned to the window's left edge. Empty string for none.\"),\n  caption: z\n    .string()\n    .default(\"Chrome drawn in divs and inline SVG — no screenshot, no assets\")\n    .describe(\"One muted line under the window, aligned to the same left edge. Empty string for none.\"),\n  trafficLights: z\n    .enum([\"mono\", \"brand\"])\n    .default(\"mono\")\n    .describe(\n      \"'mono' draws the three window dots in muted ink, which is what a product shot usually wants; 'brand' colours them accent / alt accent / muted for a warmer, more literal frame.\",\n    ),\n  lock: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the padlock at the head of the address bar. Turn it off for a localhost or intranet address, where a browser would not show one.\"),\n  seed: z\n    .string()\n    .default(\"chrome-01\")\n    .describe(\n      \"Seeds the keystroke jitter and the ragged widths of the skeleton rows, which the text rules inherit so the cross-fade lands in register. Change it for a differently-set page of identical duration; the same seed always renders the same frames.\",\n    ),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof browserChromeSchema>;\n\n/**\n * Every measurement in the component, derived once from the frame so one file\n * serves 16:9, 9:16, 1:1 and 4:5. Nothing below reads `useVideoConfig` again.\n */\ntype Layout = {\n  /** Window box. Bounded by width in portrait, by height in landscape. */\n  w: number;\n  h: number;\n  /** The two chrome rows: tab strip, then toolbar. */\n  tabRow: number;\n  navRow: number;\n  /** 1–2px. A hairline that survives a downscaled preview but never thickens into a border. */\n  hair: number;\n  /** Window corner radius, taken from the brand's own radius token. */\n  radius: number;\n  /** Page padding and the content measure inside it. */\n  pad: number;\n  content: number;\n  colGap: number;\n  colW: number;\n  /** Type scale of the synthesised page. */\n  kicker: number;\n  heading: number;\n  /** Vertical box reserved for the heading in BOTH layers, so the divider never jumps. */\n  headBlock: number;\n  /** One text row's slot: the height a skeleton block fills and a hairline rule centres in. */\n  slot: number;\n  rowGap: number;\n  subhead: number;\n  btnH: number;\n  btnFont: number;\n  /** The page's vertical rhythm unit. */\n  gap: number;\n};\n\nconst measure = (width: number, height: number, radius: number): Layout => {\n  const hair = Math.max(1, Math.round(width * 0.0011));\n  const w = Math.min(width * 0.86, (height * 0.7) / WINDOW_RATIO);\n  const h = w * WINDOW_RATIO;\n  const pad = w * 0.052;\n  const content = w - pad * 2;\n  const colGap = content * 0.075;\n  const heading = w * 0.046;\n  return {\n    w,\n    h,\n    tabRow: h * 0.088,\n    navRow: h * 0.108,\n    hair,\n    radius: Math.max(radius, 3) * 1.3,\n    pad,\n    content,\n    colGap,\n    colW: (content - colGap) / 2,\n    kicker: w * 0.0135,\n    heading,\n    headBlock: heading * 2.3,\n    slot: Math.max(hair * 3, h * 0.013),\n    rowGap: h * 0.024,\n    subhead: Math.max(hair * 2, h * 0.0075),\n    btnH: h * 0.062,\n    btnFont: h * 0.023,\n    gap: h * 0.045,\n  };\n};\n\n/**\n * A token surface with a black scrim composited onto it, in one declaration.\n *\n * The chrome needs three depths — tab strip, toolbar, document — and a brand\n * ships one `surface`, so the two upper layers are that surface, darkened. A\n * scrim is the one literal colour the brief allows, and it reads correctly on\n * light and dark brands alike: chrome sits back, the document comes forward.\n */\nconst shade = (base: string, alpha: number): React.CSSProperties => ({\n  backgroundColor: base,\n  backgroundImage: `linear-gradient(rgba(0,0,0,${alpha}), rgba(0,0,0,${alpha}))`,\n});\n\nconst TAB_SHADE = 0.05;\nconst STRIP_SHADE = 0.16;\n\n/** Ragged-right measure for one text row, shared by both page layers. */\nconst rowWidth = (seed: string, col: number, i: number) => 0.52 + random(`${seed}:row:${col}:${i}`) * 0.46;\n\n/**\n * How many characters have landed by `frame`.\n *\n * Each key is nudged up to ±35% of one interval off the metronome, seeded, so\n * the cadence reads as a hand rather than a clock. The nudge is smaller than\n * the interval, which keeps the schedule monotonic — no character can arrive\n * before the one in front of it.\n */\nconst typedCount = (frame: number, chars: number, seed: string) => {\n  const per = (TYPE_END - TYPE_START) / Math.max(1, chars);\n  let count = 0;\n  for (let i = 0; i < chars; i += 1) {\n    const at = TYPE_START + (i + 0.5 + (random(`${seed}:key:${i}`) - 0.5) * 0.7) * per;\n    if (frame >= at) count = i + 1;\n  }\n  return count;\n};\n\n/* --------------------------------------------------------------- component */\n\nconst BrowserChrome: React.FC<Props> = ({\n  url,\n  tabTitle,\n  pageTitle,\n  buttonLabel,\n  secondaryLabel,\n  eyebrow,\n  caption,\n  trafficLights,\n  lock,\n  seed,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const l = measure(width, height, t.radius);\n  const meta = width * 0.0105;\n\n  // The window arrives as one object: a gentle scale tied to presence, so the\n  // exit unwinds the entrance instead of inventing a second curve for it.\n  const scale = interpolate(stage.presence, [0, 1], [0.96, 1]);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        padding: `${height * 0.06}px ${width * 0.05}px`,\n      }}\n    >\n      {/* One soft accent wash behind the window, so the glass reads as lit rather than pasted on. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: l.w * 1.3,\n          height: l.w * 0.82,\n          transform: \"translate(-50%, -56%)\",\n          borderRadius: \"50%\",\n          backgroundImage: `radial-gradient(closest-side, ${t.accent}, rgba(0,0,0,0))`,\n          opacity: 0.09 * stage.presence,\n        }}\n      />\n\n      <div style={{ width: l.w, opacity: stage.presence }}>\n        {eyebrow ? <Eyebrow label={eyebrow} tokens={t} size={meta} hair={l.hair} gap={l.gap} /> : null}\n\n        <div\n          style={{\n            width: l.w,\n            height: l.h,\n            boxSizing: \"border-box\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            overflow: \"hidden\",\n            borderRadius: l.radius,\n            border: `${l.hair}px solid ${t.rule}`,\n            backgroundColor: t.surface,\n            boxShadow: `0 ${l.hair}px ${l.hair * 3}px rgba(0,0,0,0.18), 0 ${l.h * 0.045}px ${l.h * 0.1}px rgba(0,0,0,0.34)`,\n            transform: `scale(${scale})`,\n          }}\n        >\n          {/* Tab strip. Sits back a full shade; the active tab is the one thing on it that doesn't. */}\n          <div\n            style={{\n              flexShrink: 0,\n              height: l.tabRow,\n              display: \"flex\",\n              alignItems: \"flex-end\",\n              gap: l.tabRow * 0.24,\n              paddingLeft: l.w * 0.018,\n              ...shade(t.surface, STRIP_SHADE),\n            }}\n          >\n            <TrafficLights tokens={t} row={l.tabRow} mode={trafficLights} />\n            <Tab tokens={t} l={l} title={tabTitle} />\n            <NewTabGlyph tokens={t} row={l.tabRow} />\n          </div>\n\n          {/* Toolbar. Same shade as the active tab, so the two read as one plane. */}\n          <div\n            style={{\n              flexShrink: 0,\n              height: l.navRow,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: l.navRow * 0.2,\n              padding: `0 ${l.w * 0.018}px`,\n              ...shade(t.surface, TAB_SHADE),\n            }}\n          >\n            <Chevron tokens={t} size={l.navRow * 0.34} back opacity={0.75} />\n            <Chevron tokens={t} size={l.navRow * 0.34} back={false} opacity={0.3} />\n            <Omnibox tokens={t} l={l} url={url} lock={lock} seed={seed} />\n            <Kebab tokens={t} row={l.navRow} />\n          </div>\n\n          {/* Document. The two layers share one grid, so the cross-fade is a resolve, not a swap. */}\n          <div\n            style={{\n              flex: 1,\n              position: \"relative\",\n              overflow: \"hidden\",\n              backgroundColor: t.surface,\n              borderTop: `${l.hair}px solid ${t.rule}`,\n            }}\n          >\n            <LoadBar tokens={t} height={Math.max(l.hair, l.h * 0.0035)} />\n            <SkeletonLayer\n              tokens={t}\n              l={l}\n              seed={seed}\n              buttonLabel={buttonLabel}\n              secondaryLabel={secondaryLabel}\n            />\n            <PageLayer\n              tokens={t}\n              l={l}\n              url={url}\n              title={pageTitle}\n              buttonLabel={buttonLabel}\n              secondaryLabel={secondaryLabel}\n              seed={seed}\n            />\n          </div>\n        </div>\n\n        {caption ? <Caption label={caption} tokens={t} size={meta} gap={l.gap} /> : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------ around */\n\nconst Eyebrow: React.FC<{ label: string; tokens: BrandTokens; size: number; hair: number; gap: number }> = ({\n  label,\n  tokens,\n  size,\n  hair,\n  gap,\n}) => {\n  const s = useStaggeredStage(BUDGET, 0, PIECE_STEP);\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.7,\n        marginBottom: gap * 1.1,\n        opacity: s.enter,\n        transform: `translateY(${interpolate(s.enter, [0, 1], [size * 0.5, 0])}px)`,\n      }}\n    >\n      <div style={{ width: size * 1.8, height: hair, backgroundColor: tokens.accent }} />\n      <div\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: size,\n          fontWeight: 600,\n          letterSpacing: size * 0.14,\n          textTransform: \"uppercase\",\n          color: tokens.accent,\n        }}\n      >\n        {label}\n      </div>\n    </div>\n  );\n};\n\nconst Caption: React.FC<{ label: string; tokens: BrandTokens; size: number; gap: number }> = ({\n  label,\n  tokens,\n  size,\n  gap,\n}) => {\n  // Held back until the page has resolved: it describes a finished frame.\n  const frame = useCurrentFrame();\n  const p = interpolate(frame, [LOAD[1] - 6, LOAD[1] + 8], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  return (\n    <div\n      style={{\n        marginTop: gap,\n        fontFamily: tokens.fontBody,\n        fontSize: size * 0.92,\n        fontWeight: 400,\n        letterSpacing: size * 0.01,\n        color: tokens.inkMuted,\n        opacity: p,\n      }}\n    >\n      {label}\n    </div>\n  );\n};\n\n/* ------------------------------------------------------------------ chrome */\n\nconst TrafficLights: React.FC<{ tokens: BrandTokens; row: number; mode: Props[\"trafficLights\"] }> = ({\n  tokens,\n  row,\n  mode,\n}) => {\n  const d = row * 0.185;\n  const colors =\n    mode === \"brand\" ? [tokens.accent, tokens.accentAlt, tokens.inkMuted] : [tokens.inkMuted, tokens.inkMuted, tokens.inkMuted];\n  return (\n    <div style={{ alignSelf: \"center\", display: \"flex\", gap: d * 0.62, flexShrink: 0 }}>\n      {colors.map((c, i) => (\n        <Dot key={i} index={i} color={c} d={d} dim={mode === \"mono\"} />\n      ))}\n    </div>\n  );\n};\n\n/** One window dot, scaling up from nothing on its own step of the assembly. */\nconst Dot: React.FC<{ index: number; color: string; d: number; dim: boolean }> = ({ index, color, d, dim }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.dots + index, PIECE_STEP);\n  return (\n    <div\n      style={{\n        width: d,\n        height: d,\n        borderRadius: d / 2,\n        backgroundColor: color,\n        opacity: s.enter * (dim ? 0.45 : 1),\n        transform: `scale(${interpolate(s.enter, [0, 1], [0.2, 1])})`,\n      }}\n    />\n  );\n};\n\n/**\n * The active tab. It opens by growing from the left, which is what a real tab\n * does, and pushes the new-tab glyph along as it goes. Its bottom corners stay\n * square so it merges into the toolbar below.\n */\nconst Tab: React.FC<{ tokens: BrandTokens; l: Layout; title: string }> = ({ tokens, l, title }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.tab, PIECE_STEP);\n  const label = useStaggeredStage(BUDGET, PIECE.tabLabel, PIECE_STEP);\n  const full = l.w * 0.27;\n  const r = Math.min(l.radius, l.tabRow * 0.3);\n  const fav = l.tabRow * 0.3;\n  const glyph = l.tabRow * 0.26;\n  return (\n    <div\n      style={{\n        flexShrink: 0,\n        boxSizing: \"border-box\",\n        width: full * interpolate(s.enter, [0, 1], [0.62, 1]),\n        height: l.tabRow,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: fav * 0.55,\n        overflow: \"hidden\",\n        padding: `0 ${fav * 0.6}px`,\n        borderTopLeftRadius: r,\n        borderTopRightRadius: r,\n        borderTop: `${l.hair}px solid ${tokens.rule}`,\n        borderLeft: `${l.hair}px solid ${tokens.rule}`,\n        borderRight: `${l.hair}px solid ${tokens.rule}`,\n        opacity: s.enter,\n        ...shade(tokens.surface, TAB_SHADE),\n      }}\n    >\n      {/* Favicon: an accent square carrying the first letter of the title. */}\n      <div\n        style={{\n          flexShrink: 0,\n          width: fav,\n          height: fav,\n          borderRadius: fav * 0.28,\n          backgroundColor: tokens.accent,\n          color: tokens.accentInk,\n          fontFamily: tokens.fontMono,\n          fontSize: fav * 0.6,\n          fontWeight: 700,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          opacity: label.enter,\n        }}\n      >\n        {(title.trim().charAt(0) || \"M\").toUpperCase()}\n      </div>\n      <div\n        style={{\n          flex: 1,\n          minWidth: 0,\n          whiteSpace: \"nowrap\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n          fontFamily: tokens.fontBody,\n          fontSize: l.tabRow * 0.3,\n          fontWeight: 500,\n          letterSpacing: l.tabRow * -0.002,\n          color: tokens.ink,\n          opacity: label.enter,\n        }}\n      >\n        {title}\n      </div>\n      <svg\n        width={glyph}\n        height={glyph}\n        viewBox=\"0 0 16 16\"\n        style={{ flexShrink: 0, opacity: label.enter * 0.5, overflow: \"visible\" }}\n      >\n        <path\n          d=\"M4 4 L12 12 M12 4 L4 12\"\n          fill=\"none\"\n          stroke={tokens.inkMuted}\n          strokeWidth={1.6}\n          strokeLinecap=\"round\"\n        />\n      </svg>\n    </div>\n  );\n};\n\nconst NewTabGlyph: React.FC<{ tokens: BrandTokens; row: number }> = ({ tokens, row }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.nav, PIECE_STEP);\n  const size = row * 0.3;\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 16 16\"\n      style={{ alignSelf: \"center\", flexShrink: 0, opacity: s.enter * 0.5, overflow: \"visible\" }}\n    >\n      <path d=\"M8 3 V13 M3 8 H13\" fill=\"none\" stroke={tokens.inkMuted} strokeWidth={1.6} strokeLinecap=\"round\" />\n    </svg>\n  );\n};\n\nconst Chevron: React.FC<{ tokens: BrandTokens; size: number; back: boolean; opacity: number }> = ({\n  tokens,\n  size,\n  back,\n  opacity,\n}) => {\n  const s = useStaggeredStage(BUDGET, PIECE.nav, PIECE_STEP);\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox=\"0 0 24 24\"\n      style={{ flexShrink: 0, opacity: s.enter * opacity, overflow: \"visible\" }}\n    >\n      <path\n        d={back ? \"M14.5 5 L7.5 12 L14.5 19\" : \"M9.5 5 L16.5 12 L9.5 19\"}\n        fill=\"none\"\n        stroke={tokens.inkMuted}\n        strokeWidth={2.1}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  );\n};\n\nconst Kebab: React.FC<{ tokens: BrandTokens; row: number }> = ({ tokens, row }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.nav, PIECE_STEP);\n  const d = Math.max(2, row * 0.045);\n  return (\n    <div\n      style={{\n        flexShrink: 0,\n        display: \"flex\",\n        flexDirection: \"column\",\n        gap: d * 0.9,\n        marginLeft: row * 0.1,\n        opacity: s.enter * 0.55,\n      }}\n    >\n      {[0, 1, 2].map((i) => (\n        <div key={i} style={{ width: d, height: d, borderRadius: d / 2, backgroundColor: tokens.inkMuted }} />\n      ))}\n    </div>\n  );\n};\n\n/* ----------------------------------------------------------------- omnibox */\n\n/**\n * The address bar. Recessed to the canvas colour inside the toolbar, which is\n * how a field reads on any brand without inventing a fourth surface token.\n */\nconst Omnibox: React.FC<{ tokens: BrandTokens; l: Layout; url: string; lock: boolean; seed: string }> = ({\n  tokens,\n  l,\n  url,\n  lock,\n  seed,\n}) => {\n  const s = useStaggeredStage(BUDGET, PIECE.omnibox, PIECE_STEP);\n  const frame = useCurrentFrame();\n\n  const h = l.navRow * 0.52;\n  const font = h * 0.44;\n\n  const typed = typedCount(frame, url.length, seed);\n  const shown = url.slice(0, typed);\n  const slash = url.indexOf(\"/\");\n  const hostEnd = slash < 0 ? url.length : slash;\n  const host = shown.slice(0, hostEnd);\n  const path = shown.slice(hostEnd);\n\n  // Solid while keys are landing, blinking either side of that, gone once the\n  // address has been committed and the page owns the frame.\n  const typing = frame >= TYPE_START && typed < url.length;\n  const phase = (((frame % BLINK) + BLINK) % BLINK) / BLINK;\n  const blink = 0.15 + 0.85 * (0.5 + 0.5 * Math.cos(phase * Math.PI * 2));\n  const caret =\n    interpolate(frame, [TYPE_START - 8, TYPE_START - 3], [0, 1], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" }) *\n    interpolate(frame, [COMMIT, COMMIT + 5], [1, 0], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" }) *\n    (typing ? 1 : blink);\n\n  return (\n    <div\n      style={{\n        flex: 1,\n        minWidth: 0,\n        boxSizing: \"border-box\",\n        height: h,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: h * 0.34,\n        padding: `0 ${h * 0.42}px`,\n        borderRadius: h / 2,\n        border: `${l.hair}px solid ${tokens.rule}`,\n        backgroundColor: tokens.bg,\n        opacity: s.enter,\n        transform: `scaleX(${interpolate(s.enter, [0, 1], [0.94, 1])})`,\n        transformOrigin: \"left center\",\n      }}\n    >\n      {lock ? <Padlock color={tokens.inkMuted} size={h * 0.44} /> : null}\n      <div\n        style={{\n          flex: 1,\n          minWidth: 0,\n          display: \"flex\",\n          alignItems: \"center\",\n          overflow: \"hidden\",\n          whiteSpace: \"pre\",\n          fontFamily: tokens.fontMono,\n          fontSize: font,\n          letterSpacing: font * 0.005,\n          lineHeight: 1,\n        }}\n      >\n        <span style={{ color: tokens.ink, fontWeight: 500 }}>{host}</span>\n        <span style={{ color: tokens.inkMuted, fontWeight: 400 }}>{path}</span>\n        <span\n          style={{\n            display: \"inline-block\",\n            width: Math.max(l.hair, font * 0.07),\n            height: font * 1.15,\n            marginLeft: font * 0.09,\n            backgroundColor: tokens.accent,\n            opacity: caret,\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\n/** Padlock, drawn: a body and a shackle. Two shapes, no icon set. */\nconst Padlock: React.FC<{ color: string; size: number }> = ({ color, size }) => (\n  <svg\n    width={size}\n    height={size * (14 / 12)}\n    viewBox=\"0 0 12 14\"\n    style={{ flexShrink: 0, overflow: \"visible\" }}\n  >\n    <path d=\"M3.3 6 V4.1 a2.7 2.7 0 0 1 5.4 0 V6\" fill=\"none\" stroke={color} strokeWidth={1.3} strokeLinecap=\"round\" />\n    <rect x={1} y={6} width={10} height={7.2} rx={1.7} fill={color} />\n  </svg>\n);\n\n/* ------------------------------------------------------------------- page */\n\n/** The one accent gesture in the component: a hairline sweeping the page's top edge. */\nconst LoadBar: React.FC<{ tokens: BrandTokens; height: number }> = ({ tokens, height }) => {\n  const frame = useCurrentFrame();\n  const p = interpolate(frame, BAR, [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const fade = interpolate(frame, [BAR[1], BAR[1] + 8], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        height,\n        width: `${p * 100}%`,\n        backgroundColor: tokens.accent,\n        opacity: fade,\n      }}\n    />\n  );\n};\n\n/**\n * The loading document: the same grid the real page uses, drawn as solid\n * blocks and swept by a shimmer. The shimmer is a gradient whose background\n * position is a pure function of the frame — no keyframes, no transitions.\n */\nconst SkeletonLayer: React.FC<{\n  tokens: BrandTokens;\n  l: Layout;\n  seed: string;\n  buttonLabel: string;\n  secondaryLabel: string;\n}> = ({ tokens, l, seed, buttonLabel, secondaryLabel }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.page, PIECE_STEP);\n  const frame = useCurrentFrame();\n  // The skeleton clears faster than the page arrives. A symmetric cross-fade\n  // holds both layers at half strength through the middle of the transition,\n  // which reads as mud; letting the placeholder go first reads as a resolve.\n  const out = interpolate(frame, [LOAD[0], LOAD[0] + 13], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const block = (row: number, w: number, hgt: number): React.CSSProperties => {\n    // 45-frame cycle, offset per row, so the highlight travels down and across.\n    const cycle = 45;\n    const local = (((frame + row * 5) % cycle) + cycle) % cycle;\n    const x = interpolate(local / cycle, [0, 1], [150, -50]);\n    return {\n      width: `${w * 100}%`,\n      height: hgt,\n      borderRadius: Math.min(hgt / 2, l.slot),\n      backgroundColor: tokens.rule,\n      backgroundImage: `linear-gradient(90deg, ${tokens.rule} 0%, ${tokens.rule} 36%, ${tokens.inkMuted} 50%, ${tokens.rule} 64%, ${tokens.rule} 100%)`,\n      backgroundSize: \"260% 100%\",\n      backgroundPosition: `${x}% 0%`,\n    };\n  };\n\n  return (\n    <div style={{ ...pageFrame(l), opacity: s.enter * out }}>\n      <div style={{ ...block(0, 0.14, l.subhead * 1.4), marginBottom: l.gap * 0.6 }} />\n\n      <div style={{ height: l.headBlock, display: \"flex\", flexDirection: \"column\", justifyContent: \"flex-start\" }}>\n        <div style={{ ...block(1, 0.6, l.heading * 0.56), marginBottom: l.heading * 0.42 }} />\n        <div style={block(2, 0.36, l.heading * 0.56)} />\n      </div>\n\n      <div style={{ height: l.hair, backgroundColor: tokens.rule, marginTop: l.gap, marginBottom: l.gap }} />\n\n      <div style={{ display: \"flex\", gap: l.colGap }}>\n        {[0, 1].map((col) => (\n          <div key={col} style={{ width: l.colW }}>\n            <div style={{ ...block(3 + col, col === 0 ? 0.34 : 0.28, l.subhead), marginBottom: l.rowGap }} />\n            {Array.from({ length: ROWS }, (_, i) => (\n              <div\n                key={i}\n                style={{\n                  ...block(5 + col * ROWS + i, rowWidth(seed, col, i), l.slot),\n                  marginBottom: i === ROWS - 1 ? 0 : l.rowGap,\n                }}\n              />\n            ))}\n          </div>\n        ))}\n      </div>\n\n      <div style={{ display: \"flex\", gap: l.gap * 0.4, marginTop: l.gap }}>\n        {[buttonLabel, secondaryLabel]\n          .filter((label) => label.length > 0)\n          .map((label, i) => (\n            <div\n              key={i}\n              style={{\n                ...block(16 + i, 1, l.btnH),\n                width: pillWidth(label, l),\n                borderRadius: Math.min(l.btnH / 2, l.radius),\n              }}\n            />\n          ))}\n      </div>\n    </div>\n  );\n};\n\n/** Shared padding box. Both layers sit in exactly the same grid. */\nconst pageFrame = (l: Layout): React.CSSProperties => ({\n  position: \"absolute\",\n  inset: 0,\n  boxSizing: \"border-box\",\n  padding: `${l.pad * 0.92}px ${l.pad}px`,\n});\n\n/** Pill width from a label, so the skeleton's placeholder is the same size as the real button. */\nconst pillWidth = (label: string, l: Layout) => l.btnH * 0.95 + label.length * l.btnFont * 0.56;\n\nconst PageLayer: React.FC<{\n  tokens: BrandTokens;\n  l: Layout;\n  url: string;\n  title: string;\n  buttonLabel: string;\n  secondaryLabel: string;\n  seed: string;\n}> = ({ tokens, l, url, title, buttonLabel, secondaryLabel, seed }) => {\n  const frame = useCurrentFrame();\n\n  // Four blocks resolve in on a three-frame stagger inside the load window:\n  // the page settles the way a document paints, top down.\n  const step = (i: number) =>\n    interpolate(frame, [LOAD[0] + i * 3, LOAD[0] + i * 3 + 16], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.enter,\n    });\n  const lift = (p: number) => `translateY(${interpolate(p, [0, 1], [l.gap * 0.4, 0])}px)`;\n\n  const head = step(0);\n  const rule = step(1);\n  const body = step(2);\n  const cta = step(3);\n  const host = url.split(\"/\")[0];\n\n  return (\n    <div style={pageFrame(l)}>\n      <div style={{ opacity: head, transform: lift(head) }}>\n        <div\n          style={{\n            fontFamily: tokens.fontMono,\n            fontSize: l.kicker,\n            fontWeight: 600,\n            letterSpacing: l.kicker * 0.16,\n            textTransform: \"uppercase\",\n            color: tokens.accent,\n            marginBottom: l.gap * 0.45,\n          }}\n        >\n          {host}\n        </div>\n        <div style={{ height: l.headBlock }}>\n          <div\n            style={{\n              maxWidth: l.content * 0.78,\n              fontFamily: tokens.fontDisplay,\n              fontSize: l.heading,\n              fontWeight: 700,\n              lineHeight: 1.08,\n              letterSpacing: l.heading * -0.022,\n              color: tokens.ink,\n            }}\n          >\n            {title}\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          height: l.hair,\n          marginTop: l.gap,\n          marginBottom: l.gap,\n          backgroundColor: tokens.rule,\n          transform: `scaleX(${rule})`,\n          transformOrigin: \"left center\",\n        }}\n      />\n\n      <div style={{ display: \"flex\", gap: l.colGap, opacity: body, transform: lift(body) }}>\n        {[0, 1].map((col) => (\n          <div key={col} style={{ width: l.colW }}>\n            {/* Sub-head rule: the one row per column that resolves in ink rather than hairline grey. */}\n            <div\n              style={{\n                width: `${(col === 0 ? 0.34 : 0.28) * 100}%`,\n                height: l.subhead,\n                backgroundColor: tokens.inkMuted,\n                marginBottom: l.rowGap,\n              }}\n            />\n            {Array.from({ length: ROWS }, (_, i) => (\n              <div\n                key={i}\n                style={{\n                  height: l.slot,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                  marginBottom: i === ROWS - 1 ? 0 : l.rowGap,\n                }}\n              >\n                <div\n                  style={{\n                    width: `${rowWidth(seed, col, i) * 100}%`,\n                    height: l.hair,\n                    backgroundColor: tokens.rule,\n                  }}\n                />\n              </div>\n            ))}\n          </div>\n        ))}\n      </div>\n\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: l.gap * 0.4,\n          marginTop: l.gap,\n          opacity: cta,\n          transform: lift(cta),\n        }}\n      >\n        <div\n          style={{\n            boxSizing: \"border-box\",\n            width: pillWidth(buttonLabel, l),\n            height: l.btnH,\n            borderRadius: Math.min(l.btnH / 2, l.radius),\n            backgroundColor: tokens.accent,\n            color: tokens.accentInk,\n            fontFamily: tokens.fontBody,\n            fontSize: l.btnFont,\n            fontWeight: 600,\n            letterSpacing: l.btnFont * 0.01,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          {buttonLabel}\n        </div>\n        {secondaryLabel ? (\n          <div\n            style={{\n              boxSizing: \"border-box\",\n              width: pillWidth(secondaryLabel, l),\n              height: l.btnH,\n              borderRadius: Math.min(l.btnH / 2, l.radius),\n              border: `${l.hair}px solid ${tokens.rule}`,\n              color: tokens.inkMuted,\n              fontFamily: tokens.fontBody,\n              fontSize: l.btnFont,\n              fontWeight: 500,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n            }}\n          >\n            {secondaryLabel}\n          </div>\n        ) : null}\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"browser-chrome\",\n  name: \"Browser Chrome\",\n  category: \"product\",\n  summary:\n    \"A browser window assembles piece by piece, types a URL into the address bar, then resolves a shimmering skeleton into a finished page\",\n  tags: [\"browser\", \"window\", \"chrome\", \"url\", \"typing\", \"skeleton\", \"product shot\", \"ui mock\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: browserChromeSchema,\n  defaults: {\n    url: \"motionregistry.dev/browser-chrome\",\n    tabTitle: \"Motionstash\",\n    pageTitle: \"Draw the chrome, not the screenshot\",\n    buttonLabel: \"Copy component\",\n    secondaryLabel: \"View source\",\n    eyebrow: \"Product & UI\",\n    caption: \"Chrome drawn in divs and inline SVG — no screenshot, no assets\",\n    trafficLights: \"mono\",\n    lock: true,\n    seed: \"chrome-01\",\n    brand: \"slate\",\n  },\n  component: BrowserChrome,\n  file: \"src/registry/items/browser-chrome.tsx\",\n  origin: {\n    prompt:\n      \"A browser window mock, drawn rather than screenshotted: traffic-light dots, one tab with a favicon square and a title, a rounded address bar with a padlock. Scale the window up gently from 0.96, type a URL into the bar character by character with a caret, then cross-fade the page area from a shimmering skeleton into a synthesised page — heading, two columns of text rules, one accent button. Keep every corner radius and hairline exact.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A browser window mock, drawn rather than screenshotted: traffic-light dots, one tab with a favicon square and a title, a rounded address bar with a padlock. Scale the window up gently from 0.96, type a URL into the bar character by character with a caret, then cross-fade the page area from a shimmering skeleton into a synthesised page — heading, two columns of text rules, one accent button. Keep every corner radius and hairline exact.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.5297,
      "props": [
        {
          "name": "url",
          "type": "string",
          "description": "The address typed into the omnibox, one character at a time. Everything before the first '/' is read as the host and set in full-strength ink; the path dims behind it, the way a real browser weights it. Leave the scheme off — browsers hide it. 20–45 characters reads best; the keystroke schedule fits itself to the length.",
          "required": false,
          "defaultValue": "motionregistry.dev/browser-chrome",
          "options": null
        },
        {
          "name": "tabTitle",
          "type": "string",
          "description": "Label in the active tab, beside a favicon square carrying its first letter. The tab is roughly a quarter of the window wide, so anything past ~22 characters ellipses.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "pageTitle",
          "type": "string",
          "description": "Display heading of the page that resolves out of the skeleton. Four to seven words; it sets across three-quarters of the page and wraps to a second line.",
          "required": false,
          "defaultValue": "Draw the chrome, not the screenshot",
          "options": null
        },
        {
          "name": "buttonLabel",
          "type": "string",
          "description": "Label of the single accent-filled button on the synthesised page. Two or three words — the pill sizes itself to the text.",
          "required": false,
          "defaultValue": "Copy component",
          "options": null
        },
        {
          "name": "secondaryLabel",
          "type": "string",
          "description": "Label of the hairline ghost button beside it. Empty string for a single button.",
          "required": false,
          "defaultValue": "View source",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small mono label above the window, led by an accent dash and aligned to the window's left edge. Empty string for none.",
          "required": false,
          "defaultValue": "Product & UI",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "One muted line under the window, aligned to the same left edge. Empty string for none.",
          "required": false,
          "defaultValue": "Chrome drawn in divs and inline SVG — no screenshot, no a…",
          "options": null
        },
        {
          "name": "trafficLights",
          "type": "\"mono\" | \"brand\"",
          "description": "'mono' draws the three window dots in muted ink, which is what a product shot usually wants; 'brand' colours them accent / alt accent / muted for a warmer, more literal frame.",
          "required": false,
          "defaultValue": "mono",
          "options": [
            "mono",
            "brand"
          ]
        },
        {
          "name": "lock",
          "type": "boolean",
          "description": "Draw the padlock at the head of the address bar. Turn it off for a localhost or intranet address, where a browser would not show one.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seeds the keystroke jitter and the ragged widths of the skeleton rows, which the text rules inherit so the cross-fade lands in register. Change it for a differently-set page of identical duration; the same seed always renders the same frames.",
          "required": false,
          "defaultValue": "chrome-01",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "caption-highlight-pill",
      "name": "Caption Highlight Pill",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "A spoken line with the figure that matters lifted onto a solid accent pill below it, landing a beat later",
      "tags": [
        "caption",
        "subtitle",
        "highlight",
        "pill",
        "explainer",
        "alpha",
        "lower third"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/caption-highlight-pill.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * A spoken line, with the part that matters lifted onto a solid accent pill.\n *\n * This is the workhorse caption of the explainer genre and it is doing something\n * more specific than \"highlighting a word\". The pill is a second line, set under\n * the first and larger — so the sentence reads normally and the figure reads on\n * its own, which is how someone half-watching still comes away with the number.\n *\n * The pill wipes rather than fades, for the same reason as any solid field: a\n * saturated block at half opacity looks like a rendering error. And it lands on\n * a beat *after* the line above it, because a figure that arrives at the same\n * time as its own sentence is a figure nobody reads.\n *\n * Rendered with `ground` off it carries an alpha channel, which is what makes it\n * usable as a burned-in caption over footage in any editor.\n */\n\nconst BUDGET = BUDGETS.statement;\n\nexport const captionHighlightPillSchema = z.object({\n  lead: z\n    .string()\n    .default(\"\")\n    .describe(\"The line above the pill, set in plain white. Empty gives you the pill on its own — the 'Link in the description' case.\"),\n  pill: z.string().describe(\"The words on the accent pill. A figure or a short imperative; this is the thing you want remembered.\"),\n  position: z\n    .enum([\"lower\", \"center\", \"upper\"])\n    .default(\"lower\")\n    .describe(\"Where the block sits. `lower` clears a presenter's face and stays above the platform's own caption area.\"),\n  ground: z\n    .boolean()\n    .default(true)\n    .describe(\"Paint the frame behind it. Turn it off to render with an alpha channel and lay the caption straight over your own footage.\"),\n  scale: z\n    .number()\n    .min(0.6)\n    .max(1.6)\n    .default(1)\n    .describe(\"Overall size multiplier, for when the caption has to sit inside a busy frame.\"),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof captionHighlightPillSchema>;\n\nexport const CaptionHighlightPill: React.FC<Props> = ({\n  lead,\n  pill,\n  position,\n  ground,\n  scale,\n  brand,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  const u = (Math.min(width, height) / 1080) * scale;\n\n  const leadIn = interpolate(stage.enter, [0, 0.62], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // The pill lands a beat late, and springs a little as it settles.\n  const wipe = interpolate(frame, [BUDGET.enter * 0.75, BUDGET.enter + 8], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const settle = spring({\n    frame: frame - BUDGET.enter - 2,\n    fps,\n    config: { damping: 14, mass: 0.5 },\n  });\n\n  const drift = Math.sin((frame / fps) * 0.5) * 4 * u;\n\n  const justify =\n    position === \"lower\" ? \"flex-end\" : position === \"upper\" ? \"flex-start\" : \"center\";\n  const pad = position === \"center\" ? 0 : height * 0.14;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: ground ? t.bg : \"transparent\" }}>\n      <AbsoluteFill\n        style={{\n          opacity: stage.presence,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: justify,\n          paddingTop: position === \"upper\" ? pad : 0,\n          paddingBottom: position === \"lower\" ? pad : 0,\n          gap: 14 * u,\n          transform: `translateY(${drift}px)`,\n        }}\n      >\n        {lead ? (\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontWeight: 500,\n              fontSize: 52 * u,\n              letterSpacing: \"-0.02em\",\n              color: t.ink,\n              textAlign: \"center\",\n              maxWidth: width * 0.8,\n              lineHeight: 1.2,\n              opacity: leadIn,\n              transform: `translateY(${(1 - leadIn) * 12 * u}px)`,\n            }}\n          >\n            {lead}\n          </div>\n        ) : null}\n\n        <div\n          style={{\n            backgroundColor: t.accent,\n            padding: `${10 * u}px ${26 * u}px`,\n            borderRadius: 10 * u,\n            clipPath: `inset(0 ${(1 - wipe) * 100}% 0 0)`,\n            transform: `scale(${interpolate(Math.max(0, settle), [0, 1], [0.97, 1])})`,\n          }}\n        >\n          <span\n            style={{\n              fontFamily: t.fontDisplay,\n              fontWeight: 500,\n              fontSize: 62 * u,\n              letterSpacing: \"-0.025em\",\n              color: t.accentInk,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {pill}\n          </span>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"caption-highlight-pill\",\n  name: \"Caption Highlight Pill\",\n  category: \"titles\",\n  summary:\n    \"A spoken line with the figure that matters lifted onto a solid accent pill below it, landing a beat later\",\n  tags: [\"caption\", \"subtitle\", \"highlight\", \"pill\", \"explainer\", \"alpha\", \"lower third\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: captionHighlightPillSchema,\n  defaults: {\n    lead: \"Adobe offered to buy Figma for\",\n    pill: \"$20 Billion\",\n    position: \"lower\",\n    ground: true,\n    scale: 1,\n    brand: \"ledger\",\n  },\n  component: CaptionHighlightPill,\n  file: \"src/registry/items/caption-highlight-pill.tsx\",\n  origin: {\n    prompt:\n      \"A two-part caption: a plain white sentence, and under it the figure that matters set larger on a solid accent pill. The sentence arrives first and the pill wipes open left to right a beat later with a small settle, so the number is read separately from its own sentence rather than at the same time. The ground can be switched off so the whole thing renders with an alpha channel and drops over footage in any editor.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A two-part caption: a plain white sentence, and under it the figure that matters set larger on a solid accent pill. The sentence arrives first and the pill wipes open left to right a beat later with a small settle, so the number is read separately from its own sentence rather than at the same time. The ground can be switched off so the whole thing renders with an alpha channel and drops over footage in any editor.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2333,
      "props": [
        {
          "name": "lead",
          "type": "string",
          "description": "The line above the pill, set in plain white. Empty gives you the pill on its own — the 'Link in the description' case.",
          "required": false,
          "defaultValue": "Adobe offered to buy Figma for",
          "options": null
        },
        {
          "name": "pill",
          "type": "string",
          "description": "The words on the accent pill. A figure or a short imperative; this is the thing you want remembered.",
          "required": true,
          "defaultValue": "$20 Billion",
          "options": null
        },
        {
          "name": "position",
          "type": "\"lower\" | \"center\" | \"upper\"",
          "description": "Where the block sits. `lower` clears a presenter's face and stays above the platform's own caption area.",
          "required": false,
          "defaultValue": "lower",
          "options": [
            "lower",
            "center",
            "upper"
          ]
        },
        {
          "name": "ground",
          "type": "boolean",
          "description": "Paint the frame behind it. Turn it off to render with an alpha channel and lay the caption straight over your own footage.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "scale",
          "type": "number",
          "description": "Overall size multiplier, for when the caption has to sit inside a busy frame.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "chapter-card",
      "name": "Chapter Card",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "A section divider where the rule runs off the frame edge, the index counts into place and the title masks up",
      "tags": [
        "chapter",
        "section divider",
        "title card",
        "odometer",
        "editorial",
        "rule"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/chapter-card.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const chapterCardSchema = z.object({\n  index: z\n    .number()\n    .int()\n    .min(0)\n    .max(99)\n    .describe(\"Chapter number. Rendered zero-padded ('3' becomes '03') and counted into place.\"),\n  title: z\n    .string()\n    .describe(\"Section title. Two to five words holds the line; longer titles wrap and drop a size.\"),\n  total: z\n    .number()\n    .int()\n    .min(1)\n    .max(99)\n    .optional()\n    .describe(\"How many chapters there are, shown as a small 'of 07' under the index. Omit if the run length is unknown.\"),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono label above the rule, e.g. 'Chapter', 'Part', 'Section'. Omit for a barer card.\"),\n  dek: z\n    .string()\n    .optional()\n    .describe(\"One supporting sentence under the title. Keep it to a single line of thought — this is a divider, not a slide.\"),\n  progress: z\n    .boolean()\n    .default(true)\n    .describe(\"Show tick marks for the whole run at the right end of the rule. Needs `total`, and is skipped above 12 chapters.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof chapterCardSchema>;\n\n/**\n * Chapter Card — a full-frame section divider.\n *\n * The composition is a left margin and one rule. Everything hangs off that\n * intersection: the kicker sits above the rule, the index and the title below\n * it, and the rule itself runs out past the right edge of the frame so the\n * card reads as a spread rather than a centred slide. Nothing is centred.\n *\n * Motion is three beats in one gesture: the rule draws left to right, the\n * index rolls up into place behind it, the title masks up last. Read at rest\n * it is a piece of print.\n */\nconst ChapterCard: React.FC<Props> = ({ index, title, total, kicker, dek, progress, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n\n  // One type unit for the whole card. Taking the larger of the width and a\n  // 16:9-equivalent of the height means a 9:16 or 4:5 cut scales the type up\n  // instead of rendering this composition at half size.\n  const unit = Math.max(width, height * 0.72);\n\n  const leftPad = width * 0.075;\n  // Text stops short of the right edge; the rule deliberately does not.\n  const rightInset = width * 0.075;\n\n  const words = title.split(/\\s+/).filter(Boolean);\n  const indexSize = unit * 0.135;\n  const titleSize = unit * (words.length > 4 ? 0.046 : 0.056);\n  const kickerSize = unit * 0.0135;\n  const ruleWeight = Math.max(1, unit * 0.0011);\n\n  // Zero-pad the index and the total to the same width, so 3 of 7 reads\n  // \"03\" over \"of 07\" and the two mono figures sit in one column.\n  const padTo = Math.max(2, total === undefined ? 2 : String(total).length);\n  const digits = String(Math.round(index)).padStart(padTo, \"0\").split(\"\");\n\n  const showTicks = progress && total !== undefined && total <= 12;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        // Optical lift: the index is heavy and low, so the block sits above\n        // the true centre of the frame.\n        paddingBottom: height * 0.055,\n      }}\n    >\n      <div style={{ opacity: stage.presence, paddingLeft: leftPad }}>\n        {kicker ? (\n          <Kicker\n            label={kicker}\n            color={t.inkMuted}\n            font={t.fontMono}\n            size={kickerSize}\n            gap={kickerSize * 1.05}\n          />\n        ) : null}\n\n        <Rule\n          weight={ruleWeight}\n          faint={t.rule}\n          accent={t.accent}\n          ticks={\n            showTicks && total !== undefined ? (\n              <Ticks\n                total={total}\n                current={index}\n                weight={ruleWeight}\n                unit={unit}\n                muted={t.inkMuted}\n                accent={t.accent}\n              />\n            ) : null\n          }\n          ticksInset={rightInset}\n        />\n\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"flex-start\",\n            gap: width * 0.045,\n            paddingRight: rightInset,\n            marginTop: indexSize * 0.11,\n          }}\n        >\n          <div style={{ flexShrink: 0, minWidth: digits.length * indexSize * 0.62 }}>\n            <Index digits={digits} size={indexSize} color={t.accent} font={t.fontMono} />\n            {total !== undefined ? (\n              <OfTotal\n                total={String(total).padStart(padTo, \"0\")}\n                size={indexSize * 0.115}\n                color={t.inkMuted}\n                font={t.fontMono}\n                gap={indexSize * 0.055}\n              />\n            ) : null}\n          </div>\n\n          <div\n            style={{\n              minWidth: 0,\n              // Cap-height alignment. A mono digit sits 0.125em down inside its\n              // roll cell and an Archivo cap sits 0.13em down inside its line\n              // box, so this offset lands the two tops flush.\n              paddingTop: indexSize * 0.125 - titleSize * 0.13,\n            }}\n          >\n            <div\n              style={{\n                display: \"flex\",\n                flexWrap: \"wrap\",\n                gap: `0 ${titleSize * 0.26}px`,\n              }}\n            >\n              {words.map((word, i) => (\n                <TitleWord\n                  key={`${word}-${i}`}\n                  word={word}\n                  index={i}\n                  size={titleSize}\n                  color={t.ink}\n                  font={t.fontDisplay}\n                />\n              ))}\n            </div>\n\n            {dek ? (\n              <Dek\n                text={dek}\n                size={unit * 0.0165}\n                color={t.inkMuted}\n                font={t.fontBody}\n                gap={titleSize * 0.42}\n                maxWidth={unit * 0.33}\n              />\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Kicker: React.FC<{\n  label: string;\n  color: string;\n  font: string;\n  size: number;\n  gap: number;\n}> = ({ label, color, font, size, gap }) => {\n  const stage = useStage(BUDGETS.statement);\n  // Arrives from the left, like the rule it introduces.\n  const x = interpolate(stage.enter, [0, 1], [-size * 1.2, 0]);\n  return (\n    <div\n      style={{\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 600,\n        letterSpacing: size * 0.2,\n        textTransform: \"uppercase\",\n        color,\n        marginBottom: gap,\n        transform: `translateX(${x}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      {label}\n    </div>\n  );\n};\n\n/**\n * The rule. Two hairlines on one baseline: a short accent lead-in that snaps\n * out of the left margin, and the long faint run that follows it off the right\n * edge of the frame. Drawing the accent first gives the gesture a direction.\n */\nconst Rule: React.FC<{\n  weight: number;\n  faint: string;\n  accent: string;\n  ticks: React.ReactNode;\n  ticksInset: number;\n}> = ({ weight, faint, accent, ticks, ticksInset }) => {\n  const stage = useStage(BUDGETS.statement);\n  const lead = interpolate(stage.frame, [0, 9], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const run = interpolate(stage.frame, [1, 19], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <div style={{ position: \"relative\", height: weight, width: \"100%\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          backgroundColor: faint,\n          transform: `scaleX(${run})`,\n          transformOrigin: \"left\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          height: weight,\n          width: \"13%\",\n          backgroundColor: accent,\n          transform: `scaleX(${lead})`,\n          transformOrigin: \"left\",\n        }}\n      />\n      {ticks ? (\n        <div\n          style={{\n            position: \"absolute\",\n            right: ticksInset,\n            bottom: weight,\n          }}\n        >\n          {ticks}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/**\n * Run-length ticks, one per chapter, revealed by growing up out of the rule\n * once its leading edge has passed them. The current chapter is the tall\n * accent mark; chapters already behind you are solid, the ones ahead are dim.\n */\nconst Ticks: React.FC<{\n  total: number;\n  current: number;\n  weight: number;\n  unit: number;\n  muted: string;\n  accent: string;\n}> = ({ total, current, weight, unit, muted, accent }) => {\n  const stage = useStage(BUDGETS.statement);\n  const reveal = interpolate(stage.frame, [13, 23], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const tall = unit * 0.024;\n  const short = unit * 0.011;\n\n  return (\n    <div style={{ height: tall, overflow: \"hidden\" }}>\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"flex-end\",\n          gap: unit * 0.009,\n          height: tall,\n          transform: `translateY(${(1 - reveal) * 100}%)`,\n        }}\n      >\n        {Array.from({ length: total }, (_, i) => {\n          const isCurrent = i + 1 === current;\n          return (\n            <div\n              key={i}\n              style={{\n                width: weight,\n                height: isCurrent ? tall : short,\n                backgroundColor: isCurrent ? accent : muted,\n                opacity: isCurrent ? 1 : i + 1 < current ? 0.75 : 0.3,\n              }}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\n/** The index, as a row of odometer digits. */\nconst Index: React.FC<{\n  digits: string[];\n  size: number;\n  color: string;\n  font: string;\n}> = ({ digits, size, color, font }) => (\n  <div\n    style={{\n      display: \"flex\",\n      // Mono digits are centred in a fixed advance cell, so their ink starts\n      // inboard of the margin. A small pull left sets the numeral optically\n      // flush with the rule above it rather than boxed off it.\n      marginLeft: -size * 0.045,\n    }}\n  >\n    {digits.map((digit, i) => (\n      <DigitRoll key={i} digit={Number(digit)} index={i} size={size} color={color} font={font} />\n    ))}\n  </div>\n);\n\nconst ROLL_CELLS = 20; // 0–9 twice: enough strip to roll a full decade and land.\n\n/**\n * One digit, rolling up through a full decade into its final value. The strip\n * is real stacked glyphs behind an overflow mask, so the count stays crisp at\n * 4K instead of blurring the way a scaled bitmap would. `EASING.enter` does\n * the work: most of the decade is spent in the first three frames, then it\n * decelerates onto the number and stops — a counter, not a slot machine.\n */\nconst DigitRoll: React.FC<{\n  digit: number;\n  index: number;\n  size: number;\n  color: string;\n  font: string;\n}> = ({ digit, index, size, color, font }) => {\n  const stage = useStage(BUDGETS.statement);\n  const cell = size * 0.95;\n  const start = 1 + index * 3; // leading digits land first, as an odometer does\n  const p = interpolate(stage.frame, [start, start + 15], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const y = -p * (10 + digit) * cell;\n\n  return (\n    <div style={{ width: size * 0.62, height: cell, overflow: \"hidden\" }}>\n      <div style={{ transform: `translateY(${y}px)` }}>\n        {Array.from({ length: ROLL_CELLS }, (_, n) => (\n          <div\n            key={n}\n            style={{\n              height: cell,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              fontFamily: font,\n              fontSize: size,\n              fontWeight: 500,\n              lineHeight: 1,\n              fontVariantNumeric: \"tabular-nums\",\n              color,\n            }}\n          >\n            {n % 10}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nconst OfTotal: React.FC<{\n  total: string;\n  size: number;\n  color: string;\n  font: string;\n  gap: number;\n}> = ({ total, size, color, font, gap }) => {\n  // Last thing in, once the count has settled.\n  const stage = useStaggeredStage(BUDGETS.statement, 5, 3);\n  return (\n    <div\n      style={{\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 500,\n        letterSpacing: size * 0.05,\n        color,\n        marginTop: gap,\n        opacity: stage.enter,\n      }}\n    >\n      {`of ${total}`}\n    </div>\n  );\n};\n\n/**\n * One word of the title, rising through a hard clipping mask. Offsetting the\n * stagger index by two keeps the title behind the rule, so the frame builds\n * in one direction: rule, number, name.\n */\nconst TitleWord: React.FC<{\n  word: string;\n  index: number;\n  size: number;\n  color: string;\n  font: string;\n}> = ({ word, index, size, color, font }) => {\n  const stage = useStaggeredStage(BUDGETS.statement, index + 2, 2);\n  const y = interpolate(stage.enter, [0, 1], [100, 0]);\n  return (\n    <span style={{ display: \"block\", overflow: \"hidden\", paddingBottom: size * 0.07 }}>\n      <span\n        style={{\n          display: \"block\",\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 700,\n          lineHeight: 1.03,\n          letterSpacing: size * -0.022,\n          color,\n          transform: `translateY(${y}%)`,\n        }}\n      >\n        {word}\n      </span>\n    </span>\n  );\n};\n\nconst Dek: React.FC<{\n  text: string;\n  size: number;\n  color: string;\n  font: string;\n  gap: number;\n  maxWidth: number;\n}> = ({ text, size, color, font, gap, maxWidth }) => {\n  const stage = useStaggeredStage(BUDGETS.statement, 7, 2);\n  const y = interpolate(stage.enter, [0, 1], [size * 0.7, 0]);\n  return (\n    <div\n      style={{\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 400,\n        lineHeight: 1.45,\n        color,\n        marginTop: gap,\n        maxWidth,\n        transform: `translateY(${y}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      {text}\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"chapter-card\",\n  name: \"Chapter Card\",\n  category: \"titles\",\n  summary: \"A section divider where the rule runs off the frame edge, the index counts into place and the title masks up\",\n  tags: [\"chapter\", \"section divider\", \"title card\", \"odometer\", \"editorial\", \"rule\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: chapterCardSchema,\n  defaults: {\n    index: 3,\n    title: \"The Grammar of Time\",\n    total: 7,\n    kicker: \"Chapter\",\n    dek: \"Two beautiful scenes back to back still look wrong if their curves disagree.\",\n    progress: true,\n    brand: \"slate\",\n  },\n  component: ChapterCard,\n  file: \"src/registry/items/chapter-card.tsx\",\n  origin: {\n    prompt:\n      \"A full-frame chapter divider, left-weighted and editorial: a big mono index number in the accent colour that counts into place, a hairline rule that draws across and runs off the right edge of the frame, and the section title masking up beside the number in the display face. Small 'of 07' underneath.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A full-frame chapter divider, left-weighted and editorial: a big mono index number in the accent colour that counts into place, a hairline rule that draws across and runs off the right edge of the frame, and the section title masking up beside the number in the display face. Small 'of 07' underneath.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2649,
      "props": [
        {
          "name": "index",
          "type": "number",
          "description": "Chapter number. Rendered zero-padded ('3' becomes '03') and counted into place.",
          "required": true,
          "defaultValue": "3",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "Section title. Two to five words holds the line; longer titles wrap and drop a size.",
          "required": true,
          "defaultValue": "The Grammar of Time",
          "options": null
        },
        {
          "name": "total",
          "type": "number",
          "description": "How many chapters there are, shown as a small 'of 07' under the index. Omit if the run length is unknown.",
          "required": false,
          "defaultValue": "7",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "Small uppercase mono label above the rule, e.g. 'Chapter', 'Part', 'Section'. Omit for a barer card.",
          "required": false,
          "defaultValue": "Chapter",
          "options": null
        },
        {
          "name": "dek",
          "type": "string",
          "description": "One supporting sentence under the title. Keep it to a single line of thought — this is a divider, not a slide.",
          "required": false,
          "defaultValue": "Two beautiful scenes back to back still look wrong if the…",
          "options": null
        },
        {
          "name": "progress",
          "type": "boolean",
          "description": "Show tick marks for the whole run at the right end of the rule. Needs `total`, and is skipped above 12 chapters.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "checklist-tick",
      "name": "Checklist Tick",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "The whole list arrives first to set the scope, then each item ticks in turn with the one overshoot this library allows",
      "tags": [
        "diagram",
        "checklist",
        "tick",
        "progress",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/checklist-tick.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { evolvePath } from \"@remotion/paths\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const checklistTickSchema = z.object({\n  title: z.string().default(\"Ships with\").describe(\"Mono label above the list.\"),\n  items: z\n    .array(z.string())\n    .min(2)\n    .max(8)\n    .describe(\"The list. Four to six items reads best at 16:9.\"),\n  strike: z\n    .boolean()\n    .default(false)\n    .describe(\"Strike through each item as it ticks. Use for a 'no longer needed' list.\"),\n  perItem: z\n    .number()\n    .min(6)\n    .max(30)\n    .default(13)\n    .describe(\"Frames between ticks. Below ~10 the sequence stops reading as individual beats.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof checklistTickSchema>;\n\n/**\n * Rows arrive first as a group, then tick one at a time.\n *\n * Separating those two phases matters: if each row appeared and ticked together\n * the viewer would read it as five unrelated events, whereas showing the whole\n * list first establishes the scope and then the ticks become progress against it.\n * The tick itself is the one place in this library where a small overshoot is\n * earned — it is the moment of satisfaction the shot exists for.\n */\nconst TICK_PATH = \"M 5 12.5 L 10 17.5 L 19 7\";\n\nconst ChecklistTick: React.FC<Props> = ({ title, items, strike, perItem, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const rowH = Math.min(height * 0.11, (height * 0.62) / items.length);\n  const firstTick = 26;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        paddingLeft: width * 0.16,\n        opacity: stage.presence,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          marginBottom: height * 0.05,\n        }}\n      >\n        {title}\n      </div>\n\n      {items.map((label, i) => (\n        <Row\n          key={label}\n          label={label}\n          index={i}\n          rowH={rowH}\n          width={width}\n          tickAt={firstTick + i * perItem}\n          strike={strike}\n          tokens={t}\n        />\n      ))}\n    </AbsoluteFill>\n  );\n};\n\nconst Row: React.FC<{\n  label: string;\n  index: number;\n  rowH: number;\n  width: number;\n  tickAt: number;\n  strike: boolean;\n  tokens: BrandTokens;\n}> = ({ label, index, rowH, width, tickAt, strike, tokens }) => {\n  const arrive = useStaggeredStage(BUDGETS.feature, index, 3);\n  const stage = useStage(BUDGETS.feature);\n\n  const tick = interpolate(stage.frame, [tickAt, tickAt + 11], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  // The earned overshoot: the box pops slightly as it fills.\n  const pop = interpolate(stage.frame, [tickAt, tickAt + 5, tickAt + 12], [1, 1.14, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n\n  const box = rowH * 0.5;\n  const ev = evolvePath(tick, TICK_PATH);\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: width * 0.018,\n        height: rowH,\n        opacity: arrive.enter,\n        transform: `translateX(${(1 - arrive.enter) * width * 0.012}px)`,\n      }}\n    >\n      <div\n        style={{\n          width: box,\n          height: box,\n          borderRadius: \"50%\",\n          flexShrink: 0,\n          border: `${Math.max(1.5, box * 0.055)}px solid ${tick > 0.02 ? tokens.accent : tokens.rule}`,\n          backgroundColor: tick > 0.02 ? tokens.accent : \"transparent\",\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          transform: `scale(${pop})`,\n        }}\n      >\n        <svg width={box * 0.66} height={box * 0.66} viewBox=\"0 0 24 24\">\n          <path\n            d={TICK_PATH}\n            fill=\"none\"\n            stroke={tokens.accentInk}\n            strokeWidth={3.4}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeDasharray={ev.strokeDasharray}\n            strokeDashoffset={ev.strokeDashoffset}\n          />\n        </svg>\n      </div>\n\n      <div style={{ position: \"relative\" }}>\n        <span\n          style={{\n            fontFamily: tokens.fontDisplay,\n            fontSize: rowH * 0.44,\n            fontWeight: 600,\n            letterSpacing: rowH * -0.004,\n            color: tick > 0.5 ? tokens.ink : tokens.inkMuted,\n          }}\n        >\n          {label}\n        </span>\n        {strike ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: \"52%\",\n              height: Math.max(2, rowH * 0.028),\n              width: `${tick * 100}%`,\n              backgroundColor: tokens.inkMuted,\n            }}\n          />\n        ) : null}\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"checklist-tick\",\n  name: \"Checklist Tick\",\n  category: \"diagram\",\n  summary:\n    \"The whole list arrives first to set the scope, then each item ticks in turn with the one overshoot this library allows\",\n  tags: [\"diagram\", \"checklist\", \"tick\", \"progress\", \"explainer\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: checklistTickSchema,\n  defaults: {\n    title: \"Ships with\",\n    items: [\n      \"Typed props, validated before render\",\n      \"A shared timing grammar\",\n      \"Brand tokens for retargeting\",\n      \"One file, zero setup\",\n      \"An MCP server for agents\",\n    ],\n    strike: false,\n    perItem: 13,\n    brand: \"slate\",\n  },\n  component: ChecklistTick,\n  file: \"src/registry/items/checklist-tick.tsx\",\n  origin: {\n    prompt:\n      \"A checklist where items fade up as a group, then each checkbox fills with the accent colour and a tick draws inside it one after another, with a small satisfying overshoot.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A checklist where items fade up as a group, then each checkbox fills with the accent colour and a tick draws inside it one after another, with a small satisfying overshoot.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2683,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Mono label above the list.",
          "required": false,
          "defaultValue": "Ships with",
          "options": null
        },
        {
          "name": "items",
          "type": "array",
          "description": "The list. Four to six items reads best at 16:9.",
          "required": true,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "strike",
          "type": "boolean",
          "description": "Strike through each item as it ticks. Use for a 'no longer needed' list.",
          "required": false,
          "defaultValue": "false",
          "options": null
        },
        {
          "name": "perItem",
          "type": "number",
          "description": "Frames between ticks. Below ~10 the sequence stops reading as individual beats.",
          "required": false,
          "defaultValue": "13",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "column-pair-chart",
      "name": "Column Pair Chart",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Two small column groups grow from one dashed baseline that runs unbroken across both, so the comparison is on a single scale",
      "tags": [
        "bar chart",
        "comparison",
        "columns",
        "baseline",
        "finance",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/column-pair-chart.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * Two small column charts side by side, sharing one dashed baseline.\n *\n * The comparison only works because the baseline runs unbroken across both\n * groups: it is the single line that says these two are measured on the same\n * scale, and without it the eye reads two unrelated charts that happen to be\n * near each other. So it is drawn first, all the way across, before either group\n * exists.\n *\n * Columns are square-cornered and flat — no gradient, no rounding, no shadow. At\n * this size any of those turn six bars into six objects, and the shape of the\n * series is the only thing anyone is meant to read.\n */\n\nconst BUDGET = BUDGETS.scene;\n\nconst groupSchema = z.object({\n  title: z.string().describe(\"Caption under the group. What this series is.\"),\n  values: z\n    .array(z.number())\n    .min(2)\n    .max(12)\n    .describe(\"Bar heights. Both groups share one scale, so the numbers must be comparable — that is the entire point of putting them side by side.\"),\n  labels: z\n    .array(z.string())\n    .default([])\n    .describe(\"Tick label per bar, under the baseline. Three or four characters each.\"),\n  tone: z\n    .enum([\"accent\", \"alt\"])\n    .default(\"accent\")\n    .describe(\"`accent` for the series that is fine, `alt` for the one that is not. The pairing is usually the argument.\"),\n});\n\nexport const columnPairChartSchema = z.object({\n  groups: z\n    .array(groupSchema)\n    .min(1)\n    .max(2)\n    .describe(\"One group, or two for a comparison. Two is what this component is for.\"),\n  scaleMax: z\n    .number()\n    .default(0)\n    .describe(\"Top of the shared scale. 0 derives it from the largest value across both groups, which is almost always right.\"),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof columnPairChartSchema>;\n\nexport const ColumnPairChart: React.FC<Props> = ({ groups, scaleMax, brand }) => {\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  const u = Math.min(width, height) / 1080;\n  const baseY = height * 0.56;\n  const plotH = height * 0.34;\n\n  const push = 1 + 0.05 * stage.progress;\n\n  const max = scaleMax > 0 ? scaleMax : Math.max(...groups.flatMap((g) => g.values), 1);\n\n  // The baseline draws across the whole frame before anything stands on it.\n  const rule = interpolate(stage.enter, [0, 0.55], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const inset = width * 0.13;\n  const span = width - inset * 2;\n  const groupW = span / groups.length;\n\n  let barIndex = 0;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill\n        style={{\n          opacity: stage.presence,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 55%\",\n        }}\n      >\n        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>\n          <line\n            x1={inset}\n            y1={baseY}\n            x2={inset + span * rule}\n            y2={baseY}\n            stroke={t.inkMuted}\n            strokeWidth={Math.max(1, 1.4 * u)}\n            strokeDasharray={`${7 * u} ${7 * u}`}\n            opacity={0.65}\n          />\n\n          {groups.map((g, gi) => {\n            const gx = inset + groupW * gi;\n            const inner = groupW * 0.74;\n            const slot = inner / g.values.length;\n            const barW = slot * 0.58;\n            const startX = gx + (groupW - inner) / 2;\n            const colour = g.tone === \"alt\" ? t.accentAlt : t.accent;\n\n            return (\n              <g key={g.title + gi}>\n                {g.values.map((v, i) => {\n                  const idx = barIndex++;\n                  return (\n                    <Column\n                      key={i}\n                      index={idx}\n                      x={startX + slot * i + (slot - barW) / 2}\n                      w={barW}\n                      baseY={baseY}\n                      h={(v / max) * plotH}\n                      colour={colour}\n                      label={g.labels[i] ?? \"\"}\n                      u={u}\n                      t={t}\n                    />\n                  );\n                })}\n                <text\n                  x={gx + groupW / 2}\n                  y={baseY + 100 * u}\n                  textAnchor=\"middle\"\n                  fill={t.ink}\n                  fontFamily={t.fontDisplay}\n                  fontWeight={500}\n                  fontSize={32 * u}\n                >\n                  {g.title}\n                </text>\n              </g>\n            );\n          })}\n        </svg>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nconst Column: React.FC<{\n  index: number;\n  x: number;\n  w: number;\n  baseY: number;\n  h: number;\n  colour: string;\n  label: string;\n  u: number;\n  t: (typeof BRANDS)[string];\n}> = ({ index, x, w, baseY, h, colour, label, u, t }) => {\n  const st = useStaggeredStage(BUDGET, index + 2, 3);\n  const grown = h * st.enter;\n  return (\n    <g>\n      <rect x={x} y={baseY - grown} width={w} height={grown} fill={colour} />\n      {label ? (\n        <text\n          x={x + w / 2}\n          y={baseY + 30 * u}\n          textAnchor=\"middle\"\n          dominantBaseline=\"hanging\"\n          fill={t.ink}\n          fontFamily={t.fontBody}\n          fontSize={22 * u}\n          opacity={st.enter}\n        >\n          {label}\n        </text>\n      ) : null}\n    </g>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"column-pair-chart\",\n  name: \"Column Pair Chart\",\n  category: \"data\",\n  summary:\n    \"Two small column groups grow from one dashed baseline that runs unbroken across both, so the comparison is on a single scale\",\n  tags: [\"bar chart\", \"comparison\", \"columns\", \"baseline\", \"finance\", \"explainer\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: columnPairChartSchema,\n  defaults: {\n    groups: [\n      {\n        title: \"Salesperson's Cost\",\n        values: [52, 53, 54, 55, 57, 58],\n        labels: [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n        tone: \"accent\",\n      },\n      {\n        title: \"Inference Bill\",\n        values: [30, 26, 29, 96, 95, 94],\n        labels: [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\"],\n        tone: \"alt\",\n      },\n    ],\n    scaleMax: 0,\n    brand: \"ledger\",\n  },\n  component: ColumnPairChart,\n  file: \"src/registry/items/column-pair-chart.tsx\",\n  origin: {\n    prompt:\n      \"Two small column charts side by side sharing one dashed baseline that draws all the way across the frame before either group appears — the unbroken line is what says both are on the same scale. Bars are flat and square-cornered with no gradient or rounding, growing up from the baseline one after another with their month labels fading in beneath. One group takes the accent colour and the other the alternate, because the pairing is usually the argument.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Two small column charts side by side sharing one dashed baseline that draws all the way across the frame before either group appears — the unbroken line is what says both are on the same scale. Bars are flat and square-cornered with no gradient or rounding, growing up from the baseline one after another with their month labels fading in beneath. One group takes the accent colour and the other the alternate, because the pairing is usually the argument.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.5452,
      "props": [
        {
          "name": "groups",
          "type": "array",
          "description": "One group, or two for a comparison. Two is what this component is for.",
          "required": true,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "scaleMax",
          "type": "number",
          "description": "Top of the shared scale. 0 derives it from the largest value across both groups, which is almost always right.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "company-scorecard",
      "name": "Company Scorecard",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Two subjects, a row each, built badge by badge across a cross divider so the healthy row lands before the other one undercuts it",
      "tags": [
        "comparison",
        "scorecard",
        "badges",
        "ring",
        "icons",
        "finance",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/company-scorecard.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * Two companies, one row each, built badge by badge.\n *\n * The layout is a cross: a vertical rule separating who from what, and a\n * horizontal rule separating the two subjects. That is what lets the reader\n * compare downward — 91% against 2%, a shield against a money bag — without\n * either row being labelled \"good\" or \"bad\" in words. The colour does that job,\n * and it is the only place in this component where colour carries meaning.\n *\n * Badges build in reading order across the top row and then across the bottom,\n * never both at once, because the whole rhetorical move is letting the first row\n * land before the second one undercuts it.\n */\n\nconst BUDGET = BUDGETS.feature;\n\nconst badgeSchema = z.object({\n  kind: z\n    .enum([\"ring\", \"shield\", \"audience\", \"decline\", \"bag\", \"stack\"])\n    .describe(\n      \"Which mark to draw. `ring` shows a percentage; the rest are glyphs — a shield for safety, an audience for reach, a falling chart, a money bag, a stack for commitments.\",\n    ),\n  value: z\n    .string()\n    .default(\"\")\n    .describe(\"Text inside the mark. Only `ring` uses it, as a percentage.\"),\n  percent: z.number().min(0).max(100).default(0).describe(\"Arc fill for `ring` badges.\"),\n  caption: z.string().describe(\"Two or three words under the mark. Wraps to a second line.\"),\n});\n\nconst rowSchema = z.object({\n  name: z.string().describe(\"The subject's initials or short name, drawn in the tile.\"),\n  note: z.string().default(\"\").describe(\"Small pill under the tile — usually the headline figure.\"),\n  tone: z\n    .enum([\"accent\", \"alt\"])\n    .default(\"accent\")\n    .describe(\"`accent` for the row that looks healthy, `alt` for the one that does not.\"),\n  badges: z.array(badgeSchema).min(1).max(4).describe(\"Up to four badges. Four fills a 16:9 frame exactly.\"),\n});\n\nexport const companyScorecardSchema = z.object({\n  rows: z.array(rowSchema).min(1).max(2).describe(\"One row, or two for the comparison this is built for.\"),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof companyScorecardSchema>;\n\nexport const CompanyScorecard: React.FC<Props> = ({ rows, brand }) => {\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  const u = Math.min(width, height) / 1080;\n  const railX = width * 0.29;\n  const midY = height / 2;\n\n  // The rules draw themselves first — the frame before the content.\n  const rule = interpolate(stage.enter, [0, 0.6], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const push = 1 + 0.038 * stage.progress;\n\n  let badgeIndex = 0;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill\n        style={{\n          opacity: stage.presence,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n        }}\n      >\n        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>\n          <line\n            x1={railX}\n            y1={height * 0.11}\n            x2={railX}\n            y2={height * 0.11 + (height * 0.78) * rule}\n            stroke={t.rule}\n            strokeWidth={Math.max(1, 1.4 * u)}\n          />\n          {rows.length > 1 ? (\n            <line\n              x1={width * 0.11}\n              y1={midY}\n              x2={width * 0.11 + (width * 0.78) * rule}\n              y2={midY}\n              stroke={t.rule}\n              strokeWidth={Math.max(1, 1.4 * u)}\n            />\n          ) : null}\n        </svg>\n\n        {rows.map((row, ri) => {\n          const colour = row.tone === \"alt\" ? t.accentAlt : t.accent;\n          const top = rows.length > 1 ? (ri === 0 ? height * 0.1 : midY + height * 0.03) : height * 0.28;\n          const rowH = rows.length > 1 ? height * 0.37 : height * 0.44;\n\n          return (\n            <div\n              key={row.name + ri}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                right: 0,\n                top,\n                height: rowH,\n                display: \"flex\",\n                alignItems: \"center\",\n              }}\n            >\n              <Tile row={row} railX={railX} width={width} u={u} t={t} colour={colour} index={ri} />\n\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: railX + width * 0.045,\n                  right: width * 0.05,\n                  display: \"flex\",\n                  alignItems: \"flex-start\",\n                  justifyContent: \"space-between\",\n                  gap: width * 0.02,\n                }}\n              >\n                {row.badges.map((b, bi) => (\n                  <Badge\n                    key={b.caption + bi}\n                    index={badgeIndex++}\n                    badge={b}\n                    colour={colour}\n                    u={u}\n                    t={t}\n                  />\n                ))}\n              </div>\n            </div>\n          );\n        })}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nconst Tile: React.FC<{\n  row: z.infer<typeof rowSchema>;\n  railX: number;\n  width: number;\n  u: number;\n  t: (typeof BRANDS)[string];\n  colour: string;\n  index: number;\n}> = ({ row, railX, width, u, t, colour, index }) => {\n  const st = useStaggeredStage(BUDGET, index, 8);\n  const size = 132 * u;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: railX - width * 0.045 - size,\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        opacity: st.enter,\n        transform: `translateY(${(1 - st.enter) * 14 * u}px)`,\n      }}\n    >\n      <div\n        style={{\n          width: size,\n          height: size,\n          borderRadius: 14 * u,\n          backgroundColor: t.surface,\n          border: `${Math.max(1, 1.4 * u)}px solid ${t.rule}`,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: t.fontDisplay,\n          fontWeight: 600,\n          fontSize: 44 * u,\n          color: t.ink,\n          letterSpacing: \"-0.02em\",\n        }}\n      >\n        {row.name}\n      </div>\n      {row.note ? (\n        <div\n          style={{\n            marginTop: -12 * u,\n            backgroundColor: t.bg,\n            border: `${Math.max(1, 1.2 * u)}px solid ${colour}`,\n            borderRadius: 7 * u,\n            padding: `${5 * u}px ${13 * u}px`,\n            fontFamily: t.fontBody,\n            fontSize: 19 * u,\n            color: t.ink,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {row.note}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\nconst Badge: React.FC<{\n  index: number;\n  badge: z.infer<typeof badgeSchema>;\n  colour: string;\n  u: number;\n  t: (typeof BRANDS)[string];\n}> = ({ index, badge, colour, u, t }) => {\n  const st = useStaggeredStage(BUDGET, index + 2, 7);\n  const s = 128 * u;\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        gap: 14 * u,\n        opacity: st.enter,\n        transform: `translateY(${(1 - st.enter) * 16 * u}px)`,\n        width: s * 1.5,\n      }}\n    >\n      <svg width={s} height={s} viewBox=\"0 0 100 100\">\n        <Mark badge={badge} colour={colour} track={t.surface} ink={t.ink} sweep={st.enter} />\n      </svg>\n      <span\n        style={{\n          fontFamily: t.fontBody,\n          fontSize: 24 * u,\n          lineHeight: 1.22,\n          color: t.ink,\n          textAlign: \"center\",\n        }}\n      >\n        {badge.caption}\n      </span>\n    </div>\n  );\n};\n\n/** The five glyphs, all drawn on a 100x100 grid so they sit on one baseline. */\nconst Mark: React.FC<{\n  badge: z.infer<typeof badgeSchema>;\n  colour: string;\n  track: string;\n  ink: string;\n  sweep: number;\n}> = ({ badge, colour, track, ink, sweep }) => {\n  if (badge.kind === \"ring\") {\n    const r = 38;\n    const c = 2 * Math.PI * r;\n    return (\n      <g>\n        <circle cx=\"50\" cy=\"50\" r={r} fill=\"none\" stroke={track} strokeWidth=\"11\" />\n        <circle\n          cx=\"50\"\n          cy=\"50\"\n          r={r}\n          fill=\"none\"\n          stroke={colour}\n          strokeWidth=\"11\"\n          strokeLinecap=\"round\"\n          strokeDasharray={c}\n          strokeDashoffset={c * (1 - (badge.percent / 100) * sweep)}\n          transform=\"rotate(-90 50 50)\"\n        />\n        <text\n          x=\"50\"\n          y=\"50\"\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          fill={ink}\n          fontSize=\"22\"\n          fontWeight=\"500\"\n        >\n          {badge.value}\n        </text>\n      </g>\n    );\n  }\n\n  if (badge.kind === \"shield\") {\n    return (\n      <g fill=\"none\" stroke={colour} strokeWidth=\"4.5\" strokeLinejoin=\"round\" strokeLinecap=\"round\">\n        <path d=\"M50 12 L82 26 v26 c0 20 -18 30 -32 36 -14 -6 -32 -16 -32 -36 V26 Z\" />\n        <path d=\"M36 50 l10 11 20 -22\" />\n      </g>\n    );\n  }\n\n  if (badge.kind === \"audience\") {\n    return (\n      <g stroke={colour} strokeWidth=\"4.5\" fill=\"none\" strokeLinecap=\"round\">\n        <circle cx=\"50\" cy=\"50\" r=\"38\" />\n        <circle cx=\"50\" cy=\"42\" r=\"8\" fill={colour} stroke=\"none\" />\n        <circle cx=\"33\" cy=\"47\" r=\"6.5\" fill={colour} stroke=\"none\" />\n        <circle cx=\"67\" cy=\"47\" r=\"6.5\" fill={colour} stroke=\"none\" />\n        <path d=\"M36 66 a14 14 0 0 1 28 0\" fill={colour} stroke=\"none\" />\n        <path d=\"M22 66 a12 12 0 0 1 12 -10\" />\n        <path d=\"M78 66 a12 12 0 0 0 -12 -10\" />\n      </g>\n    );\n  }\n\n  if (badge.kind === \"decline\") {\n    return (\n      <g stroke={colour} strokeWidth=\"5\" fill=\"none\" strokeLinecap=\"round\" strokeLinejoin=\"round\">\n        <path d=\"M14 20 v62 h72\" />\n        <path d=\"M24 34 L46 56 L62 44 L84 68\" />\n        <path d=\"M84 50 v18 h-18\" />\n      </g>\n    );\n  }\n\n  if (badge.kind === \"bag\") {\n    return (\n      <g>\n        <path\n          d=\"M38 24 h24 l-6 12 h-12 Z M32 36 h36 c10 12 14 26 14 34 0 14 -14 22 -32 22 s-32 -8 -32 -22c0 -8 4 -22 14 -34 Z\"\n          fill={colour}\n          opacity=\"0.9\"\n        />\n        <text\n          x=\"50\"\n          y=\"70\"\n          textAnchor=\"middle\"\n          dominantBaseline=\"central\"\n          fill=\"#FFFFFF\"\n          fontSize=\"30\"\n          fontWeight=\"600\"\n        >\n          $\n        </text>\n      </g>\n    );\n  }\n\n  // stack\n  return (\n    <g stroke={colour} strokeWidth=\"4.5\" fill=\"none\" strokeLinecap=\"round\">\n      <rect x=\"18\" y=\"22\" width=\"64\" height=\"18\" rx=\"4\" />\n      <rect x=\"18\" y=\"46\" width=\"64\" height=\"18\" rx=\"4\" />\n      <rect x=\"18\" y=\"70\" width=\"64\" height=\"12\" rx=\"4\" />\n      <circle cx=\"30\" cy=\"31\" r=\"2.6\" fill={colour} />\n      <circle cx=\"30\" cy=\"55\" r=\"2.6\" fill={colour} />\n    </g>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"company-scorecard\",\n  name: \"Company Scorecard\",\n  category: \"data\",\n  summary:\n    \"Two subjects, a row each, built badge by badge across a cross divider so the healthy row lands before the other one undercuts it\",\n  tags: [\"comparison\", \"scorecard\", \"badges\", \"ring\", \"icons\", \"finance\", \"explainer\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: companyScorecardSchema,\n  defaults: {\n    rows: [\n      {\n        name: \"FG\",\n        note: \"$750 Million\",\n        tone: \"accent\",\n        badges: [\n          { kind: \"ring\", value: \"91%\", percent: 91, caption: \"Gross Margins\" },\n          { kind: \"ring\", value: \"28%\", percent: 28, caption: \"Free Cash Flow Margins\" },\n          { kind: \"shield\", value: \"\", percent: 0, caption: \"~$0 Debt\" },\n          { kind: \"audience\", value: \"\", percent: 0, caption: \"Massive Customer Base\" },\n        ],\n      },\n      {\n        name: \"CW\",\n        note: \"$1.9 Billion\",\n        tone: \"alt\",\n        badges: [\n          { kind: \"ring\", value: \"2%\", percent: 2, caption: \"Operating Margin\" },\n          { kind: \"decline\", value: \"\", percent: 0, caption: \"Negative Operating Cash Flow\" },\n          { kind: \"bag\", value: \"\", percent: 0, caption: \"$11 Billion+ in Debt\" },\n          { kind: \"stack\", value: \"\", percent: 0, caption: \"$15 Billion Lease Commitments\" },\n        ],\n      },\n    ],\n    brand: \"ledger\",\n  },\n  component: CompanyScorecard,\n  file: \"src/registry/items/company-scorecard.tsx\",\n  origin: {\n    prompt:\n      \"A two-row comparison scorecard on a cross divider: each row has a rounded logo tile on the left with a small outlined pill under it carrying the headline figure, and four badges to the right of the vertical rule. Badges are ring gauges that sweep to a percentage, or glyphs — a shield with a tick, an audience, a falling chart, a money bag, a stack. The top row is drawn in the accent colour and the bottom in the alternate, and colour is the only thing saying which is which. Badges build in reading order across the top row and then across the bottom, never both at once.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A two-row comparison scorecard on a cross divider: each row has a rounded logo tile on the left with a small outlined pill under it carrying the headline figure, and four badges to the right of the vertical rule. Badges are ring gauges that sweep to a percentage, or glyphs — a shield with a tick, an audience, a falling chart, a money bag, a stack. The top row is drawn in the accent colour and the bottom in the alternate, and colour is the only thing saying which is which. Badges build in reading order across the top row and then across the bottom, never both at once.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3909,
      "props": [
        {
          "name": "rows",
          "type": "array",
          "description": "One row, or two for the comparison this is built for.",
          "required": true,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "comparison-bars",
      "name": "Comparison Bars",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Paired horizontal bars per metric grow from one shared origin with a stagger down the table, figures counting at the bar ends",
      "tags": [
        "comparison",
        "horizontal bars",
        "two series",
        "counter",
        "stagger",
        "legend",
        "table"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/comparison-bars.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/* ------------------------------------------------------------------ schema */\n\nconst rowSchema = z.object({\n  label: z\n    .string()\n    .describe(\n      \"The metric being compared, set in the label column. Name the measure, not the verdict — 'Scenes per week', not 'We are faster'. Keep it to three or four words: the column is a fixed fraction of the frame and long labels wrap to a second line, which crowds the bars.\",\n    ),\n  ours: z\n    .number()\n    .min(0)\n    .describe(\n      \"The value for the accent series — the side the scene is arguing for. Bars grow rightward from a shared origin, so values are non-negative and length is honest: if this number is the smaller of the pair, the accent bar is genuinely the shorter one.\",\n    ),\n  theirs: z\n    .number()\n    .min(0)\n    .describe(\n      \"The value for the muted series — the baseline being argued against. Same unit as `ours`; the pair shares one domain, so the two bars in a row are directly comparable.\",\n    ),\n  prefix: z\n    .string()\n    .optional()\n    .describe(\"Set immediately before both figures in this row — a currency mark such as '$' or '€'. Per row, because a comparison table usually mixes units.\"),\n  suffix: z\n    .string()\n    .optional()\n    .describe(\n      \"Set immediately after both figures in this row — '%', 'min', 'k'. Rendered exactly as typed, so include a leading thin space yourself if the unit needs air.\",\n    ),\n});\n\nexport const comparisonBarsSchema = z.object({\n  rows: z\n    .array(rowSchema)\n    .min(2)\n    .max(7)\n    .describe(\n      \"The metrics, top to bottom in reading order — the reveal staggers down the list, so order them the way you want the argument made and put the most decisive row first. Four to five rows is the sweet spot; seven is the ceiling before the bars thin out.\",\n    ),\n  oursLabel: z\n    .string()\n    .default(\"Us\")\n    .describe(\"Name of the accent series, shown in the first legend chip. A product or team name reads better than 'Us'.\"),\n  theirsLabel: z\n    .string()\n    .default(\"Them\")\n    .describe(\"Name of the muted series, shown in the second legend chip. Name the baseline honestly — 'Hand-built', 'Industry median' — rather than a competitor you are not prepared to cite.\"),\n  title: z\n    .string()\n    .optional()\n    .describe(\"Headline above the table, set in the display face. State the comparison ('Registry vs. hand-built'), not the conclusion. One line reads best.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono label in the accent colour above the title. Omit for a barer table.\"),\n  basis: z\n    .enum([\"row\", \"shared\"])\n    .default(\"row\")\n    .describe(\n      \"How bar length is scaled. 'row' normalises each row to its own pair, so the winner fills the track and every row is legible even when the units differ — the right default for a mixed-unit comparison. 'shared' puts every row on one domain, which is only honest when all rows measure the same thing in the same unit.\",\n    ),\n  showLead: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Show the size of the gap in a right-hand column: a multiple ('3.4x') once one side is nearly double the other, a percentage ('+32%') below that. Accent-coloured when the accent series leads, muted when it trails. The chip is omitted for any row where the multiple is not finite (a zero on either side) or the two values are within 2%.\",\n    ),\n  legend: z\n    .boolean()\n    .default(true)\n    .describe(\"Show the two legend chips above the table, each carrying a swatch cut to the same shape as the bars. Off only when a caption elsewhere already names the two series.\"),\n  dividers: z\n    .boolean()\n    .default(true)\n    .describe(\"Hairline under each row, sweeping in with it. This is what makes the block read as a table rather than a loose stack of bars; turn it off for a softer, more editorial list.\"),\n  labelWidth: z\n    .number()\n    .min(0.16)\n    .max(0.44)\n    .default(0.27)\n    .describe(\n      \"Width of the label column as a fraction of the content width. It fixes the origin every bar grows from, so widen it for long metric names rather than letting them wrap — 0.34 or so in 9:16, where the column is narrow in absolute terms.\",\n    ),\n  barThickness: z\n    .number()\n    .min(0.12)\n    .max(0.4)\n    .default(0.24)\n    .describe(\"Thickness of one bar as a fraction of its row height. 0.24 leaves the pair reading as a pair with air around it; above 0.32 the two bars fuse into a block, below 0.16 they read as rules.\",\n    ),\n  rivalTone: z\n    .enum([\"muted\", \"alt\"])\n    .default(\"muted\")\n    .describe(\n      \"Colour of the second series. 'muted' sits between the gridline and the muted ink, so the accent series plainly wins the frame. 'alt' uses the brand's `accentAlt`, which gives the two series equal weight — use it when the scene is a genuine comparison rather than an argument.\",\n    ),\n  compact: z\n    .boolean()\n    .default(false)\n    .describe(\"Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Turn it on once the figures pass four digits.\"),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono caption under a hairline at the foot of the frame: the sample, the period, the method. One short sentence — this is where a comparison earns its credibility.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof comparisonBarsSchema>;\ntype Row = z.infer<typeof rowSchema>;\n\n/* ------------------------------------------------------------------ timing */\n\nconst BUDGET = BUDGETS.scene;\n\n/*\n * Beats, in frames from this component's own frame 0. Declared rather than\n * derived, because the order matters more than the numbers: the frame gets its\n * head (eyebrow, title, head rule), then the legend names the two series, then\n * the origin rule draws down to establish where every bar starts, and only then\n * do the bars grow out of it. The accent bar of a row leads its rival by four\n * frames, so each pair reads as a claim followed by its comparison rather than\n * two bars arriving together.\n *\n * The last figure settles around frame 71 of a 150-frame budget — inside the\n * hold, which is the contract: presence is spent in `enter`, but a counter is\n * the one piece of internal motion the hold exists to carry.\n */\nconst HEAD_FADE = 12;\nconst RULE_START = 4;\n/** Frames the head rule takes to sweep the content width. */\nconst RULE_DRAW = 18;\nconst LEGEND_START = 8;\nconst LEGEND_STEP = 4;\nconst LEGEND_FADE = 12;\nconst SPINE_START = 10;\n/** Frames the origin rule takes to draw down the table. */\nconst SPINE_DRAW = 16;\nconst ROWS_START = 15;\nconst DIVIDER_DRAW = 16;\nconst LABEL_FADE = 12;\n/** Frames one bar takes to grow to its full length. */\nconst BAR_GROW = 22;\n/** The rival bar follows the accent bar by this much. */\nconst RIVAL_LAG = 4;\n/** Fraction of a bar's growth that passes before its figure starts counting. */\nconst COUNT_LEAD = 0.4;\n/** Frames the counter runs on past its own bar, so the figure settles last. */\nconst COUNT_TAIL = 6;\nconst LEAD_FADE = 13;\n\n/**\n * Per-row delay, held inside the readable stagger range (roughly 120–230ms at\n * 30fps) and tightened as the table grows, so seven rows still read as one\n * gesture running down the page rather than seven separate events.\n */\nconst rowStagger = (count: number) => Math.min(7, Math.max(3.5, 30 / count));\n\n/* --------------------------------------------------------------- numbers */\n\n/** Typographic thousands separator. A comma is punctuation; this is spacing. */\nconst THIN_SPACE = \"\\u2009\";\n\nconst groupDigits = (digits: string) => digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, THIN_SPACE);\n\nconst trimZero = (n: number, places: number) => n.toFixed(places).replace(/\\.0+$/, \"\");\n\n/**\n * Decimal places implied by a number — 0 for integers, 1 for tenths, 2 beyond.\n * Derived rather than configured, and taken across both sides of a row, so the\n * two figures in a pair always carry the same precision. A row of whole numbers\n * never grows a trailing \".0\" just because its rival has a fraction.\n */\nconst decimalsFor = (n: number): number => {\n  if (Math.abs(n - Math.round(n)) < 1e-9) return 0;\n  if (Math.abs(n * 10 - Math.round(n * 10)) < 1e-9) return 1;\n  return 2;\n};\n\nconst formatFigure = (value: number, decimals: number, compact: boolean): string => {\n  const abs = Math.abs(value);\n  if (compact && abs >= 1e6) return `${trimZero(value / 1e6, 1)}M`;\n  if (compact && abs >= 1e3) return `${trimZero(value / 1e3, abs >= 1e4 ? 0 : 1)}k`;\n\n  const fixed = abs.toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const whole = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fraction = dot === -1 ? \"\" : fixed.slice(dot);\n  return `${groupDigits(whole)}${fraction}`;\n};\n\n/**\n * The size of the gap, phrased the way a person would say it out loud: a\n * multiple once one side is nearly double the other, a percentage below that.\n * `null` where there is nothing honest to print — a zero on either side has no\n * finite multiple, and a gap under 2% is noise dressed as a finding.\n */\nconst leadFigure = (win: number, lose: number): string | null => {\n  if (!(win > 0) || !(lose > 0)) return null;\n  const ratio = win / lose;\n  if (ratio < 1.02) return null;\n  if (ratio >= 1.95) return `${trimZero(ratio, ratio >= 9.95 ? 0 : 1)}\\u00d7`;\n  return `+${Math.round((ratio - 1) * 100)}%`;\n};\n\n/* ------------------------------------------------------------------ layout */\n\n/** Table geometry, in frame pixels, relative to the left edge of the content. */\ntype Geom = {\n  contentW: number;\n  rowH: number;\n  /** x of the shared origin rule. Every bar in every row grows from here. */\n  originX: number;\n  /** x the bars actually start at — just clear of the origin rule. */\n  barLeft: number;\n  /** Longest a bar may be, with the travelling figure's room already taken out. */\n  maxLen: number;\n  /** Bar thickness. */\n  bar: number;\n  /** Vertical gap between the two bars of a pair. */\n  pairGap: number;\n  /** Air between a bar's end and its figure. */\n  figureGap: number;\n  /** Width of the right-hand lead column. 0 when it is off. */\n  leadW: number;\n  /** Hairline weight — dividers, origin rule. */\n  hair: number;\n  /** Head rule weight, one step heavier because it closes the header. */\n  spine: number;\n  /** Corner radius on the growing end of a bar. */\n  radius: number;\n};\n\n/** Type sizes in frame pixels. */\ntype Scale = {\n  eyebrow: number;\n  title: number;\n  legend: number;\n  label: number;\n  figure: number;\n  lead: number;\n  foot: number;\n};\n\nconst CLAMP = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\n\n/** Geist Mono's advance width as a fraction of its size, for reserving figure room. */\nconst MONO_ADVANCE = 0.62;\n\n/* --------------------------------------------------------------- component */\n\nconst ComparisonBars: React.FC<Props> = ({\n  rows,\n  oursLabel,\n  theirsLabel,\n  title,\n  eyebrow,\n  basis,\n  showLead,\n  legend,\n  dividers,\n  labelWidth,\n  barThickness,\n  rivalTone,\n  compact,\n  footnote,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const f = stage.frame;\n\n  /*\n   * One length everything is sized off, chosen so the component survives every\n   * aspect: in landscape the width binds, in portrait the height does. Sizing\n   * type off `width` alone would give a 9:16 cut 12px figures.\n   */\n  const base = Math.min(width, height * 1.6);\n\n  const scale: Scale = {\n    eyebrow: base * 0.0125,\n    title: base * 0.0345,\n    legend: base * 0.0135,\n    label: base * 0.0165,\n    figure: base * 0.0178,\n    lead: base * 0.0145,\n    foot: base * 0.0118,\n  };\n\n  const padX = width * 0.072;\n  const padTop = height * 0.085;\n  const padBottom = height * 0.075;\n  const contentW = width - padX * 2;\n\n  const hair = Math.max(1, base * 0.0007);\n  const spine = Math.max(1.6, base * 0.0013);\n\n  // Vertical stack, measured rather than flowed, so the bars land on a grid.\n  const eyebrowH = eyebrow ? scale.eyebrow * 2.0 : 0;\n  const titleH = title ? scale.title * 1.2 : 0;\n  const ruleY = padTop + eyebrowH + titleH + height * 0.036;\n  const legendH = legend ? scale.legend * 2.2 : 0;\n  const legendTop = ruleY + height * 0.03;\n  const footH = footnote ? scale.foot * 2.9 : 0;\n  const rowsTop = legendTop + legendH + height * (legend ? 0.028 : 0.008);\n  const rowsH = Math.max(base * 0.2, height - padBottom - footH - rowsTop);\n  const rowH = rowsH / rows.length;\n\n  // Horizontal bands: label column, then the bar track, then the lead column.\n  const originX = contentW * labelWidth;\n  const barLeft = originX + contentW * 0.016;\n  const leadW = showLead ? contentW * 0.018 + scale.lead * 3.6 : 0;\n  const figureGap = contentW * 0.014;\n\n  const bar = rowH * barThickness;\n  const pairGap = bar * 0.44;\n\n  const decimalsOf = (r: Row) => Math.max(decimalsFor(r.ours), decimalsFor(r.theirs));\n  const figureOf = (r: Row, value: number) =>\n    `${r.prefix ?? \"\"}${formatFigure(value, decimalsOf(r), compact)}${r.suffix ?? \"\"}`;\n\n  /*\n   * The longest figure any bar will ever carry decides how much of the track the\n   * bars may take. Reserving it up front means the widest bar's figure lands\n   * exactly at the edge of the lead column instead of colliding with it, and it\n   * puts every winning figure in one right-aligned column when `basis` is 'row'.\n   */\n  const figureChars = rows.reduce(\n    (m, r) => Math.max(m, figureOf(r, r.ours).length, figureOf(r, r.theirs).length),\n    1,\n  );\n  const figureW = figureGap + figureChars * scale.figure * MONO_ADVANCE;\n  const trackW = contentW - barLeft - leadW;\n  const maxLen = Math.max(base * 0.05, trackW - figureW);\n\n  const geom: Geom = {\n    contentW,\n    rowH,\n    originX,\n    barLeft,\n    maxLen,\n    bar,\n    pairGap,\n    figureGap,\n    leadW,\n    hair,\n    spine,\n    // Radius comes from the brand, not the bar: a token set with square corners\n    // (Press) gets square bar ends.\n    radius: Math.min(bar * 0.42, Math.max(1, t.radius * 0.6)),\n  };\n\n  /*\n   * The rival fill sits between the gridline colour and the muted ink: `rule`\n   * alone reads as chrome rather than data, `inkMuted` alone competes with the\n   * accent and the row loses its winner.\n   */\n  const rivalFill = rivalTone === \"alt\" ? t.accentAlt : interpolateColors(0.42, [0, 1], [t.rule, t.inkMuted]);\n  const rivalInk = rivalTone === \"alt\" ? t.accentAlt : t.inkMuted;\n\n  const globalMax = rows.reduce((m, r) => Math.max(m, r.ours, r.theirs), 0);\n  const domainOf = (r: Row) => {\n    const raw = basis === \"shared\" ? globalMax : Math.max(r.ours, r.theirs);\n    return raw > 0 ? raw : 1;\n  };\n\n  const step = rowStagger(rows.length);\n  const lastLanding = ROWS_START + step * (rows.length - 1) + RIVAL_LAG + BAR_GROW + COUNT_TAIL;\n\n  const headIn = interpolate(f, [0, HEAD_FADE], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const titleIn = interpolate(f, [2, 17], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const headRule = interpolate(f, [RULE_START, RULE_START + RULE_DRAW], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const spineDraw = interpolate(f, [SPINE_START, SPINE_START + SPINE_DRAW], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const footRule = interpolate(f, [lastLanding - 12, lastLanding + 6], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const footIn = interpolate(f, [lastLanding - 4, lastLanding + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n\n  const lift = (1 - stage.enter) * height * 0.016;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, fontFamily: t.fontBody }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: stage.presence,\n          transform: `translateY(${lift}px)`,\n        }}\n      >\n        {/* Header, hard left. The table is the composition; the title labels it. */}\n        <div style={{ position: \"absolute\", left: padX, top: padTop, width: contentW }}>\n          {eyebrow ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: scale.eyebrow,\n                fontWeight: 600,\n                letterSpacing: scale.eyebrow * 0.18,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                opacity: headIn,\n                transform: `translateY(${(1 - headIn) * scale.eyebrow * 0.7}px)`,\n              }}\n            >\n              {eyebrow}\n            </div>\n          ) : null}\n\n          {title ? (\n            <span\n              style={{\n                display: \"block\",\n                overflow: \"hidden\",\n                marginTop: eyebrow ? scale.eyebrow * 1.05 : 0,\n                paddingBottom: scale.title * 0.08,\n              }}\n            >\n              <span\n                style={{\n                  display: \"block\",\n                  fontFamily: t.fontDisplay,\n                  fontSize: scale.title,\n                  fontWeight: 600,\n                  lineHeight: 1.06,\n                  letterSpacing: scale.title * -0.02,\n                  color: t.ink,\n                  transform: `translateY(${(1 - titleIn) * 108}%)`,\n                }}\n              >\n                {title}\n              </span>\n            </span>\n          ) : null}\n        </div>\n\n        {/* Head rule: closes the header, opens the table. */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: padX,\n            top: ruleY,\n            width: contentW,\n            height: spine,\n            backgroundColor: t.rule,\n            transform: `scaleX(${headRule})`,\n            transformOrigin: \"left\",\n          }}\n        />\n\n        {legend ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: padX + barLeft,\n              top: legendTop,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: contentW * 0.03,\n            }}\n          >\n            <LegendChip\n              label={oursLabel}\n              fill={t.accent}\n              ink={t.ink}\n              weight={600}\n              frame={f}\n              start={LEGEND_START}\n              scale={scale}\n              geom={geom}\n              t={t}\n            />\n            <LegendChip\n              label={theirsLabel}\n              fill={rivalFill}\n              ink={t.inkMuted}\n              weight={500}\n              frame={f}\n              start={LEGEND_START + LEGEND_STEP}\n              scale={scale}\n              geom={geom}\n              t={t}\n            />\n          </div>\n        ) : null}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: padX,\n            top: rowsTop,\n            width: contentW,\n            height: rowsH,\n          }}\n        >\n          {/*\n            The shared origin, made visible. Every bar in the table starts on\n            this line, which is the whole reason the label column is a fixed\n            width. Scaling a hair-wide div on Y leaves its width — and so its\n            weight — untouched, so the rule draws down at a constant 1px.\n          */}\n          <div\n            style={{\n              position: \"absolute\",\n              left: originX,\n              top: 0,\n              width: hair,\n              height: rowsH,\n              backgroundColor: t.rule,\n              transform: `scaleY(${spineDraw})`,\n              transformOrigin: \"top\",\n            }}\n          />\n\n          {rows.map((row, i) => (\n            <MetricRow\n              key={`${row.label}-${i}`}\n              row={row}\n              frame={f}\n              start={ROWS_START + step * i}\n              geom={geom}\n              scale={scale}\n              domain={domainOf(row)}\n              figure={(value) => figureOf(row, value)}\n              rivalFill={rivalFill}\n              rivalInk={rivalInk}\n              showLead={showLead}\n              divider={dividers}\n              t={t}\n            />\n          ))}\n        </div>\n\n        {footnote ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: padX,\n              top: height - padBottom - footH,\n              width: contentW,\n            }}\n          >\n            <div\n              style={{\n                height: hair,\n                backgroundColor: t.rule,\n                transform: `scaleX(${footRule})`,\n                transformOrigin: \"left\",\n              }}\n            />\n            <div\n              style={{\n                marginTop: scale.foot * 0.9,\n                fontFamily: t.fontMono,\n                fontSize: scale.foot,\n                fontWeight: 500,\n                letterSpacing: scale.foot * 0.02,\n                color: t.inkMuted,\n                opacity: footIn,\n              }}\n            >\n              {footnote}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------ pieces */\n\n/**\n * One legend chip. The swatch is cut to the same silhouette as the bars —\n * square on the left, rounded on the growing end — so the eye connects chip to\n * bar without a caption saying so.\n */\nconst LegendChip: React.FC<{\n  label: string;\n  fill: string;\n  ink: string;\n  weight: number;\n  frame: number;\n  start: number;\n  scale: Scale;\n  geom: Geom;\n  t: BrandTokens;\n}> = ({ label, fill, ink, weight, frame, start, scale, geom, t }) => {\n  const enter = interpolate(frame, [start, start + LEGEND_FADE], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  const swatchH = Math.max(2, scale.legend * 0.44);\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: scale.legend * 0.7,\n        opacity: enter,\n        transform: `translateX(${(1 - enter) * scale.legend * -1.1}px)`,\n      }}\n    >\n      <div\n        style={{\n          width: scale.legend * 2.3,\n          height: swatchH,\n          backgroundColor: fill,\n          borderTopRightRadius: Math.min(swatchH / 2, geom.radius),\n          borderBottomRightRadius: Math.min(swatchH / 2, geom.radius),\n        }}\n      />\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: scale.legend,\n          fontWeight: weight,\n          letterSpacing: scale.legend * 0.12,\n          textTransform: \"uppercase\",\n          color: ink,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {label}\n      </div>\n    </div>\n  );\n};\n\n/**\n * One metric: the label in its fixed column, the accent bar and its rival\n * growing from the shared origin, a figure riding each bar's end, the size of\n * the gap in the right-hand column, and a hairline closing the row.\n *\n * Bars grow by animating `width` rather than `scaleX`. Scaling would stretch\n * the rounded end into an ellipse on the way out and squash the travelling\n * figure with it; animating width keeps the end circular and keeps the growth\n * reading as a measurement rather than a squash.\n */\nconst MetricRow: React.FC<{\n  row: Row;\n  frame: number;\n  start: number;\n  geom: Geom;\n  scale: Scale;\n  domain: number;\n  figure: (value: number) => string;\n  rivalFill: string;\n  rivalInk: string;\n  showLead: boolean;\n  divider: boolean;\n  t: BrandTokens;\n}> = ({\n  row,\n  frame,\n  start,\n  geom,\n  scale,\n  domain,\n  figure,\n  rivalFill,\n  rivalInk,\n  showLead,\n  divider,\n  t,\n}) => {\n  const pairH = geom.bar * 2 + geom.pairGap;\n  // Nudged above centre: the row's hairline needs the room underneath, and a\n  // pair sitting dead centre reads as floating between two rules.\n  const pairTop = Math.max(0, (geom.rowH - pairH) * 0.44);\n  const pairMid = pairTop + pairH / 2;\n\n  const labelIn = interpolate(frame, [start, start + LABEL_FADE], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  const dividerDraw = interpolate(frame, [start - 2, start - 2 + DIVIDER_DRAW], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n\n  const oursWins = row.ours >= row.theirs;\n  const lead = oursWins ? leadFigure(row.ours, row.theirs) : leadFigure(row.theirs, row.ours);\n  const leadStart = start + RIVAL_LAG + BAR_GROW - 2;\n  const leadIn = interpolate(frame, [leadStart, leadStart + LEAD_FADE], [0, 1], {\n    ...CLAMP,\n    // The one accent gesture in the component: the payoff figure arrives with a\n    // touch of overshoot, and nothing else in the frame does.\n    easing: EASING.accent,\n  });\n\n  return (\n    <div style={{ position: \"relative\", height: geom.rowH }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: pairMid,\n          width: geom.originX - geom.contentW * 0.022,\n          transform: `translateY(-50%) translateX(${(1 - labelIn) * scale.label * -0.4}px)`,\n          fontFamily: t.fontBody,\n          fontSize: scale.label,\n          fontWeight: 500,\n          lineHeight: 1.16,\n          letterSpacing: scale.label * -0.004,\n          color: t.ink,\n          opacity: labelIn,\n        }}\n      >\n        {row.label}\n      </div>\n\n      <Bar\n        value={row.ours}\n        domain={domain}\n        top={pairTop}\n        start={start}\n        frame={frame}\n        geom={geom}\n        scale={scale}\n        figure={figure}\n        fill={t.accent}\n        ink={t.ink}\n        weight={600}\n        size={scale.figure}\n        t={t}\n      />\n      <Bar\n        value={row.theirs}\n        domain={domain}\n        top={pairTop + geom.bar + geom.pairGap}\n        start={start + RIVAL_LAG}\n        frame={frame}\n        geom={geom}\n        scale={scale}\n        figure={figure}\n        fill={rivalFill}\n        ink={rivalInk}\n        weight={500}\n        size={scale.figure * 0.9}\n        t={t}\n      />\n\n      {showLead && lead ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: geom.contentW - geom.leadW,\n            top: pairMid,\n            width: geom.leadW,\n            textAlign: \"right\",\n            transform: `translateY(-50%) translateX(${(1 - leadIn) * scale.lead * 0.5}px)`,\n            fontFamily: t.fontMono,\n            fontSize: scale.lead,\n            fontWeight: 600,\n            fontVariantNumeric: \"tabular-nums\",\n            letterSpacing: scale.lead * 0.01,\n            // Colour, not a caret, carries the direction: accent when the\n            // accent series leads, muted when it trails.\n            color: oursWins ? t.accent : rivalInk,\n            opacity: leadIn,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {lead}\n        </div>\n      ) : null}\n\n      {divider ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            bottom: 0,\n            width: geom.contentW,\n            height: geom.hair,\n            backgroundColor: t.rule,\n            transform: `scaleX(${dividerDraw})`,\n            transformOrigin: \"left\",\n          }}\n        />\n      ) : null}\n    </div>\n  );\n};\n\n/** One bar and the figure that rides its end, counting as the bar grows. */\nconst Bar: React.FC<{\n  value: number;\n  domain: number;\n  top: number;\n  start: number;\n  frame: number;\n  geom: Geom;\n  scale: Scale;\n  figure: (value: number) => string;\n  fill: string;\n  ink: string;\n  weight: number;\n  size: number;\n  t: BrandTokens;\n}> = ({ value, domain, top, start, frame, geom, scale, figure, fill, ink, weight, size, t }) => {\n  const grow = interpolate(frame, [start, start + BAR_GROW], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  // A couple of pixels of floor, so a value that rounds to a sliver still reads\n  // as a bar rather than as a rendering fault. A true zero draws nothing.\n  const full = value > 0 ? Math.max(geom.hair * 3, (geom.maxLen * value) / domain) : 0;\n  const len = full * grow;\n\n  const countFrom = start + BAR_GROW * COUNT_LEAD;\n  const count = interpolate(frame, [countFrom, start + BAR_GROW + COUNT_TAIL], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const figureIn = interpolate(frame, [countFrom, countFrom + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n\n  const radius = Math.min(geom.radius, len);\n\n  return (\n    <>\n      <div\n        style={{\n          position: \"absolute\",\n          left: geom.barLeft,\n          top,\n          width: len,\n          height: geom.bar,\n          backgroundColor: fill,\n          borderTopRightRadius: radius,\n          borderBottomRightRadius: radius,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          // Anchored to the bar's end, so the figure travels out with it.\n          left: geom.barLeft + len + geom.figureGap,\n          top: top + geom.bar / 2,\n          transform: \"translateY(-50%)\",\n          fontFamily: t.fontMono,\n          fontSize: size,\n          fontWeight: weight,\n          // Tabular figures: a counter whose digits change width jitters, and a\n          // jittering figure at the end of a moving bar reads as a glitch.\n          fontVariantNumeric: \"tabular-nums\",\n          letterSpacing: scale.figure * -0.005,\n          color: ink,\n          opacity: figureIn,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {figure(value * count)}\n      </div>\n    </>\n  );\n};\n\n/* ---------------------------------------------------------------- registry */\n\nexport default defineMotionItem({\n  id: \"comparison-bars\",\n  name: \"Comparison Bars\",\n  category: \"data\",\n  summary:\n    \"Paired horizontal bars per metric grow from one shared origin with a stagger down the table, figures counting at the bar ends\",\n  tags: [\"comparison\", \"horizontal bars\", \"two series\", \"counter\", \"stagger\", \"legend\", \"table\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: comparisonBarsSchema,\n  defaults: {\n    rows: [\n      { label: \"Scenes per week\", ours: 24, theirs: 7 },\n      { label: \"Components reused\", ours: 31, theirs: 5 },\n      { label: \"Brands retargeted\", ours: 6, theirs: 1 },\n      { label: \"Render pass rate\", ours: 98, theirs: 74, suffix: \"%\" },\n      { label: \"First-pass sign-off\", ours: 91, theirs: 58, suffix: \"%\" },\n    ],\n    oursLabel: \"Motionstash\",\n    theirsLabel: \"Hand-built\",\n    title: \"Registry vs. hand-built\",\n    eyebrow: \"Head to head\",\n    basis: \"row\",\n    showLead: true,\n    legend: true,\n    dividers: true,\n    labelWidth: 0.27,\n    barThickness: 0.24,\n    rivalTone: \"muted\",\n    compact: false,\n    footnote: \"Same brief, same reviewers · median across twelve launch teams\",\n    brand: \"slate\",\n  },\n  component: ComparisonBars,\n  file: \"src/registry/items/comparison-bars.tsx\",\n  origin: {\n    prompt:\n      \"An us-versus-them comparison, row by row. Each metric gets a pair of horizontal bars — the accent one and a muted one — growing from the left out of a single shared origin, staggered down the list, with the value counting up at the end of each bar as it grows. Row labels left-aligned in a fixed-width column so every bar starts on the same line, two legend chips at the top naming the series, hairline dividers under each row, and a right-hand column showing how big the gap is as a multiple or a percentage.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An us-versus-them comparison, row by row. Each metric gets a pair of horizontal bars — the accent one and a muted one — growing from the left out of a single shared origin, staggered down the list, with the value counting up at the end of each bar as it grows. Row labels left-aligned in a fixed-width column so every bar starts on the same line, two legend chips at the top naming the series, hairline dividers under each row, and a right-hand column showing how big the gap is as a multiple or a percentage.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4172,
      "props": [
        {
          "name": "rows",
          "type": "array",
          "description": "The metrics, top to bottom in reading order — the reveal staggers down the list, so order them the way you want the argument made and put the most decisive row first. Four to five rows is the sweet spot; seven is the ceiling before the bars thin out.",
          "required": true,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "oursLabel",
          "type": "string",
          "description": "Name of the accent series, shown in the first legend chip. A product or team name reads better than 'Us'.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "theirsLabel",
          "type": "string",
          "description": "Name of the muted series, shown in the second legend chip. Name the baseline honestly — 'Hand-built', 'Industry median' — rather than a competitor you are not prepared to cite.",
          "required": false,
          "defaultValue": "Hand-built",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "Headline above the table, set in the display face. State the comparison ('Registry vs. hand-built'), not the conclusion. One line reads best.",
          "required": false,
          "defaultValue": "Registry vs. hand-built",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label in the accent colour above the title. Omit for a barer table.",
          "required": false,
          "defaultValue": "Head to head",
          "options": null
        },
        {
          "name": "basis",
          "type": "\"row\" | \"shared\"",
          "description": "How bar length is scaled. 'row' normalises each row to its own pair, so the winner fills the track and every row is legible even when the units differ — the right default for a mixed-unit comparison. 'shared' puts every row on one domain, which is only honest when all rows measure the same thing in the same unit.",
          "required": false,
          "defaultValue": "row",
          "options": [
            "row",
            "shared"
          ]
        },
        {
          "name": "showLead",
          "type": "boolean",
          "description": "Show the size of the gap in a right-hand column: a multiple ('3.4x') once one side is nearly double the other, a percentage ('+32%') below that. Accent-coloured when the accent series leads, muted when it trails. The chip is omitted for any row where the multiple is not finite (a zero on either side) or the two values are within 2%.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "legend",
          "type": "boolean",
          "description": "Show the two legend chips above the table, each carrying a swatch cut to the same shape as the bars. Off only when a caption elsewhere already names the two series.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "dividers",
          "type": "boolean",
          "description": "Hairline under each row, sweeping in with it. This is what makes the block read as a table rather than a loose stack of bars; turn it off for a softer, more editorial list.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "labelWidth",
          "type": "number",
          "description": "Width of the label column as a fraction of the content width. It fixes the origin every bar grows from, so widen it for long metric names rather than letting them wrap — 0.34 or so in 9:16, where the column is narrow in absolute terms.",
          "required": false,
          "defaultValue": "0.27",
          "options": null
        },
        {
          "name": "barThickness",
          "type": "number",
          "description": "Thickness of one bar as a fraction of its row height. 0.24 leaves the pair reading as a pair with air around it; above 0.32 the two bars fuse into a block, below 0.16 they read as rules.",
          "required": false,
          "defaultValue": "0.24",
          "options": null
        },
        {
          "name": "rivalTone",
          "type": "\"muted\" | \"alt\"",
          "description": "Colour of the second series. 'muted' sits between the gridline and the muted ink, so the accent series plainly wins the frame. 'alt' uses the brand's `accentAlt`, which gives the two series equal weight — use it when the scene is a genuine comparison rather than an argument.",
          "required": false,
          "defaultValue": "muted",
          "options": [
            "muted",
            "alt"
          ]
        },
        {
          "name": "compact",
          "type": "boolean",
          "description": "Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Turn it on once the figures pass four digits.",
          "required": false,
          "defaultValue": "false",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Quiet mono caption under a hairline at the foot of the frame: the sample, the period, the method. One short sentence — this is where a comparison earns its credibility.",
          "required": false,
          "defaultValue": "Same brief, same reviewers · median across twelve launch …",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "comparison-race-chart",
      "name": "Comparison Race Chart",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Two series race across fifty points, one solid and one dashed, each carrying a badge and a live figure while a counter runs in the corner",
      "tags": [
        "comparison",
        "line chart",
        "race",
        "dual series",
        "counter",
        "legend",
        "svg"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 360,
      "durationSeconds": 12,
      "budget": {
        "enter": 27,
        "hold": 306,
        "exit": 27
      },
      "file": "src/registry/items/comparison-race-chart.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { useStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\nimport type { TimingBudget } from \"../types\";\n\n/**\n * Twelve seconds: the grid settles, then two series race each other across half\n * a century, then it holds on the finished picture long enough to be read.\n *\n * The source was a PIL script that drew 360 frames at a fixed 1920x1080. Two\n * things had to change to make it a registry component rather than a rendering\n * of one chart.\n *\n * Every measurement is a fraction of the frame instead of a pixel count, so the\n * same file composes for a 9:16 Short and a 1:1 feed post as well as it does for\n * landscape. Type is scaled by the frame's *short* edge, which is 1080 in every\n * format this registry ships — so the labels stay one size while the plot\n * changes shape underneath them.\n *\n * And the data is a prop rather than a formula. The defaults are the exact\n * curves from the original — an early leader that keeps stumbling, and a late\n * logistic that goes past it and does not look back — because that shape is the\n * whole reason to draw two lines instead of one.\n */\n\n/** 12s at 30fps. Longer than BUDGETS.feature: fifty points need time to read. */\nconst BUDGET: TimingBudget = { enter: 27, hold: 306, exit: 27 };\n\nconst N = 50;\n\n/** The original's series A: fast early growth, punctured by three setbacks. */\nconst DEFAULT_A = Array.from({ length: N + 1 }, (_, i) => {\n  let a = 20 + 58 * (1 - Math.exp(-i / 11));\n  a -= 8 * Math.exp(-((i - 25) ** 2) / 2.5);\n  a -= 13 * Math.exp(-((i - 32) ** 2) / 3.5);\n  a -= 21 * Math.exp(-((i - 44) ** 2) / 2.5);\n  return Number(Math.max(4, a).toFixed(2));\n});\n\n/** Series B: nothing for twenty years, then a logistic that clears the field. */\nconst DEFAULT_B = Array.from({ length: N + 1 }, (_, i) =>\n  Number((2 + 95 / (1 + Math.exp(-(i - 33) / 6.5))).toFixed(2)),\n);\n\nconst smoothstep = (t: number) => t * t * (3 - 2 * t);\n\nexport const comparisonRaceChartSchema = z.object({\n  title: z\n    .string()\n    .describe(\n      \"The comparison being made, set top-left. A noun phrase naming the measure — 'Revenue, indexed' rather than 'A chart of revenue'.\",\n    ),\n  subtitle: z\n    .string()\n    .default(\"\")\n    .describe(\n      \"One line under the title for the unit or the baseline — 'Index, 1976 = 20'. This is where a chart earns trust, so say what the numbers are.\",\n    ),\n  seriesAName: z.string().describe(\"Legend name for the solid series.\"),\n  seriesBName: z.string().describe(\"Legend name for the dashed series.\"),\n  seriesA: z\n    .array(z.number())\n    .min(2)\n    .describe(\n      \"The solid series, in order. Drawn with a round badge. Any length — the x axis divides itself evenly, so 20 points and 200 points both fill the plot.\",\n    ),\n  seriesB: z\n    .array(z.number())\n    .min(2)\n    .describe(\n      \"The dashed series. Should be the same length as seriesA; if it is not, the shorter one stops early and the longer one keeps going, which is usually a mistake rather than a design.\",\n    ),\n  startLabel: z\n    .number()\n    .default(1976)\n    .describe(\n      \"The value of the first x position, counted up one per point and shown large in the corner. Years are the obvious case; any integer sequence works.\",\n    ),\n  counterLabel: z\n    .string()\n    .default(\"Year\")\n    .describe(\"Small label above the running counter. Set to an empty string to drop it.\"),\n  yMax: z\n    .number()\n    .default(110)\n    .describe(\n      \"Top of the y scale. Deliberately not derived from the data: a fixed ceiling is what lets the winner visibly run out of room near the top.\",\n    ),\n  yTicks: z\n    .array(z.number())\n    .default([0, 25, 50, 75, 100])\n    .describe(\"Values that get a gridline and a left-hand label.\"),\n  xTickEvery: z\n    .number()\n    .int()\n    .min(1)\n    .default(10)\n    .describe(\"Label every Nth x position. 10 over 50 points gives six labels, which is about the most a frame can carry.\"),\n  colorA: z\n    .string()\n    .default(\"\")\n    .describe(\"Hex for the solid series. Empty uses the brand accent, which is the safer choice.\"),\n  colorB: z\n    .string()\n    .default(\"\")\n    .describe(\"Hex for the dashed series. Empty uses the brand's alternate accent.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\n      \"Brand preset. The light presets (paper, press, field) put dark type on a pale ground; the dark ones invert it. Both are composed for — this component reads its type colour from the brand rather than assuming a dark frame.\",\n    ),\n});\n\ntype Props = z.infer<typeof comparisonRaceChartSchema>;\n\n/** Partial polyline up to a fractional index, plus the value at the tip. */\nfunction traceTo(data: number[], f: number) {\n  const last = data.length - 1;\n  const k = Math.min(Math.floor(f), last);\n  const pts: { i: number; v: number }[] = [];\n  for (let i = 0; i <= k; i++) pts.push({ i, v: data[i] });\n\n  let current = data[k];\n  if (k < last) {\n    const t = f - k;\n    if (t > 0) {\n      current = data[k] + (data[k + 1] - data[k]) * t;\n      pts.push({ i: k + t, v: current });\n    }\n  }\n  if (pts.length === 1) pts.push({ i: pts[0].i + 0.001, v: pts[0].v });\n  return { pts, current };\n}\n\nexport const ComparisonRaceChart: React.FC<Props> = ({\n  title,\n  subtitle,\n  seriesAName,\n  seriesBName,\n  seriesA,\n  seriesB,\n  startLabel,\n  counterLabel,\n  yMax,\n  yTicks,\n  xTickEvery,\n  colorA,\n  colorB,\n  brand,\n}) => {\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.paper;\n\n  const A = seriesA;\n  const B = seriesB;\n  const steps = Math.max(A.length, B.length) - 1;\n\n  const cA = colorA || t.accent;\n  const cB = colorB || t.accentAlt;\n\n  /**\n   * Type is scaled by the short edge, which is 1080 in every format the registry\n   * ships. Scaling by width instead would make the same label large in landscape\n   * and small in a Short, for no reason a viewer would understand.\n   */\n  const u = Math.min(width, height) / 1080;\n\n  // Plot box as fractions of the frame, so the chart reshapes per format.\n  const px0 = width * 0.115;\n  const px1 = width * 0.885;\n  const py0 = height * 0.213;\n  const py1 = height * 0.787;\n\n  const X = (i: number) => px0 + ((px1 - px0) * i) / steps;\n  const Y = (v: number) => py1 - ((py1 - py0) * v) / yMax;\n\n  // The grid fades in over the enter window; the lines draw across the hold.\n  const ga = stage.enter;\n  const drawStart = BUDGET.enter;\n  const drawEnd = BUDGET.enter + BUDGET.hold * 0.845;\n  const p = interpolate(stage.frame, [drawStart, drawEnd], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const f = smoothstep(p) * steps;\n\n  const a = traceTo(A, f);\n  const b = traceTo(B, f);\n\n  const path = (pts: { i: number; v: number }[]) =>\n    pts.map((q, n) => `${n === 0 ? \"M\" : \"L\"}${X(q.i).toFixed(2)} ${Y(q.v).toFixed(2)}`).join(\" \");\n\n  /** Badge at the leading edge, pushed along the line's own direction. */\n  const tip = (pts: { i: number; v: number }[]) => {\n    const last = pts[pts.length - 1];\n    const prev = pts[Math.max(0, pts.length - 2)];\n    const x2 = X(last.i);\n    const y2 = Y(last.v);\n    const dx = x2 - X(prev.i);\n    const dy = y2 - Y(prev.v);\n    const m = Math.hypot(dx, dy) || 1;\n    return {\n      x: Math.min(px1 + 10 * u, Math.max(px0, x2 + (dx / m) * 46 * u)),\n      y: Math.min(py1 + 10 * u, Math.max(py0 - 20 * u, y2 + (dy / m) * 46 * u)),\n    };\n  };\n\n  const ta = tip(a.pts);\n  const tb = tip(b.pts);\n\n  // Two badges on top of each other read as one. Push them apart symmetrically.\n  const gap = 84 * u;\n  const dy = ta.y - tb.y;\n  if (Math.abs(dy) < gap && Math.abs(ta.x - tb.x) < gap) {\n    const push = (gap - Math.abs(dy)) / 2;\n    const s = dy <= 0 ? 1 : -1;\n    ta.y -= push * s;\n    tb.y += push * s;\n  }\n\n  const counter = Math.round(startLabel + f);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill style={{ opacity: stage.presence }}>\n        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>\n          {/* grid */}\n          <g opacity={ga}>\n            {yTicks.map((v) => (\n              <g key={v}>\n                <line\n                  x1={px0}\n                  y1={Y(v)}\n                  x2={px1}\n                  y2={Y(v)}\n                  stroke={t.rule}\n                  strokeWidth={Math.max(1, 2 * u)}\n                />\n                <text\n                  x={px0 - 24 * u}\n                  y={Y(v)}\n                  textAnchor=\"end\"\n                  dominantBaseline=\"middle\"\n                  fill={t.inkMuted}\n                  fontFamily={t.fontBody}\n                  fontSize={22 * u}\n                >\n                  {v}\n                </text>\n              </g>\n            ))}\n            {Array.from({ length: Math.floor(steps / xTickEvery) + 1 }, (_, n) => n * xTickEvery).map(\n              (i) => (\n                <text\n                  key={i}\n                  x={X(i)}\n                  y={py1 + 34 * u}\n                  textAnchor=\"middle\"\n                  dominantBaseline=\"hanging\"\n                  fill={t.inkMuted}\n                  fontFamily={t.fontBody}\n                  fontSize={22 * u}\n                >\n                  {startLabel + i}\n                </text>\n              ),\n            )}\n          </g>\n\n          {/* header */}\n          <g opacity={ga}>\n            <text\n              x={px0}\n              y={height * 0.076}\n              fill={t.ink}\n              fontFamily={t.fontDisplay}\n              fontSize={34 * u}\n              fontWeight={600}\n            >\n              {title}\n            </text>\n            {subtitle ? (\n              <text\n                x={px0}\n                y={height * 0.113}\n                fill={t.inkMuted}\n                fontFamily={t.fontBody}\n                fontSize={22 * u}\n              >\n                {subtitle}\n              </text>\n            ) : null}\n\n            {counterLabel ? (\n              <text\n                x={px1}\n                y={height * 0.057}\n                textAnchor=\"end\"\n                fill={t.inkMuted}\n                fontFamily={t.fontBody}\n                fontSize={20 * u}\n              >\n                {counterLabel}\n              </text>\n            ) : null}\n            <text\n              x={px1}\n              y={height * 0.122}\n              textAnchor=\"end\"\n              fill={t.ink}\n              fontFamily={t.fontDisplay}\n              fontSize={76 * u}\n              fontWeight={600}\n              style={{ fontVariantNumeric: \"tabular-nums\" }}\n            >\n              {counter}\n            </text>\n          </g>\n\n          {/* the two series */}\n          <path\n            d={path(a.pts)}\n            fill=\"none\"\n            stroke={cA}\n            strokeWidth={6 * u}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n          <path\n            d={path(b.pts)}\n            fill=\"none\"\n            stroke={cB}\n            strokeWidth={6 * u}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            strokeDasharray={`${26 * u} ${16 * u}`}\n          />\n\n          {/* leading badges and their live values */}\n          <BadgeRound x={ta.x} y={ta.y} r={34 * u} fill={cA} />\n          <text\n            x={ta.x + 48 * u}\n            y={ta.y}\n            dominantBaseline=\"middle\"\n            fill={cA}\n            fontFamily={t.fontDisplay}\n            fontSize={30 * u}\n            fontWeight={600}\n            style={{ fontVariantNumeric: \"tabular-nums\" }}\n          >\n            {Math.round(a.current)}\n          </text>\n\n          <BadgeHex x={tb.x} y={tb.y} r={34 * u} fill={cB} />\n          <text\n            x={tb.x + 48 * u}\n            y={tb.y}\n            dominantBaseline=\"middle\"\n            fill={cB}\n            fontFamily={t.fontDisplay}\n            fontSize={30 * u}\n            fontWeight={600}\n            style={{ fontVariantNumeric: \"tabular-nums\" }}\n          >\n            {Math.round(b.current)}\n          </text>\n\n          {/* legend */}\n          <g opacity={ga}>\n            <BadgeRound x={px0 + 16 * u} y={py1 + 96 * u} r={16 * u} fill={cA} />\n            <text\n              x={px0 + 44 * u}\n              y={py1 + 96 * u}\n              dominantBaseline=\"middle\"\n              fill={t.ink}\n              fontFamily={t.fontDisplay}\n              fontSize={26 * u}\n              fontWeight={600}\n            >\n              {seriesAName}\n            </text>\n\n            <BadgeHex x={px0 + 16 * u} y={py1 + 148 * u} r={16 * u} fill={cB} />\n            <text\n              x={px0 + 44 * u}\n              y={py1 + 148 * u}\n              dominantBaseline=\"middle\"\n              fill={t.ink}\n              fontFamily={t.fontDisplay}\n              fontSize={26 * u}\n              fontWeight={600}\n            >\n              {seriesBName}\n            </text>\n          </g>\n        </svg>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/** Series A's mark: a disc with a rising chevron cut into it. */\nconst BadgeRound: React.FC<{ x: number; y: number; r: number; fill: string }> = ({\n  x,\n  y,\n  r,\n  fill,\n}) => {\n  const w = r * 0.55;\n  return (\n    <g>\n      <circle cx={x} cy={y} r={r} fill={fill} />\n      <polyline\n        points={`${x - w},${y + w * 0.65} ${x},${y - w * 0.8} ${x + w},${y + w * 0.65}`}\n        fill=\"none\"\n        stroke=\"#FFFFFF\"\n        strokeWidth={r * 0.22}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </g>\n  );\n};\n\n/** Series B's mark: a hexagon with a square punched out, readable at legend size. */\nconst BadgeHex: React.FC<{ x: number; y: number; r: number; fill: string }> = ({\n  x,\n  y,\n  r,\n  fill,\n}) => {\n  const pts = [270, 330, 30, 90, 150, 210]\n    .map((deg) => {\n      const rad = (deg * Math.PI) / 180;\n      return `${x + r * Math.cos(rad)},${y + r * Math.sin(rad)}`;\n    })\n    .join(\" \");\n  const s = r * 0.36;\n  return (\n    <g>\n      <polygon points={pts} fill={fill} />\n      <rect x={x - s} y={y - s} width={s * 2} height={s * 2} fill=\"#FFFFFF\" />\n    </g>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"comparison-race-chart\",\n  name: \"Comparison Race Chart\",\n  category: \"data\",\n  summary:\n    \"Two series race across fifty points, one solid and one dashed, each carrying a badge and a live figure while a counter runs in the corner\",\n  tags: [\"comparison\", \"line chart\", \"race\", \"dual series\", \"counter\", \"legend\", \"svg\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: comparisonRaceChartSchema,\n  defaults: {\n    title: \"Umsatzentwicklung im Vergleich\",\n    subtitle: \"Index, 1976 = 20\",\n    seriesAName: \"Aurea Systems\",\n    seriesBName: \"Northlane Works\",\n    seriesA: DEFAULT_A,\n    seriesB: DEFAULT_B,\n    startLabel: 1976,\n    counterLabel: \"Jahr\",\n    yMax: 110,\n    yTicks: [0, 25, 50, 75, 100],\n    xTickEvery: 10,\n    colorA: \"\",\n    colorB: \"\",\n    brand: \"paper\",\n  },\n  component: ComparisonRaceChart,\n  file: \"src/registry/items/comparison-race-chart.tsx\",\n  origin: {\n    prompt:\n      \"Two series racing across half a century on a fixed 0-110 scale: one solid line for the early leader that keeps stumbling, one dashed for the late entrant whose logistic curve goes straight past it. Gridlines and axis labels fade up first, then both lines draw together over about eight seconds while a year counter runs large in the top-right corner. Each line carries a badge at its leading edge — a disc for the first, a hexagon for the second — with the current value beside it, and the two badges push each other apart when they would otherwise overlap.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Two series racing across half a century on a fixed 0-110 scale: one solid line for the early leader that keeps stumbling, one dashed for the late entrant whose logistic curve goes straight past it. Gridlines and axis labels fade up first, then both lines draw together over about eight seconds while a year counter runs large in the top-right corner. Each line carries a badge at its leading edge — a disc for the first, a hexagon for the second — with the current value beside it, and the two badges push each other apart when they would otherwise overlap.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.5372,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "The comparison being made, set top-left. A noun phrase naming the measure — 'Revenue, indexed' rather than 'A chart of revenue'.",
          "required": true,
          "defaultValue": "Umsatzentwicklung im Vergleich",
          "options": null
        },
        {
          "name": "subtitle",
          "type": "string",
          "description": "One line under the title for the unit or the baseline — 'Index, 1976 = 20'. This is where a chart earns trust, so say what the numbers are.",
          "required": false,
          "defaultValue": "Index, 1976 = 20",
          "options": null
        },
        {
          "name": "seriesAName",
          "type": "string",
          "description": "Legend name for the solid series.",
          "required": true,
          "defaultValue": "Aurea Systems",
          "options": null
        },
        {
          "name": "seriesBName",
          "type": "string",
          "description": "Legend name for the dashed series.",
          "required": true,
          "defaultValue": "Northlane Works",
          "options": null
        },
        {
          "name": "seriesA",
          "type": "array",
          "description": "The solid series, in order. Drawn with a round badge. Any length — the x axis divides itself evenly, so 20 points and 200 points both fill the plot.",
          "required": true,
          "defaultValue": "51 items",
          "options": null
        },
        {
          "name": "seriesB",
          "type": "array",
          "description": "The dashed series. Should be the same length as seriesA; if it is not, the shorter one stops early and the longer one keeps going, which is usually a mistake rather than a design.",
          "required": true,
          "defaultValue": "51 items",
          "options": null
        },
        {
          "name": "startLabel",
          "type": "number",
          "description": "The value of the first x position, counted up one per point and shown large in the corner. Years are the obvious case; any integer sequence works.",
          "required": false,
          "defaultValue": "1976",
          "options": null
        },
        {
          "name": "counterLabel",
          "type": "string",
          "description": "Small label above the running counter. Set to an empty string to drop it.",
          "required": false,
          "defaultValue": "Jahr",
          "options": null
        },
        {
          "name": "yMax",
          "type": "number",
          "description": "Top of the y scale. Deliberately not derived from the data: a fixed ceiling is what lets the winner visibly run out of room near the top.",
          "required": false,
          "defaultValue": "110",
          "options": null
        },
        {
          "name": "yTicks",
          "type": "array",
          "description": "Values that get a gridline and a left-hand label.",
          "required": false,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "xTickEvery",
          "type": "number",
          "description": "Label every Nth x position. 10 over 50 points gives six labels, which is about the most a frame can carry.",
          "required": false,
          "defaultValue": "10",
          "options": null
        },
        {
          "name": "colorA",
          "type": "string",
          "description": "Hex for the solid series. Empty uses the brand accent, which is the safer choice.",
          "required": false,
          "defaultValue": "",
          "options": null
        },
        {
          "name": "colorB",
          "type": "string",
          "description": "Hex for the dashed series. Empty uses the brand's alternate accent.",
          "required": false,
          "defaultValue": "",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand preset. The light presets (paper, press, field) put dark type on a pale ground; the dark ones invert it. Both are composed for — this component reads its type colour from the brand rather than assuming a dark frame.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "dashboard-shell",
      "name": "Dashboard Shell",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A product dashboard assembles itself — rail, top bar, then four cards fade up with their sparklines drawing on",
      "tags": [
        "dashboard",
        "ui mockup",
        "saas",
        "sparkline",
        "stagger",
        "screenshot"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/dashboard-shell.tsx",
      "source": "import React from \"react\";\nimport { evolvePath } from \"@remotion/paths\";\nimport { makeRect } from \"@remotion/shapes\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\n/**\n * A dashboard is read, not glanced at, so this is a `scene` (18 / 114 / 18).\n * The budget is hoisted so the component, every sub-component and the\n * registration at the bottom all quote the same numbers.\n */\nconst BUDGET = BUDGETS.scene;\n\n/**\n * The assembly schedule, in frames from the item's own frame 0, kept in one\n * place so the order of arrival is legible as a list rather than scattered\n * across twelve interpolations. Chrome first (shell, rules, logo, rail), then\n * the top bar, then the page header, then the cards. Everything lands by ~frame\n * 40 and the sparklines finish drawing by ~55, which leaves the middle of the\n * 114-frame hold as a genuinely still, designed frame.\n */\nconst AT = {\n  /** The rail and top-bar hairlines draw themselves in. */\n  rules: 2,\n  /** Logo square draws, fills, then takes its initial. */\n  logo: 1,\n  wordmark: 5,\n  /** First nav row, then `navStep` frames per row after it. */\n  nav: 6,\n  navStep: 2,\n  /** The status line pinned to the foot of the rail. */\n  railFoot: 16,\n  search: 8,\n  avatar: 12,\n  title: 10,\n  subtitle: 12,\n  range: 14,\n  /** First card, then `cardStep` frames per card in reading order. */\n  cards: 12,\n  cardStep: 3,\n} as const;\n\n/**\n * Presence for one element, delayed by `delay` frames.\n *\n * `useStaggeredStage(budget, index, perItem)` delays by `index * perItem`, so\n * passing `perItem = 1` turns the index into a frame count. Every part of this\n * scene therefore rides the shared enter curve and the shared exit window —\n * nothing here hand-rolls its own presence against raw frame numbers.\n */\nconst useDelayedStage = (delay: number) => useStaggeredStage(BUDGET, delay, 1);\n\n/**\n * Nav icons, drawn rather than imported: a registry item must work with no\n * assets and no icon dependency. All six are stroked paths on a 24×24 grid, so\n * one stroke width and one size prop keeps them optically consistent.\n */\nconst NAV_ICONS = {\n  grid: \"M3.5 3.5h7v7h-7z M13.5 3.5h7v7h-7z M3.5 13.5h7v7h-7z M13.5 13.5h7v7h-7z\",\n  chart: \"M4 20.5V12 M9.5 20.5V5.5 M15 20.5v-6.5 M20.5 20.5V9\",\n  users:\n    \"M12 3.6a3.5 3.5 0 100 7 3.5 3.5 0 000-7 M4.6 20.4c0-3.5 3.3-6.1 7.4-6.1s7.4 2.6 7.4 6.1\",\n  layers: \"M12 3.2l8.4 4.6-8.4 4.6-8.4-4.6z M3.6 12.6l8.4 4.6 8.4-4.6\",\n  doc: \"M6.2 3.5h7.4L19 8.9v11.6H6.2z M13.6 3.5v5.4H19\",\n  settings:\n    \"M12 8.7a3.3 3.3 0 100 6.6 3.3 3.3 0 000-6.6 M12 3.2v2.1 M12 18.7v2.1 M3.2 12h2.1 M18.7 12h2.1 M6 6l1.5 1.5 M16.5 16.5L18 18 M18 6l-1.5 1.5 M7.5 16.5L6 18\",\n} as const;\n\ntype NavIcon = keyof typeof NAV_ICONS;\n\n/* ------------------------------------------------------------------ schema */\n\nconst navItemSchema = z.object({\n  label: z\n    .string()\n    .describe(\"Row label. One or two words — the rail is narrow and long labels truncate.\"),\n  icon: z\n    .enum([\"grid\", \"chart\", \"users\", \"layers\", \"doc\", \"settings\"])\n    .describe(\"Which drawn glyph sits before the label. All six are stroked on the same 24px grid.\"),\n  section: z\n    .string()\n    .optional()\n    .describe(\n      \"Optional group heading rendered above this row in mono caps. Set it on the first row of each group; leave it off elsewhere.\",\n    ),\n});\n\nconst chartCardSchema = z.object({\n  label: z\n    .string()\n    .describe(\"Card label, set in mono caps. Two or three words, e.g. 'Net revenue'.\"),\n  value: z\n    .string()\n    .describe(\"The headline figure, as a formatted string so you can carry units — '$248.6k', '3.84%'.\"),\n  delta: z\n    .string()\n    .optional()\n    .describe(\"Small change figure shown beside the value in the accent colour. Omit for a plain figure.\"),\n  series: z\n    .array(z.number())\n    .describe(\n      \"The sparkline. 12–30 points reads best; values are normalised to the card, so any scale works. Two points minimum or the line is skipped.\",\n    ),\n  axis: z\n    .array(z.string())\n    .describe(\n      \"Tick labels spread evenly under the sparkline, e.g. month names. Pass an empty array for a bare line — right for the smaller card.\",\n    ),\n});\n\nexport const dashboardShellSchema = z.object({\n  appName: z\n    .string()\n    .default(\"Northwind\")\n    .describe(\"Product name in the rail. Its first letter becomes the drawn logo mark, so pick a real word.\"),\n  pageTitle: z\n    .string()\n    .default(\"Overview\")\n    .describe(\"Page heading above the card grid. One or two words, like a real product screen.\"),\n  pageSubtitle: z\n    .string()\n    .optional()\n    .describe(\"One muted line under the heading. Omit for a tighter header.\"),\n  searchPlaceholder: z\n    .string()\n    .default(\"Search metrics, cohorts, events\")\n    .describe(\"Placeholder inside the top-bar search field. Keep it plausible — it sells the mockup.\"),\n  userName: z\n    .string()\n    .default(\"Amara Okonkwo\")\n    .describe(\"Signed-in user. Only their initials are drawn, on a bordered avatar circle.\"),\n  navItems: z\n    .array(navItemSchema)\n    .default([\n      { label: \"Overview\", icon: \"grid\", section: \"Workspace\" },\n      { label: \"Analytics\", icon: \"chart\" },\n      { label: \"Audience\", icon: \"users\" },\n      { label: \"Reports\", icon: \"doc\" },\n      { label: \"Segments\", icon: \"layers\", section: \"Manage\" },\n      { label: \"Settings\", icon: \"settings\" },\n    ])\n    .describe(\"Rail rows, top to bottom. Five to eight rows fills a 16:9 rail without crowding it.\"),\n  activeNav: z\n    .number()\n    .int()\n    .min(0)\n    .default(0)\n    .describe(\"Index of the selected row. It gets the recessed pill and an accent glyph.\"),\n  railFooter: z\n    .string()\n    .optional()\n    .describe(\n      \"Status line pinned to the foot of the rail, in mono caps behind a small accent dot — an environment or version, e.g. 'Production · v2.4'. Omit to leave the rail bottom empty.\",\n    ),\n  rangeLabel: z\n    .string()\n    .default(\"Last 30 days\")\n    .describe(\"Text of the date-range control at the right of the page header.\"),\n  charts: z\n    .array(chartCardSchema)\n    .length(2)\n    .default([\n      {\n        label: \"Net revenue\",\n        value: \"$248.6k\",\n        delta: \"+8.2%\",\n        series: [18, 22, 19, 26, 24, 31, 28, 35, 33, 30, 38, 41, 37, 45, 43, 50, 48, 56, 54, 61, 66, 72],\n        axis: [\"Jun\", \"Jul\", \"Aug\", \"Sep\"],\n      },\n      {\n        label: \"Conversion\",\n        value: \"3.84%\",\n        delta: \"+0.4pt\",\n        series: [12, 14, 13, 16, 15, 18, 17, 16, 19, 21, 20, 23, 22, 25, 24, 27, 29, 28, 31, 33],\n        axis: [],\n      },\n    ])\n    .describe(\n      \"Exactly two sparkline cards: the first fills the wide top-left slot and draws in the accent colour, the second the narrow bottom-right slot in the secondary accent.\",\n    ),\n  stat: z\n    .object({\n      label: z.string().describe(\"Card label in mono caps, e.g. 'Active users'.\"),\n      value: z\n        .number()\n        .describe(\"The figure, as a number — it counts up to this on arrival and is grouped in thousands.\"),\n      delta: z.string().describe(\"Chip text beside the figure, e.g. '+12.4%'.\"),\n      direction: z\n        .enum([\"up\", \"down\"])\n        .describe(\"Chip tone and arrow. 'up' takes the accent, 'down' the secondary accent.\"),\n      footnote: z.string().describe(\"Mono caption under a hairline at the foot of the card.\"),\n    })\n    .default({\n      label: \"Active users\",\n      value: 18420,\n      delta: \"+12.4%\",\n      direction: \"up\",\n      footnote: \"vs. 16,380 previous period\",\n    })\n    .describe(\"The stat card: one large counting figure, a delta chip and a footnote.\"),\n  table: z\n    .object({\n      title: z.string().describe(\"Table label in mono caps, e.g. 'Recent activity'.\"),\n      rows: z\n        .array(\n          z.object({\n            name: z.string().describe(\"Primary text. Person or account names read best — initials become the avatar.\"),\n            meta: z.string().describe(\"Muted mono second line: what happened.\"),\n            value: z.string().describe(\"Right-aligned mono figure for the row.\"),\n          }),\n        )\n        .describe(\"Three to five rows. More than five and the hairlines crowd at this card height.\"),\n    })\n    .default({\n      title: \"Recent activity\",\n      rows: [\n        { name: \"Amara Okonkwo\", meta: \"Upgraded to Scale\", value: \"+$1,240\" },\n        { name: \"Tobias Lang\", meta: \"Added 12 seats\", value: \"+$864\" },\n        { name: \"Priya Raman\", meta: \"New workspace\", value: \"+$480\" },\n        { name: \"Jonas Weber\", meta: \"Renewed annually\", value: \"+$2,150\" },\n      ],\n    })\n    .describe(\"The activity table: a label, a head rule and hairline-separated rows.\"),\n  shadow: z\n    .boolean()\n    .default(true)\n    .describe(\"Float the window off the canvas with a soft shadow. Turn it off for a flat, printed look.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof dashboardShellSchema>;\ntype ChartSpec = Props[\"charts\"][number];\n\n/* ----------------------------------------------------------------- helpers */\n\n/** Up to two initials from a name, for the avatars. */\nconst initialsOf = (name: string) =>\n  name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((word) => word.charAt(0).toUpperCase())\n    .join(\"\") || \"—\";\n\n/**\n * Thousands grouping, written out rather than delegated to `Intl` so the\n * rendered frame cannot depend on the machine's locale.\n */\nconst groupThousands = (value: number) => {\n  const digits = Math.round(Math.abs(value)).toString();\n  let out = \"\";\n  for (let i = 0; i < digits.length; i += 1) {\n    if (i > 0 && (digits.length - i) % 3 === 0) out += \",\";\n    out += digits.charAt(i);\n  }\n  return `${value < 0 ? \"-\" : \"\"}${out}`;\n};\n\ntype SparkGeometry = { line: string; area: string; last: { x: number; y: number } };\n\n/**\n * Sparkline geometry in real pixels — not in a stretched viewBox. Sizing the\n * path to the card's actual chart box is what keeps the stroke weight equal to\n * the UI's hairlines and the end cap perfectly round; a `viewBox` with\n * `preserveAspectRatio=\"none\"` would smear both.\n *\n * Segments are cubics with control points on the segment midpoint, which gives\n * the gentle sparkline curve a chart library would draw without overshooting\n * past a data point.\n */\nconst buildSparkline = (values: number[], w: number, h: number): SparkGeometry | null => {\n  if (values.length < 2 || w <= 0 || h <= 0) return null;\n\n  let min = values[0];\n  let max = values[0];\n  for (const value of values) {\n    if (value < min) min = value;\n    if (value > max) max = value;\n  }\n  const span = max - min || 1;\n\n  const at = (i: number) => ({\n    x: (i / (values.length - 1)) * w,\n    y: h - ((values[i] - min) / span) * h,\n  });\n\n  const first = at(0);\n  let line = `M ${first.x.toFixed(2)} ${first.y.toFixed(2)}`;\n  for (let i = 1; i < values.length; i += 1) {\n    const a = at(i - 1);\n    const b = at(i);\n    const cx = (a.x + b.x) / 2;\n    line += ` C ${cx.toFixed(2)} ${a.y.toFixed(2)} ${cx.toFixed(2)} ${b.y.toFixed(2)} ${b.x.toFixed(2)} ${b.y.toFixed(2)}`;\n  }\n\n  return {\n    line,\n    area: `${line} L ${w.toFixed(2)} ${h.toFixed(2)} L 0 ${h.toFixed(2)} Z`,\n    last: at(values.length - 1),\n  };\n};\n\nconst EMPTY_CHART: ChartSpec = { label: \"—\", value: \"—\", series: [], axis: [] };\n\n/** Shared geometry, derived once from the frame and handed down. */\ntype Metrics = {\n  t: BrandTokens;\n  /** Short edge of the frame. Every size in this file is a fraction of it. */\n  unit: number;\n  /** One hairline. */\n  hair: number;\n  pad: number;\n  radiusMd: number;\n  radiusSm: number;\n};\n\n/* --------------------------------------------------------------- component */\n\nconst DashboardShell: React.FC<Props> = ({\n  appName,\n  pageTitle,\n  pageSubtitle,\n  searchPlaceholder,\n  userName,\n  navItems,\n  activeNav,\n  railFooter,\n  rangeLabel,\n  charts,\n  stat,\n  table,\n  shadow,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  // Everything types and spaces off the short edge, so the same file reads as a\n  // product screen at 16:9, 9:16, 1:1 and 4:5 without one hard-coded pixel.\n  const unit = Math.min(width, height);\n  const hair = Math.max(1, unit * 0.001);\n  const pad = unit * 0.026;\n\n  const radiusLg = Math.max(t.radius * 1.6, hair);\n  const radiusMd = Math.max(t.radius, hair);\n  const radiusSm = Math.max(t.radius * 0.6, unit * 0.002);\n\n  // The window: one inset rectangle floating on the canvas. Its own 1px border\n  // is subtracted before anything inside is measured — skip that and the bottom\n  // row of cards loses two pixels of its border to the window's clip.\n  const margin = unit * 0.072;\n  const winW = width - margin * 2;\n  const winH = height - margin * 2;\n  const railW = Math.max(winW * 0.155, unit * 0.155);\n  const barH = unit * 0.058;\n\n  // The card grid is laid out in explicit pixels rather than fractional CSS\n  // grid tracks, because the sparklines need to know their box in advance to\n  // size their own SVG. 0.615 gives the wide column the classic 3:2 dashboard\n  // split without either card looking like a leftover.\n  const contentW = winW - hair * 2 - railW;\n  const contentH = winH - hair * 2 - barH;\n  const headerH = unit * 0.072;\n  const gap = unit * 0.018;\n  const gridW = contentW - pad * 2;\n  const gridH = contentH - pad * 2 - headerH - gap;\n  const rowH = (gridH - gap) / 2;\n  const colA = (gridW - gap) * 0.615;\n  const colB = gridW - gap - colA;\n\n  const m: Metrics = { t, unit, hair, pad, radiusMd, radiusSm };\n\n  // Presence: the whole window lifts a little and settles, and leaves by fading\n  // with the faintest recede. Nothing about the interior moves on the way out —\n  // one object, one exit.\n  const shellY = interpolate(stage.enter, [0, 1], [unit * 0.018, 0]);\n  const shellScale =\n    interpolate(stage.enter, [0, 1], [0.988, 1]) * interpolate(stage.exit, [0, 1], [1, 0.994]);\n\n  // The two structural hairlines draw themselves in, which is what makes the\n  // shell read as assembling rather than simply appearing.\n  const railRule = interpolate(stage.frame, [AT.rules, AT.rules + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const barRule = interpolate(stage.frame, [AT.rules + 2, AT.rules + 18], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const chartA = charts[0] ?? EMPTY_CHART;\n  const chartB = charts[1] ?? chartA;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n        <div\n          style={{\n            width: winW,\n            height: winH,\n            boxSizing: \"border-box\",\n            display: \"flex\",\n            overflow: \"hidden\",\n            backgroundColor: t.surface,\n            border: `${hair}px solid ${t.rule}`,\n            borderRadius: radiusLg,\n            opacity: stage.presence,\n            transform: `translateY(${shellY}px) scale(${shellScale})`,\n            boxShadow: shadow\n              ? `0 ${unit * 0.002}px ${unit * 0.004}px rgba(0,0,0,0.16), 0 ${unit * 0.014}px ${unit * 0.05}px rgba(0,0,0,0.26)`\n              : \"none\",\n          }}\n        >\n          {/* ------------------------------------------------------ nav rail */}\n          <div\n            style={{\n              position: \"relative\",\n              width: railW,\n              flexShrink: 0,\n              boxSizing: \"border-box\",\n              padding: `${pad * 0.9}px ${pad * 0.62}px`,\n              display: \"flex\",\n              flexDirection: \"column\",\n            }}\n          >\n            <div style={{ display: \"flex\", alignItems: \"center\", gap: unit * 0.011 }}>\n              <LogoMark t={t} unit={unit} size={unit * 0.031} radius={radiusSm} initial={appName.charAt(0)} />\n              <Rise delay={AT.wordmark} distance={unit * 0.006}>\n                <span\n                  style={{\n                    fontFamily: t.fontDisplay,\n                    fontSize: unit * 0.019,\n                    fontWeight: 700,\n                    letterSpacing: unit * -0.0003,\n                    color: t.ink,\n                    whiteSpace: \"nowrap\",\n                  }}\n                >\n                  {appName}\n                </span>\n              </Rise>\n            </div>\n\n            {navItems.map((item, i) => (\n              <NavRow\n                key={`${item.label}-${i}`}\n                m={m}\n                item={item}\n                active={i === activeNav}\n                delay={AT.nav + i * AT.navStep}\n              />\n            ))}\n\n            <div style={{ flex: 1, minHeight: unit * 0.02 }} />\n\n            {railFooter ? (\n              <Rise delay={AT.railFoot} distance={unit * 0.006}>\n                <div\n                  style={{\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    gap: unit * 0.008,\n                    paddingTop: unit * 0.013,\n                    paddingLeft: unit * 0.009,\n                    borderTop: `${hair}px solid ${t.rule}`,\n                  }}\n                >\n                  <div\n                    style={{\n                      width: unit * 0.0045,\n                      height: unit * 0.0045,\n                      flexShrink: 0,\n                      borderRadius: \"50%\",\n                      backgroundColor: t.accent,\n                    }}\n                  />\n                  <Label t={t} size={unit * 0.0102} text={railFooter} />\n                </div>\n              </Rise>\n            ) : null}\n\n            {/* The rail's edge, drawn top-down. */}\n            <div\n              style={{\n                position: \"absolute\",\n                top: 0,\n                right: 0,\n                bottom: 0,\n                width: hair,\n                backgroundColor: t.rule,\n                transform: `scaleY(${railRule})`,\n                transformOrigin: \"top\",\n              }}\n            />\n          </div>\n\n          {/* --------------------------------------------- top bar + content */}\n          <div style={{ flex: 1, minWidth: 0, display: \"flex\", flexDirection: \"column\" }}>\n            <div\n              style={{\n                position: \"relative\",\n                height: barH,\n                flexShrink: 0,\n                display: \"flex\",\n                alignItems: \"center\",\n                padding: `0 ${pad}px`,\n                gap: pad * 0.7,\n              }}\n            >\n              <SearchField m={m} placeholder={searchPlaceholder} width={contentW * 0.31} />\n              <div style={{ flex: 1 }} />\n              <Avatar\n                m={m}\n                initials={initialsOf(userName)}\n                size={unit * 0.031}\n                delay={AT.avatar}\n                divider\n                labelSize={unit * 0.0125}\n              />\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  bottom: 0,\n                  height: hair,\n                  backgroundColor: t.rule,\n                  transform: `scaleX(${barRule})`,\n                  transformOrigin: \"left\",\n                }}\n              />\n            </div>\n\n            {/* The content canvas sits a step back from the chrome, which is\n                what lets the cards read as raised surfaces. */}\n            <div\n              style={{\n                flex: 1,\n                minHeight: 0,\n                overflow: \"hidden\",\n                backgroundColor: t.bg,\n                padding: pad,\n                display: \"flex\",\n                flexDirection: \"column\",\n              }}\n            >\n              <div\n                style={{\n                  height: headerH,\n                  flexShrink: 0,\n                  display: \"flex\",\n                  alignItems: \"flex-end\",\n                  justifyContent: \"space-between\",\n                  gap: pad,\n                }}\n              >\n                <div style={{ minWidth: 0 }}>\n                  <Rise delay={AT.title} distance={unit * 0.012}>\n                    <div\n                      style={{\n                        fontFamily: t.fontDisplay,\n                        fontSize: unit * 0.029,\n                        fontWeight: 700,\n                        letterSpacing: unit * -0.0004,\n                        lineHeight: 1.1,\n                        color: t.ink,\n                      }}\n                    >\n                      {pageTitle}\n                    </div>\n                  </Rise>\n                  {pageSubtitle ? (\n                    <Rise delay={AT.subtitle} distance={unit * 0.008}>\n                      <div\n                        style={{\n                          marginTop: unit * 0.007,\n                          fontFamily: t.fontBody,\n                          fontSize: unit * 0.0135,\n                          fontWeight: 400,\n                          lineHeight: 1.35,\n                          color: t.inkMuted,\n                        }}\n                      >\n                        {pageSubtitle}\n                      </div>\n                    </Rise>\n                  ) : null}\n                </div>\n\n                <Rise delay={AT.range} distance={unit * 0.008} style={{ flexShrink: 0 }}>\n                  <RangeControl m={m} label={rangeLabel} />\n                </Rise>\n              </div>\n\n              <div style={{ height: gap, flexShrink: 0 }} />\n\n              <div style={{ display: \"flex\", gap }}>\n                <ChartCard\n                  m={m}\n                  spec={chartA}\n                  w={colA}\n                  h={rowH}\n                  delay={AT.cards}\n                  tone={t.accent}\n                  area\n                  uid=\"a\"\n                />\n                <StatCard m={m} spec={stat} w={colB} h={rowH} delay={AT.cards + AT.cardStep} />\n              </div>\n\n              <div style={{ height: gap, flexShrink: 0 }} />\n\n              <div style={{ display: \"flex\", gap }}>\n                <TableCard m={m} spec={table} w={colA} h={rowH} delay={AT.cards + AT.cardStep * 2} />\n                <ChartCard\n                  m={m}\n                  spec={chartB}\n                  w={colB}\n                  h={rowH}\n                  delay={AT.cards + AT.cardStep * 3}\n                  tone={t.accentAlt}\n                  area={false}\n                  uid=\"b\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------- primitives */\n\n/** A block that fades and rises into place on the shared enter curve. */\nconst Rise: React.FC<{\n  delay: number;\n  distance: number;\n  style?: React.CSSProperties;\n  children: React.ReactNode;\n}> = ({ delay, distance, style, children }) => {\n  const stage = useDelayedStage(delay);\n  return (\n    <div\n      style={{\n        ...style,\n        opacity: stage.enter,\n        transform: `translateY(${interpolate(stage.enter, [0, 1], [distance, 0])}px)`,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\n/** The registry's small-label idiom: mono, caps, tracked out, muted. */\nconst Label: React.FC<{ t: BrandTokens; size: number; text: string }> = ({ t, size, text }) => (\n  <div\n    style={{\n      fontFamily: t.fontMono,\n      fontSize: size,\n      fontWeight: 500,\n      letterSpacing: size * 0.12,\n      textTransform: \"uppercase\",\n      color: t.inkMuted,\n      whiteSpace: \"nowrap\",\n      overflow: \"hidden\",\n      textOverflow: \"ellipsis\",\n    }}\n  >\n    {text}\n  </div>\n);\n\n/**\n * The logo mark builds in three beats: the rounded square is drawn on as a\n * stroke with `evolvePath`, the interior then fills with the accent, and the\n * product's initial arrives last. It is the same draw-on language as the\n * sparklines, so the scene has one gesture rather than two.\n */\nconst LogoMark: React.FC<{\n  t: BrandTokens;\n  unit: number;\n  size: number;\n  radius: number;\n  initial: string;\n}> = ({ t, unit, size, radius, initial }) => {\n  const stage = useStage(BUDGET);\n  const stroke = Math.max(1.4, unit * 0.0018);\n  const rect = makeRect({\n    width: size - stroke,\n    height: size - stroke,\n    cornerRadius: Math.min(radius, (size - stroke) / 2),\n  });\n\n  const draw = interpolate(stage.frame, [AT.logo, AT.logo + 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const fill = interpolate(stage.frame, [AT.logo + 10, AT.logo + 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const letter = interpolate(stage.frame, [AT.logo + 15, AT.logo + 24], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  return (\n    <div style={{ position: \"relative\", width: size, height: size, flexShrink: 0 }}>\n      <svg\n        width={size}\n        height={size}\n        viewBox={`0 0 ${size} ${size}`}\n        style={{ display: \"block\", position: \"absolute\", top: 0, left: 0 }}\n      >\n        <g transform={`translate(${stroke / 2} ${stroke / 2})`}>\n          <path d={rect.path} fill={t.accent} fillOpacity={fill} />\n          <path\n            d={rect.path}\n            fill=\"none\"\n            stroke={t.accent}\n            strokeWidth={stroke}\n            strokeLinecap=\"round\"\n            {...evolvePath(draw, rect.path)}\n          />\n        </g>\n      </svg>\n      <div\n        style={{\n          position: \"absolute\",\n          top: 0,\n          left: 0,\n          width: size,\n          height: size,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: t.fontDisplay,\n          fontSize: size * 0.48,\n          fontWeight: 700,\n          letterSpacing: size * -0.01,\n          color: t.accentInk,\n          opacity: letter,\n        }}\n      >\n        {initial}\n      </div>\n    </div>\n  );\n};\n\n/* -------------------------------------------------------------- nav rail */\n\nconst NavRow: React.FC<{\n  m: Metrics;\n  item: { label: string; icon: NavIcon; section?: string };\n  active: boolean;\n  delay: number;\n}> = ({ m, item, active, delay }) => {\n  const { t, unit, hair, radiusSm } = m;\n  const stage = useDelayedStage(delay);\n  // Rows arrive from the rail's own edge — a small move, in the direction the\n  // rail itself implies.\n  const x = interpolate(stage.enter, [0, 1], [-unit * 0.014, 0]);\n  const iconSize = unit * 0.019;\n  const rowHeight = unit * 0.036;\n\n  return (\n    <div style={{ opacity: stage.enter, transform: `translateX(${x}px)` }}>\n      {item.section ? (\n        <div style={{ padding: `${unit * 0.019}px ${unit * 0.009}px ${unit * 0.008}px` }}>\n          <Label t={t} size={unit * 0.0102} text={item.section} />\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: unit * 0.0105,\n          height: rowHeight,\n          padding: `0 ${unit * 0.009}px`,\n          boxSizing: \"border-box\",\n          borderRadius: radiusSm,\n          backgroundColor: active ? t.bg : \"transparent\",\n          border: `${hair}px solid ${active ? t.rule : \"transparent\"}`,\n        }}\n      >\n        <svg\n          width={iconSize}\n          height={iconSize}\n          viewBox=\"0 0 24 24\"\n          style={{ display: \"block\", flexShrink: 0 }}\n        >\n          <path\n            d={NAV_ICONS[item.icon]}\n            fill=\"none\"\n            stroke={active ? t.accent : t.inkMuted}\n            strokeWidth={1.7}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n        <span\n          style={{\n            fontFamily: t.fontBody,\n            fontSize: unit * 0.0145,\n            fontWeight: active ? 600 : 500,\n            color: active ? t.ink : t.inkMuted,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {item.label}\n        </span>\n      </div>\n    </div>\n  );\n};\n\n/* --------------------------------------------------------------- top bar */\n\nconst SearchField: React.FC<{ m: Metrics; placeholder: string; width: number }> = ({\n  m,\n  placeholder,\n  width,\n}) => {\n  const { t, unit, hair, radiusSm } = m;\n  const stage = useDelayedStage(AT.search);\n  const y = interpolate(stage.enter, [0, 1], [-unit * 0.008, 0]);\n  const fieldH = unit * 0.031;\n  const glyph = unit * 0.0155;\n\n  return (\n    <div\n      style={{\n        width,\n        height: fieldH,\n        boxSizing: \"border-box\",\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: unit * 0.008,\n        padding: `0 ${unit * 0.011}px`,\n        backgroundColor: t.bg,\n        border: `${hair}px solid ${t.rule}`,\n        borderRadius: radiusSm,\n        opacity: stage.enter,\n        transform: `translateY(${y}px)`,\n      }}\n    >\n      <svg width={glyph} height={glyph} viewBox=\"0 0 16 16\" style={{ display: \"block\", flexShrink: 0 }}>\n        <circle\n          cx={6.9}\n          cy={6.9}\n          r={4.4}\n          fill=\"none\"\n          stroke={t.inkMuted}\n          strokeWidth={1.5}\n        />\n        <path d=\"M10.2 10.2L14 14\" fill=\"none\" stroke={t.inkMuted} strokeWidth={1.5} strokeLinecap=\"round\" />\n      </svg>\n      <span\n        style={{\n          fontFamily: t.fontBody,\n          fontSize: unit * 0.0132,\n          color: t.inkMuted,\n          whiteSpace: \"nowrap\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n        }}\n      >\n        {placeholder}\n      </span>\n    </div>\n  );\n};\n\n/**\n * Initials on a bordered circle. No image, ever — and neutral rather than\n * accent-filled, so the accent stays with the data where it earns its keep.\n */\nconst Avatar: React.FC<{\n  m: Metrics;\n  initials: string;\n  size: number;\n  delay: number;\n  divider?: boolean;\n  labelSize: number;\n}> = ({ m, initials, size, delay, divider, labelSize }) => {\n  const { t, unit, hair } = m;\n  const stage = useDelayedStage(delay);\n  const scale = interpolate(stage.enter, [0, 1], [0.86, 1]);\n\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: unit * 0.013, opacity: stage.enter }}>\n      {divider ? (\n        <div style={{ width: hair, height: size * 0.95, backgroundColor: t.rule }} />\n      ) : null}\n      <div\n        style={{\n          width: size,\n          height: size,\n          flexShrink: 0,\n          boxSizing: \"border-box\",\n          borderRadius: \"50%\",\n          backgroundColor: t.bg,\n          border: `${hair}px solid ${t.rule}`,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: t.fontMono,\n          fontSize: labelSize,\n          fontWeight: 600,\n          letterSpacing: labelSize * 0.02,\n          color: t.ink,\n          transform: `scale(${scale})`,\n        }}\n      >\n        {initials}\n      </div>\n    </div>\n  );\n};\n\nconst RangeControl: React.FC<{ m: Metrics; label: string }> = ({ m, label }) => {\n  const { t, unit, hair, radiusSm } = m;\n  const glyph = unit * 0.011;\n  return (\n    <div\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: unit * 0.009,\n        height: unit * 0.029,\n        padding: `0 ${unit * 0.012}px`,\n        boxSizing: \"border-box\",\n        backgroundColor: t.surface,\n        border: `${hair}px solid ${t.rule}`,\n        borderRadius: radiusSm,\n        fontFamily: t.fontMono,\n        fontSize: unit * 0.0118,\n        color: t.inkMuted,\n        letterSpacing: unit * 0.0002,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      {label}\n      <svg width={glyph} height={glyph} viewBox=\"0 0 12 12\" style={{ display: \"block\" }}>\n        <path\n          d=\"M2.4 4.4L6 8l3.6-3.6\"\n          fill=\"none\"\n          stroke={t.inkMuted}\n          strokeWidth={1.4}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    </div>\n  );\n};\n\n/* ----------------------------------------------------------------- cards */\n\n/**\n * The card surface, and the only place the staggered fade-up lives. Four cards\n * arriving on the same curve, three frames apart, in reading order.\n */\nconst CardShell: React.FC<{\n  m: Metrics;\n  w: number;\n  h: number;\n  delay: number;\n  children: React.ReactNode;\n}> = ({ m, w, h, delay, children }) => {\n  const { t, hair, pad, radiusMd } = m;\n  const stage = useDelayedStage(delay);\n  const y = interpolate(stage.enter, [0, 1], [h * 0.07, 0]);\n\n  return (\n    <div\n      style={{\n        width: w,\n        height: h,\n        boxSizing: \"border-box\",\n        padding: pad * 0.9,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"space-between\",\n        backgroundColor: t.surface,\n        border: `${hair}px solid ${t.rule}`,\n        borderRadius: radiusMd,\n        opacity: stage.enter,\n        transform: `translateY(${y}px)`,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst ChartCard: React.FC<{\n  m: Metrics;\n  spec: ChartSpec;\n  w: number;\n  h: number;\n  delay: number;\n  tone: string;\n  area: boolean;\n  uid: string;\n}> = ({ m, spec, w, h, delay, tone, area, uid }) => {\n  const { t, unit, hair, pad } = m;\n  const stage = useDelayedStage(delay);\n\n  const innerW = w - pad * 1.8 - hair * 2;\n  // The chart takes half the card, or a little more where there are no tick\n  // labels under it, so the wide and the narrow card sit on the same rhythm.\n  const chartH = h * (spec.axis.length > 0 ? 0.5 : 0.55);\n  const stroke = Math.max(1.5, unit * 0.0022);\n  // The path is inset by one stroke on every side so the round end cap sits\n  // fully inside the chart box instead of being nudged off its own data point.\n  const plotW = innerW - stroke * 2;\n  const geo = buildSparkline(spec.series, plotW, chartH - stroke * 2);\n\n  // The line draws on once its card has landed, then the end cap appears — the\n  // scene's one accent gesture, and the reason the frame reads as data arriving.\n  const draw = interpolate(stage.frame, [8, 30], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const cap = interpolate(stage.frame, [27, 35], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const clipId = `dashboard-shell-clip-${uid}`;\n\n  return (\n    <CardShell m={m} w={w} h={h} delay={delay}>\n      <div>\n        <Label t={t} size={unit * 0.0118} text={spec.label} />\n        <div\n          style={{\n            marginTop: unit * 0.011,\n            display: \"flex\",\n            alignItems: \"baseline\",\n            gap: unit * 0.01,\n          }}\n        >\n          <span\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: unit * 0.036,\n              fontWeight: 700,\n              letterSpacing: unit * -0.0005,\n              lineHeight: 1,\n              color: t.ink,\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n          >\n            {spec.value}\n          </span>\n          {spec.delta ? (\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: unit * 0.0125,\n                fontWeight: 500,\n                color: tone,\n              }}\n            >\n              {spec.delta}\n            </span>\n          ) : null}\n        </div>\n      </div>\n\n      <div>\n        <svg\n          width={innerW}\n          height={chartH}\n          viewBox={`0 0 ${innerW} ${chartH}`}\n          style={{ display: \"block\" }}\n        >\n          <defs>\n            {/* The area fill is revealed by a rect that keeps pace with the\n                stroke, so the line is never running ahead of its own shading. */}\n            <clipPath id={clipId}>\n              <rect x={0} y={0} width={stroke + Math.max(0, plotW * draw)} height={chartH} />\n            </clipPath>\n          </defs>\n\n          {/* Two gridlines and a baseline: enough structure to read as a chart,\n              not enough to read as graph paper. */}\n          {[0.34, 0.67].map((f) => (\n            <rect\n              key={f}\n              x={0}\n              y={chartH * f}\n              width={innerW}\n              height={hair}\n              fill={t.rule}\n            />\n          ))}\n          <rect x={0} y={chartH - hair} width={innerW} height={hair} fill={t.rule} />\n\n          {geo && area ? (\n            <g transform={`translate(${stroke} ${stroke})`} clipPath={`url(#${clipId})`}>\n              <path d={geo.area} fill={tone} fillOpacity={0.1} />\n            </g>\n          ) : null}\n\n          {geo ? (\n            <g transform={`translate(${stroke} ${stroke})`}>\n              <path\n                d={geo.line}\n                fill=\"none\"\n                stroke={tone}\n                strokeWidth={stroke}\n                strokeLinecap=\"round\"\n                strokeLinejoin=\"round\"\n                {...evolvePath(draw, geo.line)}\n              />\n              <circle\n                cx={geo.last.x}\n                cy={geo.last.y}\n                r={stroke * 1.5}\n                fill={t.surface}\n                stroke={tone}\n                strokeWidth={stroke * 0.85}\n                opacity={cap}\n              />\n            </g>\n          ) : null}\n        </svg>\n\n        {spec.axis.length > 0 ? (\n          <div\n            style={{\n              marginTop: unit * 0.009,\n              display: \"flex\",\n              justifyContent: \"space-between\",\n              fontFamily: t.fontMono,\n              fontSize: unit * 0.0105,\n              letterSpacing: unit * 0.0004,\n              color: t.inkMuted,\n            }}\n          >\n            {spec.axis.map((tick, i) => (\n              <span key={`${tick}-${i}`}>{tick}</span>\n            ))}\n          </div>\n        ) : null}\n      </div>\n    </CardShell>\n  );\n};\n\nconst StatCard: React.FC<{\n  m: Metrics;\n  spec: Props[\"stat\"];\n  w: number;\n  h: number;\n  delay: number;\n}> = ({ m, spec, w, h, delay }) => {\n  const { t, unit, hair } = m;\n  const stage = useDelayedStage(delay);\n\n  // Internal motion, which the grammar permits during hold: the figure counts\n  // to its value and is finished long before the frame comes to rest.\n  const shown = interpolate(stage.frame, [6, 30], [0, spec.value], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const tone = spec.direction === \"up\" ? t.accent : t.accentAlt;\n\n  return (\n    <CardShell m={m} w={w} h={h} delay={delay}>\n      <Label t={t} size={unit * 0.0118} text={spec.label} />\n\n      <div>\n        <div\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: unit * 0.058,\n            fontWeight: 700,\n            letterSpacing: unit * -0.0009,\n            lineHeight: 1,\n            color: t.ink,\n            fontVariantNumeric: \"tabular-nums\",\n          }}\n        >\n          {groupThousands(shown)}\n        </div>\n        <div style={{ marginTop: unit * 0.015 }}>\n          <DeltaChip m={m} label={spec.delta} tone={tone} up={spec.direction === \"up\"} />\n        </div>\n      </div>\n\n      <div>\n        <div style={{ height: hair, backgroundColor: t.rule }} />\n        <div\n          style={{\n            marginTop: unit * 0.011,\n            fontFamily: t.fontMono,\n            fontSize: unit * 0.0112,\n            color: t.inkMuted,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {spec.footnote}\n        </div>\n      </div>\n    </CardShell>\n  );\n};\n\nconst DeltaChip: React.FC<{ m: Metrics; label: string; tone: string; up: boolean }> = ({\n  m,\n  label,\n  tone,\n  up,\n}) => {\n  const { t, unit, hair, radiusSm } = m;\n  const glyph = unit * 0.0092;\n  return (\n    <div\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: unit * 0.006,\n        height: unit * 0.026,\n        padding: `0 ${unit * 0.009}px`,\n        boxSizing: \"border-box\",\n        border: `${hair}px solid ${tone}`,\n        borderRadius: radiusSm,\n        fontFamily: t.fontMono,\n        fontSize: unit * 0.0118,\n        fontWeight: 500,\n        color: tone,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      <svg width={glyph} height={glyph} viewBox=\"0 0 12 12\" style={{ display: \"block\" }}>\n        <path d={up ? \"M6 2.4l4.4 7.2H1.6z\" : \"M6 9.6L1.6 2.4h8.8z\"} fill={tone} />\n      </svg>\n      {label}\n    </div>\n  );\n};\n\nconst TableCard: React.FC<{\n  m: Metrics;\n  spec: Props[\"table\"];\n  w: number;\n  h: number;\n  delay: number;\n}> = ({ m, spec, w, h, delay }) => {\n  const { t, unit, hair, pad } = m;\n  const rows = spec.rows;\n  const labelSize = unit * 0.0118;\n  // The rows divide the space left under the label and its head rule, so any\n  // row count from three to five stays on the same rhythm.\n  const bodyH = h - pad * 1.8 - hair * 2 - labelSize * 1.25 - unit * 0.014;\n  const rowHeight = rows.length > 0 ? bodyH / rows.length : bodyH;\n\n  return (\n    <CardShell m={m} w={w} h={h} delay={delay}>\n      <div>\n        <Label t={t} size={labelSize} text={spec.title} />\n        <div style={{ height: hair, backgroundColor: t.rule, marginTop: unit * 0.013 }} />\n        {rows.map((row, i) => (\n          <TableRow\n            key={`${row.name}-${i}`}\n            m={m}\n            row={row}\n            height={rowHeight}\n            first={i === 0}\n            delay={delay + 6 + i * 2}\n          />\n        ))}\n      </div>\n    </CardShell>\n  );\n};\n\nconst TableRow: React.FC<{\n  m: Metrics;\n  row: { name: string; meta: string; value: string };\n  height: number;\n  first: boolean;\n  delay: number;\n}> = ({ m, row, height, first, delay }) => {\n  const { t, unit, hair } = m;\n  const stage = useDelayedStage(delay);\n  const y = interpolate(stage.enter, [0, 1], [unit * 0.008, 0]);\n\n  return (\n    <div\n      style={{\n        height,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: unit * 0.012,\n        boxSizing: \"border-box\",\n        borderTop: first ? undefined : `${hair}px solid ${t.rule}`,\n        opacity: stage.enter,\n        transform: `translateY(${y}px)`,\n      }}\n    >\n      <Avatar\n        m={m}\n        initials={initialsOf(row.name)}\n        size={Math.min(unit * 0.028, height * 0.62)}\n        delay={delay}\n        labelSize={unit * 0.0105}\n      />\n      <div style={{ flex: 1, minWidth: 0 }}>\n        <div\n          style={{\n            fontFamily: t.fontBody,\n            fontSize: unit * 0.0145,\n            fontWeight: 500,\n            color: t.ink,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {row.name}\n        </div>\n        <div\n          style={{\n            marginTop: unit * 0.0025,\n            fontFamily: t.fontMono,\n            fontSize: unit * 0.0108,\n            color: t.inkMuted,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {row.meta}\n        </div>\n      </div>\n      <div\n        style={{\n          flexShrink: 0,\n          fontFamily: t.fontMono,\n          fontSize: unit * 0.0135,\n          color: t.ink,\n          fontVariantNumeric: \"tabular-nums\",\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {row.value}\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"dashboard-shell\",\n  name: \"Dashboard Shell\",\n  category: \"product\",\n  summary:\n    \"A product dashboard assembles itself — rail, top bar, then four cards fade up with their sparklines drawing on\",\n  tags: [\"dashboard\", \"ui mockup\", \"saas\", \"sparkline\", \"stagger\", \"screenshot\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: dashboardShellSchema,\n  defaults: {\n    appName: \"Northwind\",\n    pageTitle: \"Overview\",\n    pageSubtitle: \"Revenue, activation and retention across every workspace\",\n    searchPlaceholder: \"Search metrics, cohorts, events\",\n    userName: \"Amara Okonkwo\",\n    navItems: [\n      { label: \"Overview\", icon: \"grid\", section: \"Workspace\" },\n      { label: \"Analytics\", icon: \"chart\" },\n      { label: \"Audience\", icon: \"users\" },\n      { label: \"Reports\", icon: \"doc\" },\n      { label: \"Segments\", icon: \"layers\", section: \"Manage\" },\n      { label: \"Settings\", icon: \"settings\" },\n    ],\n    activeNav: 0,\n    railFooter: \"Production · v2.4\",\n    rangeLabel: \"Last 30 days\",\n    charts: [\n      {\n        label: \"Net revenue\",\n        value: \"$248.6k\",\n        delta: \"+8.2%\",\n        series: [18, 22, 19, 26, 24, 31, 28, 35, 33, 30, 38, 41, 37, 45, 43, 50, 48, 56, 54, 61, 66, 72],\n        axis: [\"Jun\", \"Jul\", \"Aug\", \"Sep\"],\n      },\n      {\n        label: \"Conversion\",\n        value: \"3.84%\",\n        delta: \"+0.4pt\",\n        series: [12, 14, 13, 16, 15, 18, 17, 16, 19, 21, 20, 23, 22, 25, 24, 27, 29, 28, 31, 33],\n        axis: [],\n      },\n    ],\n    stat: {\n      label: \"Active users\",\n      value: 18420,\n      delta: \"+12.4%\",\n      direction: \"up\",\n      footnote: \"vs. 16,380 previous period\",\n    },\n    table: {\n      title: \"Recent activity\",\n      rows: [\n        { name: \"Amara Okonkwo\", meta: \"Upgraded to Scale\", value: \"+$1,240\" },\n        { name: \"Tobias Lang\", meta: \"Added 12 seats\", value: \"+$864\" },\n        { name: \"Priya Raman\", meta: \"New workspace\", value: \"+$480\" },\n        { name: \"Jonas Weber\", meta: \"Renewed annually\", value: \"+$2,150\" },\n      ],\n    },\n    shadow: true,\n    brand: \"slate\",\n  },\n  component: DashboardShell,\n  file: \"src/registry/items/dashboard-shell.tsx\",\n  origin: {\n    prompt:\n      \"The generic SaaS dashboard, assembling. A left nav rail with a logo square that draws itself and nav rows sliding in, a top bar with a search field and an avatar, then a content grid of four cards that fade up in a stagger — two with tiny sparklines that draw on, one big stat with a delta chip, one small table of hairline rows. Hairline rules everywhere, 1px borders, surface panels on the canvas colour. It should look like a real product, not a toy.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "The generic SaaS dashboard, assembling. A left nav rail with a logo square that draws itself and nav rows sliding in, a top bar with a search field and an avatar, then a content grid of four cards that fade up in a stagger — two with tiny sparklines that draw on, one big stat with a delta chip, one small table of hairline rows. Hairline rules everywhere, 1px borders, surface panels on the canvas colour. It should look like a real product, not a toy.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2774,
      "props": [
        {
          "name": "appName",
          "type": "string",
          "description": "Product name in the rail. Its first letter becomes the drawn logo mark, so pick a real word.",
          "required": false,
          "defaultValue": "Northwind",
          "options": null
        },
        {
          "name": "pageTitle",
          "type": "string",
          "description": "Page heading above the card grid. One or two words, like a real product screen.",
          "required": false,
          "defaultValue": "Overview",
          "options": null
        },
        {
          "name": "pageSubtitle",
          "type": "string",
          "description": "One muted line under the heading. Omit for a tighter header.",
          "required": false,
          "defaultValue": "Revenue, activation and retention across every workspace",
          "options": null
        },
        {
          "name": "searchPlaceholder",
          "type": "string",
          "description": "Placeholder inside the top-bar search field. Keep it plausible — it sells the mockup.",
          "required": false,
          "defaultValue": "Search metrics, cohorts, events",
          "options": null
        },
        {
          "name": "userName",
          "type": "string",
          "description": "Signed-in user. Only their initials are drawn, on a bordered avatar circle.",
          "required": false,
          "defaultValue": "Amara Okonkwo",
          "options": null
        },
        {
          "name": "navItems",
          "type": "array",
          "description": "Rail rows, top to bottom. Five to eight rows fills a 16:9 rail without crowding it.",
          "required": false,
          "defaultValue": "6 items",
          "options": null
        },
        {
          "name": "activeNav",
          "type": "number",
          "description": "Index of the selected row. It gets the recessed pill and an accent glyph.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "railFooter",
          "type": "string",
          "description": "Status line pinned to the foot of the rail, in mono caps behind a small accent dot — an environment or version, e.g. 'Production · v2.4'. Omit to leave the rail bottom empty.",
          "required": false,
          "defaultValue": "Production · v2.4",
          "options": null
        },
        {
          "name": "rangeLabel",
          "type": "string",
          "description": "Text of the date-range control at the right of the page header.",
          "required": false,
          "defaultValue": "Last 30 days",
          "options": null
        },
        {
          "name": "charts",
          "type": "array",
          "description": "Exactly two sparkline cards: the first fills the wide top-left slot and draws in the accent colour, the second the narrow bottom-right slot in the secondary accent.",
          "required": false,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "stat",
          "type": "object",
          "description": "The stat card: one large counting figure, a delta chip and a footnote.",
          "required": false,
          "defaultValue": "object",
          "options": null
        },
        {
          "name": "table",
          "type": "object",
          "description": "The activity table: a label, a head rule and hairline-separated rows.",
          "required": false,
          "defaultValue": "object",
          "options": null
        },
        {
          "name": "shadow",
          "type": "boolean",
          "description": "Float the window off the canvas with a soft shadow. Turn it off for a flat, printed look.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "depth-field-cards",
      "name": "Depth Field Cards",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A field of cards on four depth planes with real depth of field and aerial perspective, revealed by one long lateral camera move",
      "tags": [
        "depth of field",
        "parallax",
        "camera move",
        "product",
        "cards",
        "premium"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/depth-field-cards.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const depthFieldCardsSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"The line that sits in the sharp plane. Three to six words.\"),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\"Supporting line under the headline, in muted ink.\"),\n  heroLabel: z\n    .string()\n    .default(\"Registry\")\n    .describe(\"Label on the card that holds the focal plane.\"),\n  heroMetric: z\n    .string()\n    .default(\"76\")\n    .describe(\"The figure on the focal card. Two or three characters.\"),\n  heroUnit: z\n    .string()\n    .default(\"components\")\n    .describe(\"Unit under the focal card's figure.\"),\n  aperture: z\n    .number()\n    .min(0.2)\n    .max(1)\n    .default(0.7)\n    .describe(\"Depth of field. Higher throws the near and far planes further out of focus.\"),\n  drift: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.55)\n    .describe(\"How far the camera travels laterally across the shot. The parallax between planes is what makes the space read as deep.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof depthFieldCardsSchema>;\n\nconst BUDGET = BUDGETS.feature;\nconst SEED = \"depth-field-cards\";\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.84\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * A card in the field.\n *\n * Deliberately low-detail: at every depth except the focal plane these are\n * blurred, and a card that renders real content only to have it destroyed by a\n * 20px blur is wasted render time. What matters at depth is silhouette, edge\n * light and the suggestion of structure.\n */\nconst Card: React.FC<{\n  w: number;\n  h: number;\n  tone: number;\n  t: typeof DEFAULT_BRAND;\n  accent: boolean;\n}> = ({ w, h, tone, t, accent }) => (\n  <div\n    style={{\n      width: w,\n      height: h,\n      borderRadius: t.radius * 1.6,\n      background: `linear-gradient(155deg, ${alpha(t.surface, 0.96)} 0%, ${alpha(\n        t.surface,\n        0.7,\n      )} 58%, ${alpha(t.bg, 0.9)} 100%)`,\n      border: `1px solid ${alpha(t.ink, 0.09)}`,\n      boxShadow: `inset 0 1px 0 0 ${alpha(\"#fff\", 0.07)}, 0 ${h * 0.06}px ${h * 0.16}px ${alpha(\n        \"#000\",\n        0.4,\n      )}`,\n      padding: w * 0.075,\n      display: \"flex\",\n      flexDirection: \"column\",\n      gap: h * 0.055,\n      opacity: tone,\n    }}\n  >\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: w * 0.05 }}>\n      <div\n        style={{\n          width: w * 0.11,\n          height: w * 0.11,\n          borderRadius: t.radius * 0.8,\n          background: accent\n            ? `linear-gradient(140deg, ${alpha(t.accent, 0.95)} 0%, ${alpha(t.accent, 0.4)} 100%)`\n            : alpha(t.ink, 0.14),\n        }}\n      />\n      <div style={{ flex: 1, height: w * 0.028, borderRadius: 99, backgroundColor: alpha(t.ink, 0.22) }} />\n    </div>\n    <div style={{ width: \"72%\", height: w * 0.05, borderRadius: 99, backgroundColor: alpha(t.ink, 0.16) }} />\n    <div style={{ flex: 1, borderRadius: t.radius, backgroundColor: alpha(t.ink, 0.06) }} />\n    <div style={{ display: \"flex\", gap: w * 0.04 }}>\n      <div style={{ width: \"34%\", height: w * 0.03, borderRadius: 99, backgroundColor: alpha(t.ink, 0.14) }} />\n      <div style={{ width: \"22%\", height: w * 0.03, borderRadius: 99, backgroundColor: alpha(t.accent, 0.4) }} />\n    </div>\n  </div>\n);\n\ntype Plane = { z: number; count: number; y: number };\n\n/**\n * Four depth planes rather than free-floating z values.\n *\n * Discrete planes are how matte painters and title designers have always faked\n * depth, and they beat scattering cards through a continuous volume: each plane\n * gets one blur value and one parallax rate, so the eye reads clean separation\n * instead of mush.\n */\nconst PLANES: Plane[] = [\n  { z: 0.12, count: 5, y: 0.2 },\n  { z: 0.34, count: 4, y: 0.62 },\n  { z: 0.62, count: 3, y: 0.26 },\n  { z: 1, count: 0, y: 0.5 },\n];\n\nconst FOCUS_Z = 1;\n\nconst DepthFieldCards: React.FC<Props> = ({\n  headline,\n  kicker,\n  heroLabel,\n  heroMetric,\n  heroUnit,\n  aperture,\n  drift,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  /**\n   * The camera.\n   *\n   * One continuous lateral move for the whole eight seconds, eased at both ends\n   * so it never starts or stops abruptly. Everything else in the frame is\n   * static — the depth comes entirely from these planes sliding past each other\n   * at different rates.\n   */\n  const cam = interpolate(stage.progress, [0, 1], [-1, 1], { easing: EASING.travel }) * drift;\n  const push = interpolate(stage.progress, [0, 1], [1, 1.05]);\n\n  const heroIn = interpolate(frame, [8, 40], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const headIn = interpolate(frame, [22, 52], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const kickIn = interpolate(frame, [36, 66], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* Atmosphere. The far planes sit in this, which is what stops them from\n          reading as small cards rather than distant ones. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(110% 70% at 68% 18%, ${alpha(t.surface, 0.8)} 0%, ${alpha(\n            t.surface,\n            0.3,\n          )} 34%, ${alpha(t.bg, 0.85)} 68%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(58% 46% at 22% 84%, ${alpha(t.accent, 0.2)} 0%, ${alpha(\n            t.accent,\n            0,\n          )} 66%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {PLANES.filter((p) => p.count > 0).map((plane, pi) => {\n          const dist = Math.abs(plane.z - FOCUS_Z);\n          const blur = dist * width * 0.016 * aperture;\n          // Nearer planes travel further for the same camera move.\n          const parallax = cam * width * 0.09 * plane.z;\n          const scale = 0.42 + plane.z * 0.62;\n          const cardW = width * 0.19 * scale * 1.5;\n          const cardH = cardW * 1.28;\n          // Aerial perspective: distant things lose contrast toward the\n          // background before they lose sharpness.\n          const tone = 0.3 + plane.z * 0.7;\n\n          return (\n            <div\n              key={plane.z}\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                filter: `blur(${blur}px)`,\n                transform: `translateX(${parallax}px)`,\n              }}\n            >\n              {Array.from({ length: plane.count }).map((_, i) => {\n                const jx = random(`${SEED}-x-${pi}-${i}`);\n                const jy = random(`${SEED}-y-${pi}-${i}`);\n                const jr = random(`${SEED}-r-${pi}-${i}`);\n                const x = ((i + 0.5) / plane.count) * 1.34 - 0.17 + (jx - 0.5) * 0.1;\n                const y = plane.y + (jy - 0.5) * 0.34;\n                return (\n                  <div\n                    key={i}\n                    style={{\n                      position: \"absolute\",\n                      left: x * width,\n                      top: y * height,\n                      transform: `translate(-50%, -50%) rotate(${(jr - 0.5) * 5}deg)`,\n                    }}\n                  >\n                    <Card w={cardW} h={cardH} tone={tone} t={t} accent={i % 3 === 0} />\n                  </div>\n                );\n              })}\n            </div>\n          );\n        })}\n\n        {/* ------------------------------------------------ the focal plane */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: width * 0.05,\n            padding: `0 ${width * 0.09}px`,\n            transform: `translateX(${cam * width * 0.09}px)`,\n          }}\n        >\n          <div\n            style={{\n              flexShrink: 0,\n              transform: `translateY(${interpolate(heroIn, [0, 1], [height * 0.07, 0])}px) scale(${interpolate(\n                heroIn,\n                [0, 1],\n                [0.94, 1],\n              )})`,\n              opacity: heroIn,\n              filter: `blur(${interpolate(heroIn, [0, 1], [width * 0.006, 0])}px)`,\n            }}\n          >\n            <div\n              style={{\n                width: width * 0.235,\n                borderRadius: t.radius * 2,\n                background: `linear-gradient(155deg, ${alpha(t.surface, 1)} 0%, ${alpha(\n                  t.surface,\n                  0.82,\n                )} 60%, ${alpha(t.bg, 0.95)} 100%)`,\n                border: `1px solid ${alpha(t.ink, 0.12)}`,\n                boxShadow: `inset 0 1px 0 0 ${alpha(\"#fff\", 0.1)}, 0 ${height * 0.03}px ${\n                  height * 0.09\n                }px ${alpha(\"#000\", 0.55)}`,\n                padding: width * 0.022,\n              }}\n            >\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: width * 0.0115,\n                  fontWeight: 500,\n                  letterSpacing: width * 0.0016,\n                  textTransform: \"uppercase\",\n                  color: t.accent,\n                }}\n              >\n                {heroLabel}\n              </div>\n              <div\n                style={{\n                  marginTop: width * 0.012,\n                  fontFamily: t.fontDisplay,\n                  fontSize: width * 0.078,\n                  fontWeight: 700,\n                  lineHeight: 0.94,\n                  letterSpacing: width * -0.003,\n                  color: t.ink,\n                }}\n              >\n                {heroMetric}\n              </div>\n              <div\n                style={{\n                  marginTop: width * 0.006,\n                  fontFamily: t.fontBody,\n                  fontSize: width * 0.0135,\n                  color: t.inkMuted,\n                }}\n              >\n                {heroUnit}\n              </div>\n              <div\n                style={{\n                  marginTop: width * 0.018,\n                  height: Math.max(2, width * 0.0022),\n                  width: \"100%\",\n                  borderRadius: 99,\n                  background: `linear-gradient(90deg, ${alpha(t.accent, 0.9)} 0%, ${alpha(\n                    t.accentAlt,\n                    0.7,\n                  )} 68%, ${alpha(t.accent, 0)} 100%)`,\n                  transform: `scaleX(${heroIn})`,\n                  transformOrigin: \"left\",\n                }}\n              />\n            </div>\n          </div>\n\n          <div style={{ minWidth: 0 }}>\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: width * 0.062,\n                fontWeight: 700,\n                lineHeight: 1.05,\n                letterSpacing: width * -0.0022,\n                color: t.ink,\n                opacity: headIn,\n                transform: `translateY(${interpolate(headIn, [0, 1], [width * 0.016, 0])}px)`,\n                filter: `blur(${interpolate(headIn, [0, 1], [width * 0.004, 0])}px)`,\n              }}\n            >\n              {headline}\n            </div>\n            {kicker ? (\n              <div\n                style={{\n                  marginTop: width * 0.016,\n                  maxWidth: \"78%\",\n                  fontFamily: t.fontBody,\n                  fontSize: width * 0.0165,\n                  lineHeight: 1.5,\n                  color: t.inkMuted,\n                  opacity: kickIn,\n                  transform: `translateY(${interpolate(kickIn, [0, 1], [width * 0.01, 0])}px)`,\n                }}\n              >\n                {kicker}\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(120% 86% at 46% 46%, ${alpha(\"#000\", 0)} 28%, ${alpha(\n            \"#000\",\n            0.18,\n          )} 58%, ${alpha(\"#000\", 0.5)} 88%, ${alpha(\"#000\", 0.68)} 100%)`,\n        }}\n      />\n      <Grain id=\"depth-field-grain\" frame={frame} opacity={0.075} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"depth-field-cards\",\n  name: \"Depth Field Cards\",\n  category: \"product\",\n  summary:\n    \"A field of cards on four depth planes with real depth of field and aerial perspective, revealed by one long lateral camera move\",\n  tags: [\"depth of field\", \"parallax\", \"camera move\", \"product\", \"cards\", \"premium\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: depthFieldCardsSchema,\n  defaults: {\n    headline: \"One registry, every format\",\n    kicker:\n      \"Each component sizes itself from the composition, so the same file renders 16:9, 9:16, 1:1 and 4:5.\",\n    heroLabel: \"Registry\",\n    heroMetric: \"76\",\n    heroUnit: \"components\",\n    aperture: 0.7,\n    drift: 0.55,\n    brand: \"circuit\",\n  },\n  component: DepthFieldCards,\n  file: \"src/registry/items/depth-field-cards.tsx\",\n  origin: {\n    prompt:\n      \"A wide product frame with genuine depth: cards arranged on several distinct depth planes, each plane with its own blur and its own parallax rate, so one slow lateral camera move across the whole shot reveals the space. Distant planes should lose contrast into the background as well as sharpness, the way aerial perspective works. Keep one card and the headline in the sharp focal plane.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A wide product frame with genuine depth: cards arranged on several distinct depth planes, each plane with its own blur and its own parallax rate, so one slow lateral camera move across the whole shot reveals the space. Distant planes should lose contrast into the background as well as sharpness, the way aerial perspective works. Keep one card and the headline in the sharp focal plane.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.8441,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "The line that sits in the sharp plane. Three to six words.",
          "required": true,
          "defaultValue": "One registry, every format",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "Supporting line under the headline, in muted ink.",
          "required": false,
          "defaultValue": "Each component sizes itself from the composition, so the …",
          "options": null
        },
        {
          "name": "heroLabel",
          "type": "string",
          "description": "Label on the card that holds the focal plane.",
          "required": false,
          "defaultValue": "Registry",
          "options": null
        },
        {
          "name": "heroMetric",
          "type": "string",
          "description": "The figure on the focal card. Two or three characters.",
          "required": false,
          "defaultValue": "76",
          "options": null
        },
        {
          "name": "heroUnit",
          "type": "string",
          "description": "Unit under the focal card's figure.",
          "required": false,
          "defaultValue": "components",
          "options": null
        },
        {
          "name": "aperture",
          "type": "number",
          "description": "Depth of field. Higher throws the near and far planes further out of focus.",
          "required": false,
          "defaultValue": "0.7",
          "options": null
        },
        {
          "name": "drift",
          "type": "number",
          "description": "How far the camera travels laterally across the shot. The parallax between planes is what makes the space read as deep.",
          "required": false,
          "defaultValue": "0.55",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "device-rise",
      "name": "Device Rise",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A phone rises into an overhead light pool, un-tilting as it lands, then the screen ignites and a rim light crosses the case",
      "tags": [
        "device",
        "product",
        "vertical",
        "reflection",
        "contact shadow",
        "rim light",
        "premium"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/device-rise.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const deviceRiseSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"Line above the device. Two to five words; it lands before the device arrives.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\"Quiet line under the headline. Omit when the device should carry the frame alone.\"),\n  screenTitle: z\n    .string()\n    .default(\"Today\")\n    .describe(\"The large title drawn on the device screen.\"),\n  screenMetric: z\n    .string()\n    .default(\"4,182\")\n    .describe(\"The hero figure on the screen. Kept short — it is read in half a second.\"),\n  screenUnit: z\n    .string()\n    .default(\"steps\")\n    .describe(\"Unit label beside the figure on the screen.\"),\n  tilt: z\n    .number()\n    .min(0)\n    .max(22)\n    .default(11)\n    .describe(\"Degrees of perspective the device starts at before settling flat. 0 is a locked front-on rise; above 16 starts to read as a 3D render.\"),\n  brand: z\n    .string()\n    .default(\"signal\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof deviceRiseSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\n/**\n * Film grain. Steps on a 15fps cadence, which is where grain stops reading as\n * video noise, and doubles as the fix for banding in the floor light pool.\n */\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.85\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * The screen.\n *\n * Drawn rather than imported, which is the constraint that makes this component\n * portable — and, incidentally, the reason it never goes out of date. Kept to\n * one figure and three rows: a mockup that tries to show a whole product reads\n * as clutter at this size, and the screen is on frame for two seconds.\n */\nconst Screen: React.FC<{\n  w: number;\n  h: number;\n  ignite: number;\n  title: string;\n  metric: string;\n  unit: string;\n  t: typeof DEFAULT_BRAND;\n}> = ({ w, h, ignite, title, metric, unit, t }) => {\n  const pad = w * 0.085;\n  const rowIn = (i: number) =>\n    interpolate(ignite, [0.25 + i * 0.13, 0.7 + i * 0.13], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    });\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        overflow: \"hidden\",\n        // The screen's own glow, brightening as it ignites — a lit display is\n        // brighter at its centre than at its edges.\n        background: `radial-gradient(120% 80% at 50% 22%, ${alpha(t.surface, 1)} 0%, ${alpha(\n          t.bg,\n          1,\n        )} 68%, ${alpha(\"#000\", 0.35)} 100%)`,\n        opacity: ignite,\n      }}\n    >\n      {/* status row */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: pad * 0.72,\n          left: pad,\n          right: pad,\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          alignItems: \"center\",\n          fontFamily: t.fontMono,\n          fontSize: w * 0.038,\n          letterSpacing: w * 0.001,\n          color: t.inkMuted,\n          opacity: rowIn(0),\n        }}\n      >\n        <span>9:41</span>\n        <div style={{ display: \"flex\", gap: w * 0.022, alignItems: \"center\" }}>\n          <div style={{ width: w * 0.05, height: w * 0.022, borderRadius: 2, backgroundColor: alpha(t.inkMuted, 0.75) }} />\n          <div style={{ width: w * 0.03, height: w * 0.022, borderRadius: 2, backgroundColor: alpha(t.inkMuted, 0.5) }} />\n        </div>\n      </div>\n\n      {/* title */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: h * 0.13,\n          left: pad,\n          right: pad,\n          fontFamily: t.fontDisplay,\n          fontSize: w * 0.115,\n          fontWeight: 700,\n          letterSpacing: w * -0.004,\n          color: t.ink,\n          opacity: rowIn(1),\n          transform: `translateY(${interpolate(rowIn(1), [0, 1], [w * 0.03, 0])}px)`,\n        }}\n      >\n        {title}\n      </div>\n\n      {/* the hero figure */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: h * 0.24,\n          left: pad,\n          right: pad,\n          display: \"flex\",\n          alignItems: \"baseline\",\n          gap: w * 0.035,\n          opacity: rowIn(2),\n          transform: `translateY(${interpolate(rowIn(2), [0, 1], [w * 0.035, 0])}px)`,\n        }}\n      >\n        <span\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: w * 0.27,\n            fontWeight: 700,\n            lineHeight: 1,\n            letterSpacing: w * -0.012,\n            color: t.accent,\n          }}\n        >\n          {metric}\n        </span>\n        <span\n          style={{\n            fontFamily: t.fontBody,\n            fontSize: w * 0.06,\n            fontWeight: 500,\n            color: t.inkMuted,\n          }}\n        >\n          {unit}\n        </span>\n      </div>\n\n      {/* an activity ramp, drawn as bars that grow with the ignition */}\n      <div\n        style={{\n          position: \"absolute\",\n          top: h * 0.42,\n          left: pad,\n          right: pad,\n          height: h * 0.16,\n          display: \"flex\",\n          alignItems: \"flex-end\",\n          gap: w * 0.018,\n        }}\n      >\n        {[0.34, 0.52, 0.41, 0.78, 0.62, 1, 0.71].map((v, i) => (\n          <div\n            key={i}\n            style={{\n              flex: 1,\n              height: `${v * 100 * Math.min(1, rowIn(2 + i * 0.12))}%`,\n              borderRadius: w * 0.012,\n              background: `linear-gradient(180deg, ${alpha(t.accent, 0.95)} 0%, ${alpha(\n                t.accent,\n                0.4,\n              )} 100%)`,\n            }}\n          />\n        ))}\n      </div>\n\n      {/* two list rows */}\n      {[0, 1].map((i) => (\n        <div\n          key={i}\n          style={{\n            position: \"absolute\",\n            left: pad,\n            right: pad,\n            top: h * (0.64 + i * 0.115),\n            height: h * 0.095,\n            borderRadius: t.radius * 1.4,\n            backgroundColor: alpha(t.ink, 0.05),\n            border: `1px solid ${alpha(t.ink, 0.07)}`,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: w * 0.05,\n            paddingLeft: w * 0.06,\n            opacity: rowIn(3 + i * 0.5),\n            transform: `translateY(${interpolate(rowIn(3 + i * 0.5), [0, 1], [w * 0.04, 0])}px)`,\n          }}\n        >\n          <div\n            style={{\n              width: w * 0.09,\n              height: w * 0.09,\n              borderRadius: \"50%\",\n              background: `linear-gradient(140deg, ${alpha(i === 0 ? t.accent : t.accentAlt, 0.9)} 0%, ${alpha(\n                i === 0 ? t.accent : t.accentAlt,\n                0.35,\n              )} 100%)`,\n            }}\n          />\n          <div style={{ flex: 1 }}>\n            <div style={{ width: `${58 - i * 14}%`, height: w * 0.024, borderRadius: 99, backgroundColor: alpha(t.ink, 0.34) }} />\n            <div style={{ marginTop: w * 0.022, width: `${34 - i * 8}%`, height: w * 0.02, borderRadius: 99, backgroundColor: alpha(t.ink, 0.16) }} />\n          </div>\n        </div>\n      ))}\n    </div>\n  );\n};\n\n/**\n * The device body: bezel, screen, and the specular edge.\n *\n * The bezel is two stacked gradients — a broad diagonal for the body and a\n * one-pixel inner ring for the machined edge. That inner ring is most of what\n * separates \"a rounded rectangle\" from \"an object\": real hardware has a bright\n * chamfer where the case meets the glass.\n */\nconst Device: React.FC<{\n  w: number;\n  h: number;\n  ignite: number;\n  rim: number;\n  t: typeof DEFAULT_BRAND;\n  title: string;\n  metric: string;\n  unit: string;\n  /** Position, 0–1, of the reflection sliding across the glass. */\n  sheen: number;\n}> = ({ w, h, ignite, rim, t, title, metric, unit, sheen }) => {\n  const radius = w * 0.13;\n  const bezel = w * 0.022;\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: w,\n        height: h,\n        borderRadius: radius,\n        overflow: \"hidden\",\n        background: `linear-gradient(145deg, ${alpha(t.ink, 0.26)} 0%, ${alpha(t.rule, 0.9)} 22%, ${alpha(\n          t.bg,\n          1,\n        )} 50%, ${alpha(t.rule, 0.75)} 78%, ${alpha(t.ink, 0.2)} 100%)`,\n        padding: bezel,\n        boxShadow: `inset 0 0 0 1px ${alpha(\"#fff\", 0.1)}, inset 0 1px 0 0 ${alpha(\"#fff\", 0.16)}, 0 ${\n          h * 0.02\n        }px ${h * 0.06}px ${alpha(\"#000\", 0.5)}`,\n      }}\n    >\n      {/* The travelling rim light.\n       *\n       * Painted UNDER the screen rather than masked into a ring: the display is\n       * opaque and sits above this in paint order, so the highlight can only\n       * ever show in the bezel margin. That gets the same result as a\n       * mask-composite ring without depending on mask-composite, which is the\n       * kind of CSS that quietly does nothing in a headless renderer and leaves\n       * a white bar across the product shot. */}\n      <div\n        aria-hidden\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          opacity: rim > 0 && rim < 1 ? 1 : 0,\n          background: `linear-gradient(100deg, ${alpha(\"#fff\", 0)} ${rim * 100 - 14}%, ${alpha(\n            \"#fff\",\n            0.45,\n          )} ${rim * 100 - 3}%, ${alpha(\"#fff\", 0.92)} ${rim * 100}%, ${alpha(\"#fff\", 0.45)} ${\n            rim * 100 + 3\n          }%, ${alpha(\"#fff\", 0)} ${rim * 100 + 14}%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"relative\",\n          width: \"100%\",\n          height: \"100%\",\n          borderRadius: radius - bezel,\n          overflow: \"hidden\",\n          backgroundColor: t.bg,\n        }}\n      >\n        <Screen w={w - bezel * 2} h={h - bezel * 2} ignite={ignite} title={title} metric={metric} unit={unit} t={t} />\n\n        {/* Glass reflection. Slides continuously rather than sitting still: the\n            device holds its position through the whole hold, so this is the only\n            thing left telling the viewer it is a physical object under a light\n            rather than a rectangle with a picture in it. Low contrast, so it\n            never competes with the screen content. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            pointerEvents: \"none\",\n            background: `linear-gradient(122deg, ${alpha(\"#fff\", 0)} ${sheen * 100 - 42}%, ${alpha(\n              \"#fff\",\n              0.035,\n            )} ${sheen * 100 - 14}%, ${alpha(\"#fff\", 0.085)} ${sheen * 100}%, ${alpha(\n              \"#fff\",\n              0.03,\n            )} ${sheen * 100 + 16}%, ${alpha(\"#fff\", 0)} ${sheen * 100 + 44}%)`,\n          }}\n        />\n      </div>\n\n    </div>\n  );\n};\n\nconst DeviceRise: React.FC<Props> = ({\n  headline,\n  caption,\n  screenTitle,\n  screenMetric,\n  screenUnit,\n  tilt,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const sheen = (((frame / fps) * 0.17) % 1.9) - 0.45;\n\n  const dw = width * 0.6;\n  const dh = dw * 2.03;\n\n  /**\n   * The rise.\n   *\n   * A long, heavy ease over 34 frames rather than a spring: a bounce on an\n   * object this size reads as light and plastic. The device also un-tilts as it\n   * arrives, so the perspective resolves at the same moment the motion stops —\n   * two changes landing together read as one gesture.\n   */\n  const rise = interpolate(frame, [4, 38], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const y = interpolate(rise, [0, 1], [height * 0.62, 0]);\n  const rotX = interpolate(rise, [0, 1], [tilt, 0]);\n  const scale = interpolate(rise, [0, 1], [0.92, 1]);\n\n  // Screen ignites once the device is nearly at rest, never during the move.\n  const ignite = interpolate(frame, [32, 58], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  // One rim pass, after ignition.\n  const rim = interpolate(frame, [60, 86], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  /**\n   * Contact shadow.\n   *\n   * The physics that sells the rise: near the floor the shadow is small, dark\n   * and tight; as the object lifts it grows, softens and loses density. Getting\n   * this backwards — a constant shadow — is the most common tell in motion work.\n   */\n  const lift = 1 - rise;\n  const shadowW = dw * interpolate(lift, [0, 1], [0.86, 0.52]);\n  const shadowBlur = width * interpolate(lift, [0, 1], [0.075, 0.022]);\n  const shadowOpacity = interpolate(lift, [0, 1], [0.55, 0.78]) * rise;\n\n  const headStage = useStage(BUDGET);\n  const headIn = interpolate(headStage.frame, [0, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const capIn = interpolate(headStage.frame, [10, 32], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1.03, 1]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* Overhead light pool — the reason the floor is not a flat colour. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(80% 42% at 50% 12%, ${alpha(t.surface, 0.9)} 0%, ${alpha(\n            t.surface,\n            0.42,\n          )} 30%, ${alpha(t.bg, 0.6)} 62%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: height * 0.52,\n          height: height * 0.48,\n          background: `radial-gradient(60% 70% at 50% 0%, ${alpha(t.accent, 0.16 * ignite)} 0%, ${alpha(\n            t.accent,\n            0,\n          )} 70%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 46%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* type, above the device */}\n        <div\n          style={{\n            position: \"absolute\",\n            top: height * 0.1,\n            left: width * 0.09,\n            right: width * 0.09,\n            textAlign: \"center\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: width * 0.093,\n              fontWeight: 700,\n              lineHeight: 1.06,\n              letterSpacing: width * -0.0034,\n              color: t.ink,\n              opacity: headIn,\n              transform: `translateY(${interpolate(headIn, [0, 1], [width * 0.03, 0])}px)`,\n              filter: `blur(${interpolate(headIn, [0, 1], [width * 0.006, 0])}px)`,\n            }}\n          >\n            {headline}\n          </div>\n          {caption ? (\n            <div\n              style={{\n                marginTop: width * 0.028,\n                fontFamily: t.fontBody,\n                fontSize: width * 0.032,\n                fontWeight: 400,\n                lineHeight: 1.45,\n                color: t.inkMuted,\n                opacity: capIn,\n                transform: `translateY(${interpolate(capIn, [0, 1], [width * 0.02, 0])}px)`,\n              }}\n            >\n              {caption}\n            </div>\n          ) : null}\n        </div>\n\n        {/* the stage floor: shadow, then reflection, then the device */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: \"50%\",\n            top: height * 0.335,\n            width: dw,\n            marginLeft: -dw / 2,\n            perspective: width * 2.2,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: dh * 0.99,\n              width: shadowW,\n              height: shadowW * 0.13,\n              marginLeft: -shadowW / 2,\n              borderRadius: \"50%\",\n              backgroundColor: alpha(\"#000\", shadowOpacity),\n              filter: `blur(${shadowBlur}px)`,\n            }}\n          />\n\n          <div\n            style={{\n              transform: `translateY(${y}px) rotateX(${rotX}deg) scale(${scale})`,\n              transformOrigin: \"50% 100%\",\n            }}\n          >\n            <Device\n              w={dw}\n              h={dh}\n              ignite={ignite}\n              rim={rim}\n              t={t}\n              title={screenTitle}\n              metric={screenMetric}\n              unit={screenUnit}\n              sheen={sheen}\n            />\n\n            {/* Floor reflection: the same body, flipped, blurred, and faded out\n                downward with a mask so it dies into the floor instead of\n                stopping at an edge. */}\n            <div\n              style={{\n                position: \"absolute\",\n                top: dh,\n                left: 0,\n                width: dw,\n                height: dh * 0.42,\n                overflow: \"hidden\",\n                opacity: 0.24 * rise,\n                transform: \"scaleY(-1)\",\n                transformOrigin: \"50% 0%\",\n                WebkitMaskImage: `linear-gradient(to top, ${alpha(\"#000\", 0)} 0%, ${alpha(\n                  \"#000\",\n                  0.35,\n                )} 55%, #000 100%)`,\n                maskImage: `linear-gradient(to top, ${alpha(\"#000\", 0)} 0%, ${alpha(\n                  \"#000\",\n                  0.35,\n                )} 55%, #000 100%)`,\n                filter: `blur(${width * 0.008}px)`,\n              }}\n            >\n              <div style={{ position: \"absolute\", bottom: 0, left: 0, width: dw, height: dh }}>\n                <Device\n                  w={dw}\n                  h={dh}\n                  ignite={ignite * 0.8}\n                  rim={0}\n                  t={t}\n                  title={screenTitle}\n                  metric={screenMetric}\n                  unit={screenUnit}\n                  sheen={sheen}\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(115% 80% at 50% 38%, ${alpha(\"#000\", 0)} 34%, ${alpha(\n            \"#000\",\n            0.14,\n          )} 62%, ${alpha(\"#000\", 0.42)} 88%, ${alpha(\"#000\", 0.62)} 100%)`,\n        }}\n      />\n      <Grain id=\"device-rise-grain\" frame={frame} opacity={0.07} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"device-rise\",\n  name: \"Device Rise\",\n  category: \"product\",\n  summary:\n    \"A phone rises into an overhead light pool, un-tilting as it lands, then the screen ignites and a rim light crosses the case\",\n  tags: [\"device\", \"product\", \"vertical\", \"reflection\", \"contact shadow\", \"rim light\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: deviceRiseSchema,\n  defaults: {\n    headline: \"Every metric, at a glance\",\n    caption: \"Drawn in code. Nothing to re-record when the product ships.\",\n    screenTitle: \"Today\",\n    screenMetric: \"4,182\",\n    screenUnit: \"steps\",\n    tilt: 11,\n    brand: \"signal\",\n  },\n  component: DeviceRise,\n  file: \"src/registry/items/device-rise.tsx\",\n  origin: {\n    prompt:\n      \"An Apple-style device rise for a vertical ad. A phone should lift up from below the frame into an overhead pool of light, tilted in perspective and straightening as it settles, on a long heavy ease with no bounce. The contact shadow underneath has to behave: tight and dark near the floor, wide and soft as it lifts. Once it lands, the screen ignites and its content staggers in, then a single rim highlight travels across the case edge. Add a blurred floor reflection, a vignette and film grain.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An Apple-style device rise for a vertical ad. A phone should lift up from below the frame into an overhead pool of light, tilted in perspective and straightening as it settles, on a long heavy ease with no bounce. The contact shadow underneath has to behave: tight and dark near the floor, wide and soft as it lifts. Once it lands, the screen ignites and its content staggers in, then a single rim highlight travels across the case edge. Add a blurred floor reflection, a vignette and film grain.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7049,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "Line above the device. Two to five words; it lands before the device arrives.",
          "required": true,
          "defaultValue": "Every metric, at a glance",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Quiet line under the headline. Omit when the device should carry the frame alone.",
          "required": false,
          "defaultValue": "Drawn in code. Nothing to re-record when the product ships.",
          "options": null
        },
        {
          "name": "screenTitle",
          "type": "string",
          "description": "The large title drawn on the device screen.",
          "required": false,
          "defaultValue": "Today",
          "options": null
        },
        {
          "name": "screenMetric",
          "type": "string",
          "description": "The hero figure on the screen. Kept short — it is read in half a second.",
          "required": false,
          "defaultValue": "4,182",
          "options": null
        },
        {
          "name": "screenUnit",
          "type": "string",
          "description": "Unit label beside the figure on the screen.",
          "required": false,
          "defaultValue": "steps",
          "options": null
        },
        {
          "name": "tilt",
          "type": "number",
          "description": "Degrees of perspective the device starts at before settling flat. 0 is a locked front-on rise; above 16 starts to read as a 3D render.",
          "required": false,
          "defaultValue": "11",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "signal",
          "options": null
        }
      ]
    },
    {
      "id": "donut-progress-ring",
      "name": "Donut Progress Ring",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "One KPI as an arc sweeping from twelve o'clock, with a centre figure that lands its last digit on the frame the arc completes",
      "tags": [
        "donut",
        "progress ring",
        "gauge",
        "kpi",
        "dasharray",
        "counter"
      ],
      "aspect": "1:1",
      "width": 1080,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/donut-progress-ring.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const donutProgressRingSchema = z.object({\n  value: z\n    .number()\n    .min(0)\n    .describe(\n      \"The figure the arc sweeps to and the centre counts to. This is the shot — one number, under four digits reads best.\",\n    ),\n  max: z\n    .number()\n    .positive()\n    .default(100)\n    .describe(\n      \"The value that fills the ring completely. Leave at 100 for a percentage; set it to a quota or a headcount to show 'value of max' as a share of a full turn.\",\n    ),\n  from: z\n    .number()\n    .min(0)\n    .default(0)\n    .describe(\n      \"Where the arc and the count start. 0 reads as 'filling up'; a nearby value reads as 'moving' and keeps the gesture short.\",\n    ),\n  suffix: z\n    .string()\n    .default(\"%\")\n    .describe(\n      \"Unit set after the figure at just under half its size, quiet — '%', 'x', 'k', 'GB'. Empty string for a bare figure.\",\n    ),\n  decimals: z\n    .number()\n    .int()\n    .min(0)\n    .max(2)\n    .default(0)\n    .describe(\n      \"Fixed decimal places on the centre figure. 0 for a clean percentage; 1 when the last digit is the point of the shot.\",\n    ),\n  label: z\n    .string()\n    .optional()\n    .describe(\n      \"Small uppercase mono line at the top left — what the metric is. Two to five words; it sits above a hairline that frames the card.\",\n    ),\n  caption: z\n    .string()\n    .optional()\n    .describe(\n      \"Quiet mono line inside the ring, under the figure: the absolute numbers behind the share. Keep it to about five words or it crowds the well.\",\n    ),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\n      \"Bottom-left mono line: the period, the source, the caveat. One short clause — this is the fine print, not a second caption.\",\n    ),\n  target: z\n    .number()\n    .min(0)\n    .optional()\n    .describe(\n      \"Optional threshold, in the same units as `value`. Draws a notch across the track and a readout bottom right; the notch resolves to the second accent the moment the arc passes it. Omit for a bare gauge.\",\n    ),\n  targetLabel: z\n    .string()\n    .default(\"Target\")\n    .describe(\"Wording for the target readout, e.g. 'Target', 'Goal', 'Q3 pace'. Set in uppercase mono next to the figure.\"),\n  thickness: z\n    .number()\n    .min(0.02)\n    .max(0.2)\n    .default(0.085)\n    .describe(\n      \"Track stroke width, as a fraction of the gauge's overall footprint. 0.03 reads as a hairline dial, 0.085 as a donut, 0.16 as a heavy band. A thicker track leaves less room in the well, and the figure shrinks to fit it.\",\n    ),\n  ticks: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Draw the graticule outside the track — ten minor ticks plus a longer one at twelve o'clock marking zero. Turn off for a plainer, softer card.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof donutProgressRingSchema>;\n\nconst BUDGET = BUDGETS.statement;\nconst EXIT_START = BUDGET.enter + BUDGET.hold;\n\n/**\n * The sweep window.\n *\n * Deliberately longer than the enter window: presence is finished inside\n * `enter`, but an arc filling and a figure counting are the canonical piece of\n * internal motion the budget allows to run on into the hold. It still lands a\n * full second before the exit window opens, so the held frame is at rest.\n */\nconst SWEEP_START = 5;\nconst SWEEP_FRAMES = BUDGET.enter + 25;\n/**\n * The frame the arc's leading cap reaches its final angle. Both the arc and the\n * figure are driven by one eased scalar that hits exactly 1 here, and the figure\n * is quantised toward `from` (see `shown` below), so the last digit flips on\n * precisely this frame — not two frames early, which is what makes a counter\n * inside a ring read as one instrument rather than two.\n */\nconst SWEEP_END = SWEEP_START + SWEEP_FRAMES;\n\n/* ---------------------------------------------------------- ring geometry */\n\n/*\n * All ring geometry is expressed in the SVG's own 100x100 viewBox rather than in\n * pixels, so the drawing is resolution-independent: the same numbers compose the\n * gauge whether it renders at 400px in a 1:1 square or at 900px in a 16:9 frame.\n * Only type sizes are in px, and those are derived from the measured ring.\n */\n\n/** Keeps the outermost mark just inside the viewBox so nothing can clip. */\nconst EDGE = 0.5;\n/** Gap between the outer edge of the track and the graticule. */\nconst TICK_GAP = 2.2;\nconst TICK_LEN = 2.4;\n/** The twelve o'clock tick is longer: it marks zero, which is where the arc starts. */\nconst ORIGIN_TICK_LEN = 4.4;\n/** How far the target notch oversails the track on each side. */\nconst NOTCH_OVER = 1.8;\n/** Minor ticks. Ten divisions is the reading a percentage asks for. */\nconst TICK_COUNT = 10;\n/*\n * Mark weights, also in viewBox units: one unit is a hundredth of the gauge, so\n * these resolve to 1-3px hairlines at any ring size. Setting them in units\n * rather than px is what keeps the graticule a graticule in a 400px preview and\n * in a 4K render alike.\n */\nconst TICK_W = 0.22;\nconst ORIGIN_TICK_W = 0.3;\nconst NOTCH_W = 0.5;\n\n/* --------------------------------------------------------- type metrics */\n\n/**\n * Advance widths in em of the figure size. Declaring them rather than measuring\n * the glyphs is what lets the centre readout reserve its final width up front:\n * the digits fill a fixed slot from the left while the unit stays pinned, so\n * nothing slides sideways as the count runs through 9 to 10 to 100.\n */\nconst DIGIT_EM = 0.615;\nconst MARK_EM = 0.3;\n/** Suffix size, as a fraction of the figure size. */\nconst SUFFIX_EM = 0.44;\n/** Per-character advance of the suffix, in its own em. */\nconst SUFFIX_ADV = 0.62;\n/** Line box height in em — shared by figure and suffix so they sit in one row. */\nconst CELL_EM = 1.1;\n/** Approximate (ascent - descent) / 2 for the display face, used to drop the\n *  smaller suffix onto the figure's baseline rather than its centre. */\nconst BASELINE_EM = 0.3;\n\nconst fmt = (n: number, decimals: number) => n.toFixed(decimals);\n\n/** Width of a formatted figure, in em of the figure size. */\nconst slotEmOf = (text: string) =>\n  text\n    .split(\"\")\n    .reduce((acc, ch) => acc + (ch === \".\" || ch === \",\" ? MARK_EM : DIGIT_EM), 0);\n\n/** Point on the ring: degrees clockwise from twelve o'clock, in viewBox units. */\nconst polar = (deg: number, radius: number) => {\n  const a = ((deg - 90) * Math.PI) / 180;\n  return { x: 50 + radius * Math.cos(a), y: 50 + radius * Math.sin(a) };\n};\n\nconst DonutProgressRing: React.FC<Props> = ({\n  value,\n  max,\n  from,\n  suffix,\n  decimals,\n  label,\n  caption,\n  footnote,\n  target,\n  targetLabel,\n  thickness,\n  ticks,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  const span = Math.max(max, 1e-6);\n  const unit = Math.min(width, height);\n  const pad = unit * 0.085;\n  const hairline = Math.max(1, unit * 0.0016);\n\n  /* ------------------------------------------------------------- the sweep */\n\n  // ONE scalar drives the arc and the figure. Everything else about the\n  // synchronisation follows from not computing it twice.\n  const sweep = interpolate(stage.frame, [SWEEP_START, SWEEP_END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const swept = from + (value - from) * sweep;\n\n  // The figure is quantised toward `from`, so it can only display the target\n  // once `swept` has actually arrived at it. A plain round() would show the\n  // final digit two or three frames early, where the eased curve flattens.\n  const scale = 10 ** decimals;\n  const rising = value >= from;\n  const shown =\n    sweep >= 1\n      ? value\n      : (rising ? Math.floor(swept * scale) : Math.ceil(swept * scale)) / scale;\n  const shownText = fmt(shown, decimals);\n\n  // Values over `max` cap the ring rather than lapping it — a second turn would\n  // read as a lower number than the first.\n  const arcFrac = Math.min(1, Math.max(0, swept / span));\n\n  /* ---------------------------------------------------------- ring sizing */\n\n  const strokeW = thickness * 100;\n  const outerRoom = Math.max(\n    ticks ? TICK_GAP + ORIGIN_TICK_LEN : 0,\n    target === undefined ? 0 : NOTCH_OVER,\n  );\n  const r = 50 - EDGE - outerRoom - strokeW / 2;\n\n  const ringSize = Math.min(width - pad * 2, (height - pad * 2) * 0.78, unit * 0.62);\n  /** Diameter of the well inside the track, in px — the box the readout gets. */\n  const innerDia = (ringSize * 2 * (r - strokeW / 2)) / 100;\n\n  // The figure is set as large as the well will take it: capped against the well\n  // and against the measured width of this particular number, so \"9%\" and\n  // \"128.5k\" are both composed rather than cropped.\n  const slotEm = Math.max(slotEmOf(fmt(value, decimals)), slotEmOf(fmt(from, decimals)));\n  const suffixEm = suffix ? suffix.length * SUFFIX_ADV * SUFFIX_EM + 0.06 : 0;\n  const figureSize = Math.min(innerDia * 0.42, (innerDia * 0.78) / (slotEm + suffixEm));\n\n  /* ------------------------------------------------------------- the beats */\n\n  // The label leads, the hairlines draw outward from opposite ends of the frame,\n  // the arc sweeps, and the caption arrives as the count lands.\n  const labelIn = interpolate(stage.frame, [0, 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const ruleTop = interpolate(stage.frame, [2, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const ruleBottom = interpolate(stage.frame, [5, 23], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const metaIn = interpolate(stage.frame, [14, 28], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const captionIn = interpolate(\n    stage.frame,\n    [Math.min(SWEEP_END - 10, EXIT_START - 20), Math.min(SWEEP_END + 2, EXIT_START - 8)],\n    [0, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.enter },\n  );\n\n  // Presence: the header drops in from above, the footer rises from below, and\n  // the gauge settles out of a barely-there scale-up. All three are gated by the\n  // stage, so an overlapping scene cross-fades cleanly against this one.\n  const headerY = interpolate(stage.enter, [0, 1], [-unit * 0.018, 0]);\n  const footerY = interpolate(stage.enter, [0, 1], [unit * 0.018, 0]);\n  const ringScale = interpolate(stage.enter, [0, 1], [0.955, 1]);\n\n  // The target notch and its readout resolve to the second accent as the arc's\n  // leading edge crosses them — a state change, not a second gesture. If the\n  // value never reaches the target they simply stay muted, which is the point.\n  const reached =\n    target === undefined\n      ? 0\n      : interpolate(swept, [target - span * 0.012, target], [0, 1], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n        });\n  const notchColor = interpolateColors(reached, [0, 1], [t.inkMuted, t.accentAlt]);\n\n  const labelSize = unit * 0.0235;\n  const metaSize = unit * 0.019;\n  const hasFooter = Boolean(footnote) || target !== undefined;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        flexDirection: \"column\",\n        padding: pad,\n      }}\n    >\n      <div\n        style={{\n          flex: 1,\n          display: \"flex\",\n          flexDirection: \"column\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* Header: the metric, anchored left, over a hairline that spans the\n            content width and draws from the same edge. */}\n        <div style={{ transform: `translateY(${headerY}px)` }}>\n          {label ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: labelSize,\n                fontWeight: 600,\n                letterSpacing: labelSize * 0.13,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                marginBottom: labelSize * 0.85,\n                opacity: labelIn,\n                transform: `translateY(${interpolate(labelIn, [0, 1], [labelSize * 0.5, 0])}px)`,\n              }}\n            >\n              {label}\n            </div>\n          ) : null}\n          <Hairline color={t.rule} thickness={hairline} draw={ruleTop} origin=\"left\" />\n        </div>\n\n        {/* The gauge, centred in what the header and footer leave behind. */}\n        <div\n          style={{\n            flex: 1,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <div\n            style={{\n              position: \"relative\",\n              width: ringSize,\n              height: ringSize,\n              transform: `scale(${ringScale})`,\n            }}\n          >\n            <Ring\n              size={ringSize}\n              radius={r}\n              strokeW={strokeW}\n              arcFrac={arcFrac}\n              targetFrac={target === undefined ? null : Math.min(1, target / span)}\n              ticks={ticks}\n              trackColor={t.rule}\n              arcColor={t.accent}\n              tickColor={t.rule}\n              originTickColor={t.inkMuted}\n              notchColor={notchColor}\n            />\n\n            {/* The readout sits in the well, dead centre of the ring. */}\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                display: \"flex\",\n                flexDirection: \"column\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n              }}\n            >\n              <Figure\n                text={shownText}\n                slotEm={slotEm}\n                suffix={suffix}\n                size={figureSize}\n                font={t.fontDisplay}\n                color={t.ink}\n                suffixColor={t.inkMuted}\n              />\n              {caption ? (\n                <div\n                  style={{\n                    marginTop: figureSize * 0.1,\n                    maxWidth: innerDia * 0.7,\n                    fontFamily: t.fontMono,\n                    fontSize: figureSize * 0.145,\n                    fontWeight: 500,\n                    lineHeight: 1.45,\n                    color: t.inkMuted,\n                    textAlign: \"center\",\n                    opacity: captionIn,\n                    transform: `translateY(${interpolate(captionIn, [0, 1], [figureSize * 0.06, 0])}px)`,\n                  }}\n                >\n                  {caption}\n                </div>\n              ) : null}\n            </div>\n          </div>\n        </div>\n\n        {/* Footer: the fine print left, the threshold right, over a hairline\n            that draws from the opposite edge to the header's. */}\n        <div style={{ transform: `translateY(${footerY}px)` }}>\n          <Hairline color={t.rule} thickness={hairline} draw={ruleBottom} origin=\"right\" />\n          {hasFooter ? (\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"baseline\",\n                justifyContent: \"space-between\",\n                gap: metaSize * 2,\n                marginTop: metaSize * 1.1,\n                opacity: metaIn,\n              }}\n            >\n              <span\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: metaSize,\n                  fontWeight: 500,\n                  color: t.inkMuted,\n                }}\n              >\n                {footnote ?? \"\"}\n              </span>\n              {target === undefined ? null : (\n                <span\n                  style={{\n                    display: \"flex\",\n                    alignItems: \"baseline\",\n                    gap: metaSize * 0.55,\n                    whiteSpace: \"nowrap\",\n                    fontFamily: t.fontMono,\n                    fontSize: metaSize,\n                  }}\n                >\n                  <span\n                    style={{\n                      fontWeight: 500,\n                      letterSpacing: metaSize * 0.12,\n                      textTransform: \"uppercase\",\n                      color: t.inkMuted,\n                    }}\n                  >\n                    {targetLabel}\n                  </span>\n                  <span\n                    style={{\n                      fontWeight: 600,\n                      color: notchColor,\n                      fontVariantNumeric: \"tabular-nums\",\n                      fontFeatureSettings: '\"tnum\" 1',\n                    }}\n                  >\n                    {`${fmt(target, decimals)}${suffix}`}\n                  </span>\n                </span>\n              )}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * The gauge itself: a full-circle track, an accent arc cut out of an identical\n * circle by `strokeDasharray`, and the marks that let you read it.\n *\n * The arc is one `<circle>` with a two-part dash pattern — a dash exactly as\n * long as the arc, then a gap as long as the whole circumference — rotated so\n * the dash begins at twelve o'clock and runs clockwise. That is one attribute\n * per frame rather than a rebuilt path, which is why an arc is drawn this way\n * and not with an `A` command.\n */\nconst Ring: React.FC<{\n  size: number;\n  radius: number;\n  strokeW: number;\n  arcFrac: number;\n  targetFrac: number | null;\n  ticks: boolean;\n  trackColor: string;\n  arcColor: string;\n  tickColor: string;\n  originTickColor: string;\n  notchColor: string;\n}> = ({\n  size,\n  radius,\n  strokeW,\n  arcFrac,\n  targetFrac,\n  ticks,\n  trackColor,\n  arcColor,\n  tickColor,\n  originTickColor,\n  notchColor,\n}) => {\n  const circumference = 2 * Math.PI * radius;\n  const arcSpan = circumference * arcFrac;\n\n  // Round caps add half a stroke width beyond each end of the dash. Shortening\n  // the dash by a full stroke and rotating the start by the same arc angle puts\n  // the *visible* ends exactly on zero and on the value: at 100% the two caps\n  // meet at twelve o'clock instead of overlapping into a bump, and at 40% the\n  // leading edge is genuinely at 40%. This is the tell that a gauge was drawn\n  // rather than approximated.\n  const capDeg = ((strokeW / 2) / radius) * (180 / Math.PI);\n  const dash = Math.max(arcSpan - strokeW, 0.01);\n  // Below roughly a third of a stroke width there is no honest arc to draw —\n  // a round cap alone is already wider than the value asks for.\n  const showArc = arcSpan > strokeW * 0.35;\n\n  const trackOuter = radius + strokeW / 2;\n  const trackInner = radius - strokeW / 2;\n\n  // The threshold notch cuts across the track and oversails it on both sides, so\n  // it reads whether or not the arc has reached it yet.\n  const notch =\n    targetFrac === null\n      ? null\n      : {\n          from: polar(targetFrac * 360, trackInner - NOTCH_OVER),\n          to: polar(targetFrac * 360, trackOuter + NOTCH_OVER),\n        };\n\n  const minorTicks: number[] = [];\n  for (let i = 1; i < TICK_COUNT; i += 1) minorTicks.push((i * 360) / TICK_COUNT);\n\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 100 100\" style={{ display: \"block\" }}>\n      {ticks ? (\n        <g>\n          {minorTicks.map((deg) => {\n            const a = polar(deg, trackOuter + TICK_GAP);\n            const b = polar(deg, trackOuter + TICK_GAP + TICK_LEN);\n            return (\n              <line\n                key={`tick-${deg}`}\n                x1={a.x}\n                y1={a.y}\n                x2={b.x}\n                y2={b.y}\n                stroke={tickColor}\n                strokeWidth={TICK_W}\n              />\n            );\n          })}\n          {/* Zero. The arc leaves from here, so it gets the long tick. */}\n          <line\n            x1={50}\n            y1={50 - (trackOuter + TICK_GAP)}\n            x2={50}\n            y2={50 - (trackOuter + TICK_GAP + ORIGIN_TICK_LEN)}\n            stroke={originTickColor}\n            strokeWidth={ORIGIN_TICK_W}\n          />\n        </g>\n      ) : null}\n\n      <circle\n        cx={50}\n        cy={50}\n        r={radius}\n        fill=\"none\"\n        stroke={trackColor}\n        strokeWidth={strokeW}\n      />\n\n      {showArc ? (\n        <circle\n          cx={50}\n          cy={50}\n          r={radius}\n          fill=\"none\"\n          stroke={arcColor}\n          strokeWidth={strokeW}\n          strokeLinecap=\"round\"\n          strokeDasharray={`${dash} ${circumference}`}\n          transform={`rotate(${-90 + capDeg} 50 50)`}\n        />\n      ) : null}\n\n      {notch === null ? null : (\n        <line\n          x1={notch.from.x}\n          y1={notch.from.y}\n          x2={notch.to.x}\n          y2={notch.to.y}\n          stroke={notchColor}\n          strokeWidth={NOTCH_W}\n          strokeLinecap=\"round\"\n        />\n      )}\n    </svg>\n  );\n};\n\n/**\n * The centre readout: digits in a fixed slot, unit pinned beside them.\n *\n * The slot is sized from the *final* figure and the digits are right-aligned in\n * it, so the unit never moves while the count runs and, at rest, the pair is\n * exactly centred in the well. Tabular figures do the rest.\n */\nconst Figure: React.FC<{\n  text: string;\n  slotEm: number;\n  suffix: string;\n  size: number;\n  font: string;\n  color: string;\n  suffixColor: string;\n}> = ({ text, slotEm, suffix, size, font, color, suffixColor }) => {\n  const cellH = size * CELL_EM;\n  const suffixSize = size * SUFFIX_EM;\n  return (\n    <div style={{ display: \"flex\", flexDirection: \"row\", alignItems: \"flex-start\" }}>\n      <div\n        style={{\n          width: slotEm * size,\n          height: cellH,\n          lineHeight: `${cellH}px`,\n          textAlign: \"right\",\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 700,\n          letterSpacing: size * -0.022,\n          color,\n          fontVariantNumeric: \"tabular-nums\",\n          fontFeatureSettings: '\"tnum\" 1',\n        }}\n      >\n        {text}\n      </div>\n      {suffix ? (\n        <div\n          style={{\n            marginLeft: size * 0.06,\n            height: cellH,\n            lineHeight: `${cellH}px`,\n            fontFamily: font,\n            fontSize: suffixSize,\n            fontWeight: 600,\n            color: suffixColor,\n            transform: `translateY(${(size - suffixSize) * BASELINE_EM}px)`,\n          }}\n        >\n          {suffix}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/** A hairline that draws from one end. Two of them, mirrored, frame the card. */\nconst Hairline: React.FC<{\n  color: string;\n  thickness: number;\n  draw: number;\n  origin: \"left\" | \"right\";\n}> = ({ color, thickness, draw, origin }) => (\n  <div\n    style={{\n      width: \"100%\",\n      height: thickness,\n      backgroundColor: color,\n      transform: `scaleX(${draw})`,\n      transformOrigin: origin,\n    }}\n  />\n);\n\nexport default defineMotionItem({\n  id: \"donut-progress-ring\",\n  name: \"Donut Progress Ring\",\n  category: \"data\",\n  summary:\n    \"One KPI as an arc sweeping from twelve o'clock, with a centre figure that lands its last digit on the frame the arc completes\",\n  tags: [\"donut\", \"progress ring\", \"gauge\", \"kpi\", \"dasharray\", \"counter\"],\n  budget: BUDGETS.statement,\n  aspect: \"1:1\",\n  schema: donutProgressRingSchema,\n  defaults: {\n    value: 78,\n    max: 100,\n    from: 0,\n    suffix: \"%\",\n    decimals: 0,\n    label: \"New ARR · annual target\",\n    caption: \"€4.2M of a €5.4M goal\",\n    footnote: \"Fiscal Q3 · updated weekly\",\n    target: 70,\n    targetLabel: \"Q3 pace\",\n    thickness: 0.085,\n    ticks: true,\n    brand: \"slate\",\n  },\n  component: DonutProgressRing,\n  file: \"src/registry/items/donut-progress-ring.tsx\",\n  origin: {\n    prompt:\n      \"A square KPI card built around a donut gauge: a thin track ring in the hairline colour with a small graticule outside it, an accent arc sweeping clockwise from twelve o'clock with round caps, and the percentage counting up in the middle in the display face with tabular figures and a quiet mono caption underneath. The count has to finish on the exact frame the arc stops — that synchronisation is the whole point. Put a notch on the ring for the target that lights up when the arc passes it, and frame the card with a small label top left and a footnote bottom left.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A square KPI card built around a donut gauge: a thin track ring in the hairline colour with a small graticule outside it, an accent arc sweeping clockwise from twelve o'clock with round caps, and the percentage counting up in the middle in the display face with tabular figures and a quiet mono caption underneath. The count has to finish on the exact frame the arc stops — that synchronisation is the whole point. Put a notch on the ring for the target that lights up when the arc passes it, and frame the card with a small label top left and a footnote bottom left.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.339,
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The figure the arc sweeps to and the centre counts to. This is the shot — one number, under four digits reads best.",
          "required": true,
          "defaultValue": "78",
          "options": null
        },
        {
          "name": "max",
          "type": "number",
          "description": "The value that fills the ring completely. Leave at 100 for a percentage; set it to a quota or a headcount to show 'value of max' as a share of a full turn.",
          "required": false,
          "defaultValue": "100",
          "options": null
        },
        {
          "name": "from",
          "type": "number",
          "description": "Where the arc and the count start. 0 reads as 'filling up'; a nearby value reads as 'moving' and keeps the gesture short.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "suffix",
          "type": "string",
          "description": "Unit set after the figure at just under half its size, quiet — '%', 'x', 'k', 'GB'. Empty string for a bare figure.",
          "required": false,
          "defaultValue": "%",
          "options": null
        },
        {
          "name": "decimals",
          "type": "number",
          "description": "Fixed decimal places on the centre figure. 0 for a clean percentage; 1 when the last digit is the point of the shot.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "label",
          "type": "string",
          "description": "Small uppercase mono line at the top left — what the metric is. Two to five words; it sits above a hairline that frames the card.",
          "required": false,
          "defaultValue": "New ARR · annual target",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Quiet mono line inside the ring, under the figure: the absolute numbers behind the share. Keep it to about five words or it crowds the well.",
          "required": false,
          "defaultValue": "€4.2M of a €5.4M goal",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Bottom-left mono line: the period, the source, the caveat. One short clause — this is the fine print, not a second caption.",
          "required": false,
          "defaultValue": "Fiscal Q3 · updated weekly",
          "options": null
        },
        {
          "name": "target",
          "type": "number",
          "description": "Optional threshold, in the same units as `value`. Draws a notch across the track and a readout bottom right; the notch resolves to the second accent the moment the arc passes it. Omit for a bare gauge.",
          "required": false,
          "defaultValue": "70",
          "options": null
        },
        {
          "name": "targetLabel",
          "type": "string",
          "description": "Wording for the target readout, e.g. 'Target', 'Goal', 'Q3 pace'. Set in uppercase mono next to the figure.",
          "required": false,
          "defaultValue": "Q3 pace",
          "options": null
        },
        {
          "name": "thickness",
          "type": "number",
          "description": "Track stroke width, as a fraction of the gauge's overall footprint. 0.03 reads as a hairline dial, 0.085 as a donut, 0.16 as a heavy band. A thicker track leaves less room in the well, and the figure shrinks to fit it.",
          "required": false,
          "defaultValue": "0.085",
          "options": null
        },
        {
          "name": "ticks",
          "type": "boolean",
          "description": "Draw the graticule outside the track — ten minor ticks plus a longer one at twelve o'clock marking zero. Turn off for a plainer, softer card.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "doodle-card-stack",
      "name": "Doodle Card Stack",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "Hand-drawn marker cards drop into a stack bottom row first, each outline wobbled off true by its own seed",
      "tags": [
        "illustrated",
        "hand drawn",
        "light",
        "vertical",
        "stack",
        "playful",
        "marker"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/doodle-card-stack.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const doodleCardStackSchema = z.object({\n  title: z\n    .string()\n    .optional()\n    .describe(\"Hand-lettered line above the stack. Omit to let the stack carry the frame alone.\"),\n  rows: z\n    .array(z.number().int().min(1).max(5))\n    .min(2)\n    .max(5)\n    .describe(\"Cards per row, bottom row FIRST. [4,3,2,1] builds a pyramid; [3,3,3] builds a wall. Keep rows under five or the cards get too small to read.\"),\n  wobble: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.55)\n    .describe(\"How hand-drawn the outlines are. 0 is a clean vector rectangle; 1 is a loose marker sketch.\"),\n  crown: z\n    .boolean()\n    .default(true)\n    .describe(\"Pop a marker star above the top card once the stack completes.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\"Brand token id — one of the keys of BRANDS. Written for the light brands.\"),\n});\n\ntype Props = z.infer<typeof doodleCardStackSchema>;\n\nconst BUDGET = BUDGETS.scene;\nconst SEED = \"doodle-card-stack\";\n\n/**\n * A rounded rectangle drawn by hand.\n *\n * `border-radius` cannot produce this: the character of a marker outline is that\n * no two edges are the same distance from true, and the line thickens where the\n * pen slowed at the corners. So the perimeter is walked as points, each pushed\n * off its own normal by a seeded amount, and emitted as one closed path. The\n * seed is per-card, so every card is wrong in its own way — which is what stops\n * a stack of them looking like a repeated asset.\n */\nconst wobblyRect = (w: number, h: number, r: number, seed: string, amp: number) => {\n  const pts: [number, number][] = [];\n  const per = 9;\n  const corners: [number, number, number][] = [\n    [w - r, h - r, 0],\n    [r, h - r, 90],\n    [r, r, 180],\n    [w - r, r, 270],\n  ];\n  for (const [ccx, ccy, from] of corners) {\n    for (let i = 0; i <= per; i += 1) {\n      const a = ((from + (i / per) * 90) * Math.PI) / 180;\n      pts.push([ccx + Math.cos(a) * r, ccy + Math.sin(a) * r]);\n    }\n  }\n\n  let d = \"\";\n  pts.forEach(([x, y], i) => {\n    const nx = x - w / 2;\n    const ny = y - h / 2;\n    const len = Math.hypot(nx, ny) || 1;\n    const push = (random(`${seed}-${i}`) - 0.5) * amp;\n    const px = x + (nx / len) * push;\n    const py = y + (ny / len) * push;\n    d += `${i === 0 ? \"M\" : \"L\"}${px.toFixed(1)} ${py.toFixed(1)}`;\n  });\n  return `${d}Z`;\n};\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"multiply\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.9\"\n        numOctaves={4}\n        seed={Math.floor(frame / 3) % 48}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst DoodleCardStack: React.FC<Props> = ({ title, rows, wobble, crown, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const widest = Math.max(...rows);\n  const gap = width * 0.028;\n  const cardW = (width * 0.88 - gap * (widest - 1)) / widest;\n  const cardH = cardW * 0.62;\n  // A marker line is FAT. At 0.028 the outlines read as a hairline border and\n  // the whole illustrated register collapses back into ordinary UI cards.\n  const stroke = Math.max(4, cardW * 0.045);\n  const amp = wobble * cardW * 0.055;\n\n  // Four flat fills in rotation. Flat colour is the point — a gradient here\n  // would break the illustrated register immediately.\n  const fills = [t.accent, t.accentAlt, t.surface, t.ink];\n\n  const titleIn = interpolate(frame, [0, 18], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const total = rows.reduce((n, r) => n + r, 0);\n  const stackH = rows.length * (cardH + gap * 0.55);\n  const baseY = height * 0.8;\n\n  const push = interpolate(stage.progress, [0, 1], [1.03, 1]);\n\n  let index = 0;\n  const cards: React.ReactNode[] = [];\n\n  rows.forEach((count, rowIdx) => {\n    const rowW = count * cardW + (count - 1) * gap;\n    const rowX = (width - rowW) / 2;\n    const y = baseY - rowIdx * (cardH + gap * 0.55);\n\n    for (let c = 0; c < count; c += 1) {\n      const i = index;\n      index += 1;\n      const x = rowX + c * (cardW + gap);\n      const seed = `${SEED}-${i}`;\n\n      /**\n       * Bottom row first, left to right — the stack is built the way a person\n       * would build it. These are light objects being placed by hand, so unlike\n       * the device components they DO get an overshoot: `pop` is the one spring\n       * in the grammar with a deliberate bounce, and an illustrated card is\n       * exactly what it is for.\n       */\n      const delay = 8 + i * 3.2;\n      const e = spring({\n        frame: frame - delay,\n        fps,\n        config: SPRINGS.pop,\n        durationInFrames: 26,\n      });\n\n      const drop = interpolate(e, [0, 1], [height * 0.16, 0]);\n      const rot = interpolate(e, [0, 1], [(random(`${seed}-r`) - 0.5) * 16, (random(`${seed}-r2`) - 0.5) * 4]);\n      const fill = fills[i % fills.length];\n      const onDark = fill === t.ink;\n      const inner = onDark ? alpha(\"#fff\", 0.22) : alpha(t.ink, 0.16);\n\n      cards.push(\n        <div\n          key={i}\n          style={{\n            position: \"absolute\",\n            left: x,\n            top: y - cardH,\n            width: cardW,\n            height: cardH,\n            opacity: Math.min(1, e * 3),\n            transform: `translateY(${drop}px) rotate(${rot}deg)`,\n          }}\n        >\n          <svg width={cardW} height={cardH} style={{ position: \"absolute\", inset: 0, overflow: \"visible\" }} aria-hidden>\n            <path\n              d={wobblyRect(cardW, cardH, cardW * 0.11, seed, amp)}\n              fill={fill}\n              stroke={t.ink}\n              strokeWidth={stroke}\n              strokeLinejoin=\"round\"\n              strokeLinecap=\"round\"\n            />\n          </svg>\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              padding: cardW * 0.11,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: cardW * 0.075,\n            }}\n          >\n            {i % 3 === 0 ? (\n              <div\n                style={{\n                  width: cardW * 0.17,\n                  height: cardW * 0.17,\n                  borderRadius: \"50%\",\n                  backgroundColor: inner,\n                  flexShrink: 0,\n                }}\n              />\n            ) : null}\n            <div style={{ flex: 1, display: \"flex\", flexDirection: \"column\", gap: cardH * 0.1 }}>\n              <div style={{ height: cardH * 0.12, width: \"82%\", borderRadius: 999, backgroundColor: inner }} />\n              <div style={{ height: cardH * 0.12, width: \"56%\", borderRadius: 999, backgroundColor: inner }} />\n            </div>\n          </div>\n        </div>,\n      );\n    }\n  });\n\n  // The star lands after the last card, and only just after — a beat later and\n  // it reads as a separate event rather than as the stack's punctuation.\n  const crownAt = 8 + total * 3.2 + 12;\n  const crownE = spring({ frame: frame - crownAt, fps, config: SPRINGS.pop, durationInFrames: 22 });\n  const starR = cardW * 0.3;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(90% 60% at 50% 12%, ${alpha(\"#fff\", 0.85)} 0%, ${alpha(\n            \"#fff\",\n            0.3,\n          )} 40%, ${alpha(t.ink, 0.03)} 74%, ${alpha(t.ink, 0.09)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: `50% ${((baseY - stackH * 0.5) / height) * 100}%`,\n          opacity: stage.presence,\n        }}\n      >\n        {title ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: width * 0.08,\n              right: width * 0.08,\n              top: height * 0.11,\n              textAlign: \"center\",\n              fontFamily: t.fontDisplay,\n              fontSize: width * 0.085,\n              fontWeight: 700,\n              lineHeight: 1.04,\n              letterSpacing: width * -0.003,\n              color: t.ink,\n              opacity: titleIn,\n              transform: `translateY(${interpolate(titleIn, [0, 1], [width * 0.03, 0])}px) rotate(-1deg)`,\n            }}\n          >\n            {title}\n          </div>\n        ) : null}\n\n        {cards}\n\n        {crown ? (\n          <svg\n            width={starR * 2}\n            height={starR * 2}\n            style={{\n              position: \"absolute\",\n              left: width / 2 - starR,\n              top: baseY - rows.length * (cardH + gap * 0.55) - starR * 1.5,\n              opacity: Math.min(1, crownE * 2),\n              transform: `scale(${crownE}) rotate(${interpolate(crownE, [0, 1], [-40, 0])}deg)`,\n              overflow: \"visible\",\n            }}\n            aria-hidden\n          >\n            <path\n              d={(() => {\n                let d = \"\";\n                for (let i = 0; i < 10; i += 1) {\n                  const a = (i / 10) * Math.PI * 2 - Math.PI / 2;\n                  const rr = i % 2 === 0 ? starR * 0.92 : starR * 0.4;\n                  const jitter = (random(`${SEED}-star-${i}`) - 0.5) * amp * 1.4;\n                  d += `${i === 0 ? \"M\" : \"L\"}${(starR + Math.cos(a) * (rr + jitter)).toFixed(1)} ${(\n                    starR + Math.sin(a) * (rr + jitter)\n                  ).toFixed(1)}`;\n                }\n                return `${d}Z`;\n              })()}\n              fill={t.accentAlt}\n              stroke={t.ink}\n              strokeWidth={stroke}\n              strokeLinejoin=\"round\"\n            />\n          </svg>\n        ) : null}\n      </div>\n\n      <Grain id=\"doodle-card-grain\" frame={frame} opacity={0.12} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"doodle-card-stack\",\n  name: \"Doodle Card Stack\",\n  category: \"diagram\",\n  summary:\n    \"Hand-drawn marker cards drop into a stack bottom row first, each outline wobbled off true by its own seed\",\n  tags: [\"illustrated\", \"hand drawn\", \"light\", \"vertical\", \"stack\", \"playful\", \"marker\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: doodleCardStackSchema,\n  defaults: {\n    title: \"Everything, stacked\",\n    rows: [4, 3, 2, 1],\n    wobble: 0.55,\n    crown: true,\n    brand: \"paper\",\n  },\n  component: DoodleCardStack,\n  file: \"src/registry/items/doodle-card-stack.tsx\",\n  origin: {\n    prompt:\n      \"A vertical frame where flat illustrated cards drop in and build up into a pyramid, bottom row first. The outlines must look hand-drawn with a marker — wobbling off true, thicker at the corners — not like rounded rectangles. Flat colour fills only, no gradients. Let them overshoot slightly as they land, since they are light illustrated objects, and pop a hand-drawn star above the top card once the stack completes.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical frame where flat illustrated cards drop in and build up into a pyramid, bottom row first. The outlines must look hand-drawn with a marker — wobbling off true, thicker at the corners — not like rounded rectangles. Flat colour fills only, no gradients. Let them overshoot slightly as they land, since they are light illustrated objects, and pop a hand-drawn star above the top card once the stack completes.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.7961,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Hand-lettered line above the stack. Omit to let the stack carry the frame alone.",
          "required": false,
          "defaultValue": "Everything, stacked",
          "options": null
        },
        {
          "name": "rows",
          "type": "array",
          "description": "Cards per row, bottom row FIRST. [4,3,2,1] builds a pyramid; [3,3,3] builds a wall. Keep rows under five or the cards get too small to read.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "wobble",
          "type": "number",
          "description": "How hand-drawn the outlines are. 0 is a clean vector rectangle; 1 is a loose marker sketch.",
          "required": false,
          "defaultValue": "0.55",
          "options": null
        },
        {
          "name": "crown",
          "type": "boolean",
          "description": "Pop a marker star above the top card once the stack completes.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. Written for the light brands.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "dot-grid-parallax",
      "name": "Dot Grid Parallax",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "Three dot fields drift at integer multiples of one grid cell, so the loop returns exactly home, with density falling off at the frame edges",
      "tags": [
        "background",
        "parallax",
        "grid",
        "loop",
        "atmosphere"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/dot-grid-parallax.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const dotGridParallaxSchema = z.object({\n  density: z\n    .number()\n    .min(10)\n    .max(40)\n    .default(22)\n    .describe(\"Dots across the frame on the base layer. Higher is busier and slower to render.\"),\n  intensity: z\n    .number()\n    .min(0.2)\n    .max(1)\n    .default(0.6)\n    .describe(\"Overall opacity of the field. Keep it low if type will sit on top.\"),\n  gridLines: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw faint hairlines behind the dots.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof dotGridParallaxSchema>;\n\n/**\n * Three depth layers drifting at integer multiples of one grid cell, so the last\n * frame is pixel-identical to the first and the background can loop under a held\n * shot indefinitely. Loop-safety is a property of the arithmetic here, not of a\n * tolerance — `progress * spacing * k` with integer k returns exactly home.\n */\nconst LAYERS = [\n  { scale: 1, rate: 1, alpha: 0.9, sizeMul: 1 },\n  { scale: 1.6, rate: 2, alpha: 0.55, sizeMul: 0.72 },\n  { scale: 2.5, rate: 3, alpha: 0.3, sizeMul: 0.5 },\n];\n\nconst DotGridParallax: React.FC<Props> = ({ density, intensity, gridLines, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {gridLines ? <GridLines width={width} height={height} density={density} tokens={t} /> : null}\n\n      {LAYERS.map((layer, i) => (\n        <DotLayer\n          key={i}\n          layer={layer}\n          index={i}\n          width={width}\n          height={height}\n          density={density}\n          intensity={intensity}\n          tokens={t}\n        />\n      ))}\n\n      {/* Density falls off toward the edges so the field never fights the frame. */}\n      <AbsoluteFill\n        style={{\n          backgroundImage: `radial-gradient(ellipse at 50% 50%, transparent 30%, ${t.bg} 82%)`,\n          opacity: 0.95,\n        }}\n      />\n      <AbsoluteFill style={{ opacity: 1 - stage.presence, backgroundColor: t.bg }} />\n    </AbsoluteFill>\n  );\n};\n\nconst GridLines: React.FC<{\n  width: number;\n  height: number;\n  density: number;\n  tokens: typeof DEFAULT_BRAND;\n}> = ({ width, height, density, tokens }) => {\n  const spacing = width / density;\n  const cols = Math.ceil(width / spacing) + 1;\n  const rows = Math.ceil(height / spacing) + 1;\n  return (\n    <svg width={width} height={height} style={{ position: \"absolute\", inset: 0, opacity: 0.5 }}>\n      {Array.from({ length: cols }, (_, i) => (\n        <line key={`c${i}`} x1={i * spacing} y1={0} x2={i * spacing} y2={height} stroke={tokens.rule} strokeWidth={1} />\n      ))}\n      {Array.from({ length: rows }, (_, i) => (\n        <line key={`r${i}`} x1={0} y1={i * spacing} x2={width} y2={i * spacing} stroke={tokens.rule} strokeWidth={1} />\n      ))}\n    </svg>\n  );\n};\n\nconst DotLayer: React.FC<{\n  layer: (typeof LAYERS)[number];\n  index: number;\n  width: number;\n  height: number;\n  density: number;\n  intensity: number;\n  tokens: typeof DEFAULT_BRAND;\n}> = ({ layer, index, width, height, density, intensity, tokens }) => {\n  const stage = useStage(BUDGETS.feature);\n  const spacing = (width / density) * layer.scale;\n  const dot = Math.max(1.5, spacing * 0.075 * layer.sizeMul);\n\n  // One extra row and column, drawn off-frame, so the wrap is never visible.\n  const cols = Math.ceil(width / spacing) + 2;\n  const rows = Math.ceil(height / spacing) + 2;\n\n  const shift = stage.progress * spacing * layer.rate;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        transform: `translate(${-spacing + (shift % spacing)}px, ${-spacing + (shift % spacing) * 0.4}px)`,\n        opacity: layer.alpha * intensity,\n      }}\n    >\n      <svg width={width + spacing * 2} height={height + spacing * 2}>\n        {Array.from({ length: rows }, (_, r) =>\n          Array.from({ length: cols }, (_, c) => {\n            const seed = `dot-${index}-${r}-${c}`;\n            const accent = random(seed) > 0.955;\n            return (\n              <circle\n                key={`${r}-${c}`}\n                cx={c * spacing + spacing}\n                cy={r * spacing + spacing}\n                r={dot * (accent ? 1.5 : 1)}\n                fill={accent ? tokens.accent : tokens.inkMuted}\n                opacity={accent ? 0.9 : 0.5}\n              />\n            );\n          }),\n        )}\n      </svg>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"dot-grid-parallax\",\n  name: \"Dot Grid Parallax\",\n  category: \"background\",\n  summary:\n    \"Three dot fields drift at integer multiples of one grid cell, so the loop returns exactly home, with density falling off at the frame edges\",\n  tags: [\"background\", \"parallax\", \"grid\", \"loop\", \"atmosphere\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: dotGridParallaxSchema,\n  defaults: { density: 22, intensity: 0.6, gridLines: true, brand: \"slate\" },\n  component: DotGridParallax,\n  file: \"src/registry/items/dot-grid-parallax.tsx\",\n  origin: {\n    prompt:\n      \"A seamlessly looping background of dots on a faint grid, with three depth layers drifting at different rates and density falling off toward the frame edges so a headline stays readable on top.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A seamlessly looping background of dots on a faint grid, with three depth layers drifting at different rates and density falling off toward the frame edges so a headline stays readable on top.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.1697,
      "props": [
        {
          "name": "density",
          "type": "number",
          "description": "Dots across the frame on the base layer. Higher is busier and slower to render.",
          "required": false,
          "defaultValue": "22",
          "options": null
        },
        {
          "name": "intensity",
          "type": "number",
          "description": "Overall opacity of the field. Keep it low if type will sit on top.",
          "required": false,
          "defaultValue": "0.6",
          "options": null
        },
        {
          "name": "gridLines",
          "type": "boolean",
          "description": "Draw faint hairlines behind the dots.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "end-card-cta",
      "name": "End Card CTA",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "A closing frame: drawn geometric monogram, tagline, URL pill and a QR-shaped block, composed as a page",
      "tags": [
        "end card",
        "cta",
        "outro",
        "logo lockup",
        "qr",
        "closing frame"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/end-card-cta.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const endCardCtaSchema = z.object({\n  wordmark: z\n    .string()\n    .describe(\"The name in the lockup, set in the brand display face beside the drawn mark. One or two words; the type steps down as the name gets longer, so up to about 22 characters still fits on one line.\"),\n  tagline: z\n    .string()\n    .optional()\n    .describe(\"One sentence under the lockup. Say what happens next, not what just happened. Two lines maximum — it wraps at a measure of roughly 34 characters.\"),\n  url: z\n    .string()\n    .describe(\"The destination, shown in the pill. Write it as you want it read — no protocol, lowercase, no trailing slash ('motion.registry/gallery').\"),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\"Small mono label in the top-left corner, e.g. 'Thanks for watching' or 'The end'. Omit to leave the top of the frame empty.\"),\n  qrCaption: z\n    .string()\n    .optional()\n    .describe(\"Mono caption under the QR block. Tell people what scanning gets them. Ignored when `qr` is false.\"),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Bottom-left credit line above the closing rule — licence, studio, series name.\"),\n  handle: z\n    .string()\n    .optional()\n    .describe(\"Bottom-right social handle. Include the '@' yourself; it is rendered verbatim.\"),\n  qr: z\n    .boolean()\n    .default(true)\n    .describe(\"Show the QR-shaped block. It is decorative geometry, not a scannable code — set false if the frame needs to carry a real one composited over it.\"),\n  seed: z\n    .string()\n    .default(\"end-card\")\n    .describe(\"Seed for the QR block's module pattern. Change it to get a different-looking code; the same seed always renders the same pattern.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof endCardCtaSchema>;\n\n/**\n * End Card CTA — the closing frame, built to be held and screenshotted.\n *\n * The composition is two columns hung off one hairline: a lockup, tagline and\n * URL pill on the left, a QR-shaped block on the right, with a mono kicker in\n * the top corner and a credit rule at the foot. Nothing is centred; the frame\n * is read as a page.\n *\n * The mark is drawn, not fetched: four quarter-discs of one circle that arrive\n * along their own diagonals and close onto a fixed gap. Two are solid ink, one\n * is the accent, one is left as a quarter-ring outline — a constructed monogram\n * rather than a glyph, so it retargets with the tokens like everything else.\n *\n * Everything settles. Nothing overshoots, because a card that gets held for\n * five seconds has no use for a bounce. The single loop in the frame is the\n * arrow in the pill, which nudges once every 1.8s: enough to say \"go here\",\n * quiet enough to ignore.\n */\nconst EndCardCta: React.FC<Props> = ({\n  wordmark,\n  tagline,\n  url,\n  kicker,\n  qrCaption,\n  footnote,\n  handle,\n  qr,\n  seed,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  // One type unit for the whole card. Taking the larger of the width and a\n  // 16:9-equivalent of the height means a 9:16 or 4:5 cut scales the type up\n  // rather than rendering this composition at half size.\n  const unit = Math.max(width, height * 0.72);\n  const hair = Math.max(1, unit * 0.0009);\n\n  // Below ~5:4 the two columns stop fitting side by side, so the QR drops\n  // under the pill and the divider turns into a horizontal rule.\n  const stacked = width / height < 1.2;\n\n  const padX = width * 0.075;\n  const padY = height * 0.095;\n\n  const markSize = unit * 0.072;\n  // The wordmark steps down continuously with its length rather than at one\n  // breakpoint, so a long name stays inside the column instead of shouldering\n  // the divider and the QR block off the right margin.\n  const wordSize =\n    unit *\n    interpolate(wordmark.length, [8, 22], [0.05, 0.032], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n    });\n  const taglineSize = unit * 0.0195;\n  const metaSize = unit * 0.0115;\n  const plate = unit * (stacked ? 0.135 : 0.155);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      {/* A single soft lift behind the lockup, so a flat card has some depth\n          when it is on screen for five seconds. Static — nothing to watch. */}\n      <AbsoluteFill\n        style={{\n          backgroundImage: `radial-gradient(120% 95% at 20% 26%, ${t.surface} 0%, ${t.bg} 62%)`,\n          opacity: 0.9,\n        }}\n      />\n\n      <AbsoluteFill\n        style={{\n          padding: `${padY}px ${padX}px`,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"space-between\",\n          opacity: stage.presence,\n        }}\n      >\n        {kicker ? (\n          <Kicker label={kicker} size={metaSize} color={t.inkMuted} accent={t.accent} font={t.fontMono} />\n        ) : (\n          <div />\n        )}\n\n        <div\n          style={{\n            display: \"flex\",\n            flexDirection: stacked ? \"column\" : \"row\",\n            alignItems: stacked ? \"flex-start\" : \"center\",\n            gap: unit * (stacked ? 0.032 : 0.05),\n          }}\n        >\n          <div style={{ flex: \"1 1 auto\", minWidth: 0 }}>\n            <Lockup\n              wordmark={wordmark}\n              markSize={markSize}\n              wordSize={wordSize}\n              ink={t.ink}\n              accent={t.accent}\n              font={t.fontDisplay}\n            />\n\n            {tagline ? (\n              <Tagline\n                text={tagline}\n                size={taglineSize}\n                color={t.inkMuted}\n                font={t.fontBody}\n                top={unit * 0.03}\n                measure={unit * 0.33}\n              />\n            ) : null}\n\n            <UrlPill\n              url={url}\n              height={unit * 0.046}\n              top={unit * 0.034}\n              surface={t.surface}\n              rule={t.rule}\n              ink={t.ink}\n              accent={t.accent}\n              accentInk={t.accentInk}\n              font={t.fontMono}\n            />\n          </div>\n\n          {qr ? (\n            <>\n              <Divider stacked={stacked} hair={hair} length={unit * 0.17} color={t.rule} />\n              <QrBlock\n                seed={seed}\n                caption={qrCaption}\n                plate={plate}\n                lift={unit * 0.018}\n                captionSize={metaSize}\n                surface={t.surface}\n                rule={t.rule}\n                ink={t.ink}\n                accent={t.accent}\n                muted={t.inkMuted}\n                font={t.fontMono}\n                radius={t.radius}\n              />\n            </>\n          ) : null}\n        </div>\n\n        {footnote || handle ? (\n          <Footer\n            footnote={footnote}\n            handle={handle}\n            size={metaSize}\n            hair={hair}\n            color={t.inkMuted}\n            rule={t.rule}\n            font={t.fontMono}\n          />\n        ) : (\n          <div />\n        )}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/* --------------------------------------------------------------- choreography */\n\n/**\n * Entrance order, in stagger slots of 2 frames. The whole card is in place by\n * frame ~32 of a 150-frame budget, which leaves the hold genuinely still.\n */\nconst SLOT = {\n  kicker: 0,\n  mark: 0, // quadrants take slots 0–3\n  wordmark: 3,\n  qr: 3,\n  divider: 4,\n  tagline: 5,\n  pill: 6,\n  footer: 7,\n} as const;\n\nconst BEAT = 2;\n\nconst useSlotStage = (index: number) => useStaggeredStage(BUDGETS.scene, index, BEAT);\n\n/* ---------------------------------------------------------------------- mark */\n\n/**\n * The four quarters of one circle, each with its outward diagonal. The arc\n * sweeps are written in a 100-unit box so the whole mark scales with the frame\n * and the hairline scales with it.\n */\nconst MARK_QUADRANTS = [\n  { d: \"M 2 50 A 48 48 0 0 1 50 2 L 50 50 Z\", dx: -1, dy: -1, paint: \"ink\" },\n  { d: \"M 50 2 A 48 48 0 0 1 98 50 L 50 50 Z\", dx: 1, dy: -1, paint: \"accent\" },\n  { d: \"M 98 50 A 48 48 0 0 1 50 98 L 50 50 Z\", dx: 1, dy: 1, paint: \"ink\" },\n  { d: \"M 50 98 A 48 48 0 0 1 2 50 L 50 50 Z\", dx: -1, dy: 1, paint: \"outline\" },\n] as const;\n\n/** Resting gap between the quarters, and how far out each one starts. */\nconst MARK_GAP = 1.8;\nconst MARK_TRAVEL = 20;\n\nconst GeometricMark: React.FC<{ size: number; ink: string; accent: string }> = ({ size, ink, accent }) => (\n  <svg width={size} height={size} viewBox=\"0 0 100 100\" style={{ display: \"block\", overflow: \"visible\" }}>\n    {MARK_QUADRANTS.map((q, i) => (\n      <MarkQuadrant key={i} index={i} quadrant={q} ink={ink} accent={accent} />\n    ))}\n  </svg>\n);\n\nconst MarkQuadrant: React.FC<{\n  index: number;\n  quadrant: (typeof MARK_QUADRANTS)[number];\n  ink: string;\n  accent: string;\n}> = ({ index, quadrant, ink, accent }) => {\n  const stage = useSlotStage(SLOT.mark + index);\n  // Each quarter comes in from the direction it will end up pointing, so the\n  // mark assembles rather than appearing.\n  const out = MARK_GAP + (1 - stage.enter) * MARK_TRAVEL;\n  const outline = quadrant.paint === \"outline\";\n\n  return (\n    <g\n      transform={`translate(${quadrant.dx * out} ${quadrant.dy * out})`}\n      opacity={stage.enter}\n    >\n      <path\n        d={quadrant.d}\n        fill={outline ? \"none\" : quadrant.paint === \"accent\" ? accent : ink}\n        stroke={outline ? ink : \"none\"}\n        strokeWidth={outline ? 2.6 : 0}\n        strokeOpacity={outline ? 0.5 : 1}\n      />\n    </g>\n  );\n};\n\n/* -------------------------------------------------------------------- lockup */\n\n/**\n * Mark plus wordmark. The type rises through a hard mask while its tracking\n * closes from open to tight — a wordmark setting itself, which lands with far\n * more weight than a fade and never leaves a soft glyph edge behind.\n */\nconst Lockup: React.FC<{\n  wordmark: string;\n  markSize: number;\n  wordSize: number;\n  ink: string;\n  accent: string;\n  font: string;\n}> = ({ wordmark, markSize, wordSize, ink, accent, font }) => {\n  const stage = useSlotStage(SLOT.wordmark);\n  const y = interpolate(stage.enter, [0, 1], [104, 0]);\n  const tracking = interpolate(stage.enter, [0, 1], [wordSize * 0.13, wordSize * -0.03]);\n\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: markSize * 0.34 }}>\n      <GeometricMark size={markSize} ink={ink} accent={accent} />\n      <span style={{ display: \"block\", overflow: \"hidden\", paddingBottom: wordSize * 0.08 }}>\n        <span\n          style={{\n            display: \"block\",\n            fontFamily: font,\n            fontSize: wordSize,\n            fontWeight: 700,\n            lineHeight: 1.04,\n            letterSpacing: tracking,\n            color: ink,\n            whiteSpace: \"nowrap\",\n            transform: `translateY(${y}%)`,\n            opacity: stage.enter,\n          }}\n        >\n          {wordmark}\n        </span>\n      </span>\n    </div>\n  );\n};\n\n/* ------------------------------------------------------------------- tagline */\n\nconst Tagline: React.FC<{\n  text: string;\n  size: number;\n  color: string;\n  font: string;\n  top: number;\n  measure: number;\n}> = ({ text, size, color, font, top, measure }) => {\n  const stage = useSlotStage(SLOT.tagline);\n  const y = interpolate(stage.enter, [0, 1], [size * 0.55, 0]);\n  return (\n    <div\n      style={{\n        marginTop: top,\n        maxWidth: measure,\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 400,\n        lineHeight: 1.45,\n        letterSpacing: size * -0.004,\n        color,\n        transform: `translateY(${y}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      {text}\n    </div>\n  );\n};\n\n/* ------------------------------------------------------------------ url pill */\n\n/** One nudge every 1.8s: out on a travel curve, back, then a long rest. */\nconst NUDGE_PERIOD = 54;\n\nconst UrlPill: React.FC<{\n  url: string;\n  height: number;\n  top: number;\n  surface: string;\n  rule: string;\n  ink: string;\n  accent: string;\n  accentInk: string;\n  font: string;\n}> = ({ url, height, top, surface, rule, ink, accent, accentInk, font }) => {\n  const stage = useSlotStage(SLOT.pill);\n  const y = interpolate(stage.enter, [0, 1], [height * 0.3, 0]);\n\n  // The loop only starts once the card has settled, so it never competes with\n  // the entrance. Before that `loopFrame` is 0 and the arrow sits still.\n  const loopFrame = Math.max(0, stage.frame - BUDGETS.scene.enter);\n  const cycle = (loopFrame % NUDGE_PERIOD) / NUDGE_PERIOD;\n  const nudge = interpolate(cycle, [0, 0.16, 0.34, 1], [0, 1, 0, 0], {\n    easing: EASING.travel,\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const dot = height * 0.7;\n\n  return (\n    <div\n      style={{\n        marginTop: top,\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: height * 0.42,\n        height,\n        paddingLeft: height * 0.6,\n        paddingRight: height * 0.15,\n        borderRadius: height / 2,\n        backgroundColor: surface,\n        border: `${Math.max(1, height * 0.014)}px solid ${rule}`,\n        transform: `translateY(${y}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      <span\n        style={{\n          fontFamily: font,\n          fontSize: height * 0.3,\n          fontWeight: 500,\n          letterSpacing: height * 0.012,\n          color: ink,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        {url}\n      </span>\n      <span\n        style={{\n          width: dot,\n          height: dot,\n          borderRadius: dot / 2,\n          backgroundColor: accent,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          flex: \"0 0 auto\",\n        }}\n      >\n        <svg\n          width={dot * 0.5}\n          height={dot * 0.5}\n          viewBox=\"0 0 24 24\"\n          style={{ display: \"block\", transform: `translateX(${nudge * dot * 0.16}px)` }}\n        >\n          <path\n            d=\"M3 12 H19 M12.5 5.5 L19 12 L12.5 18.5\"\n            fill=\"none\"\n            stroke={accentInk}\n            strokeWidth={2.4}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </span>\n    </div>\n  );\n};\n\n/* ------------------------------------------------------------------- divider */\n\nconst Divider: React.FC<{ stacked: boolean; hair: number; length: number; color: string }> = ({\n  stacked,\n  hair,\n  length,\n  color,\n}) => {\n  const stage = useSlotStage(SLOT.divider);\n  return (\n    <div\n      style={{\n        width: stacked ? \"100%\" : hair,\n        height: stacked ? hair : length,\n        flex: \"0 0 auto\",\n        backgroundColor: color,\n        transform: stacked ? `scaleX(${stage.enter})` : `scaleY(${stage.enter})`,\n        transformOrigin: stacked ? \"left center\" : \"center top\",\n      }}\n    />\n  );\n};\n\n/* ------------------------------------------------------------------------ qr */\n\nconst QR_N = 25; // the module count of a real version-2 code\n\nconst isFinder = (lr: number, lc: number) =>\n  lr === 0 || lr === 6 || lc === 0 || lc === 6 || (lr >= 2 && lr <= 4 && lc >= 2 && lc <= 4);\n\nconst isFinderCore = (lr: number, lc: number) => lr >= 2 && lr <= 4 && lc >= 2 && lc <= 4;\n\ntype QrCell = { on: boolean; accent: boolean };\n\n/**\n * A QR-shaped mask, not a QR code.\n *\n * The furniture that makes a code legible as a code is placed by hand — three\n * finder squares with their separators, the two timing tracks, one alignment\n * square — and only the payload area is filled from `random(seed)`, so the\n * block reads instantly as a code without ever claiming to be one. Same seed,\n * same pattern, every render.\n */\nconst qrCell = (seed: string, n: number, r: number, c: number): QrCell => {\n  const far = n - 7;\n\n  if (r < 7 && c < 7) return { on: isFinder(r, c), accent: isFinderCore(r, c) };\n  if (r < 7 && c >= far) return { on: isFinder(r, c - far), accent: false };\n  if (r >= far && c < 7) return { on: isFinder(r - far, c), accent: false };\n\n  // The one-module quiet ring each finder sits inside.\n  if ((r < 8 && c < 8) || (r < 8 && c >= far - 1) || (r >= far - 1 && c < 8)) {\n    return { on: false, accent: false };\n  }\n\n  // Timing tracks.\n  if (r === 6) return { on: c % 2 === 0, accent: false };\n  if (c === 6) return { on: r % 2 === 0, accent: false };\n\n  // Alignment square, bottom right.\n  const a = n - 9;\n  if (r >= a && r <= a + 4 && c >= a && c <= a + 4) {\n    const lr = r - a;\n    const lc = c - a;\n    return { on: lr === 0 || lr === 4 || lc === 0 || lc === 4 || (lr === 2 && lc === 2), accent: false };\n  }\n\n  return { on: random(`${seed}:${r}:${c}`) > 0.5, accent: false };\n};\n\nconst QrBlock: React.FC<{\n  seed: string;\n  caption?: string;\n  plate: number;\n  lift: number;\n  captionSize: number;\n  surface: string;\n  rule: string;\n  ink: string;\n  accent: string;\n  muted: string;\n  font: string;\n  radius: number;\n}> = ({ seed, caption, plate, lift, captionSize, surface, rule, ink, accent, muted, font, radius }) => {\n  const stage = useSlotStage(SLOT.qr);\n  const y = interpolate(stage.enter, [0, 1], [lift, 0]);\n\n  const edge = Math.max(1, plate * 0.004);\n  const quiet = plate * 0.055;\n  // Border-box: the frame and the quiet zone both come out of the plate, so\n  // the module grid gets what is left rather than overflowing it.\n  const inner = plate - (quiet + edge) * 2;\n\n  const cells: React.ReactNode[] = [];\n  for (let r = 0; r < QR_N; r++) {\n    for (let c = 0; c < QR_N; c++) {\n      const cell = qrCell(seed, QR_N, r, c);\n      if (!cell.on) continue;\n\n      // Modules resolve on a diagonal sweep, each growing from its own centre.\n      // Internal motion, so it is free — but it is timed to finish inside the\n      // enter window regardless, because the hold has to be a still.\n      const band = (r + c) / (2 * (QR_N - 1));\n      const start = band * 8;\n      const p = interpolate(stage.frame, [start, start + 5], [0, 1], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.enter,\n      });\n      // 1.02 rather than 1 at rest: a hair of overlap so adjacent modules read\n      // as one block instead of a grid with seams.\n      const s = interpolate(p, [0, 1], [0.42, 1.02]);\n\n      cells.push(\n        <rect\n          key={`${r}-${c}`}\n          x={c + (1 - s) / 2}\n          y={r + (1 - s) / 2}\n          width={s}\n          height={s}\n          fill={cell.accent ? accent : ink}\n          opacity={p}\n        />,\n      );\n    }\n  }\n\n  return (\n    <div style={{ flex: \"0 0 auto\", transform: `translateY(${y}px)`, opacity: stage.enter }}>\n      <div\n        style={{\n          width: plate,\n          height: plate,\n          padding: quiet,\n          boxSizing: \"border-box\",\n          borderRadius: radius,\n          backgroundColor: surface,\n          border: `${edge}px solid ${rule}`,\n        }}\n      >\n        <svg width={inner} height={inner} viewBox={`0 0 ${QR_N} ${QR_N}`} style={{ display: \"block\" }}>\n          {cells}\n        </svg>\n      </div>\n\n      {caption ? (\n        <div\n          style={{\n            marginTop: plate * 0.075,\n            maxWidth: plate,\n            fontFamily: font,\n            fontSize: captionSize,\n            fontWeight: 500,\n            lineHeight: 1.4,\n            letterSpacing: captionSize * 0.16,\n            textTransform: \"uppercase\",\n            color: muted,\n          }}\n        >\n          {caption}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/* -------------------------------------------------------------- small labels */\n\nconst Kicker: React.FC<{\n  label: string;\n  size: number;\n  color: string;\n  accent: string;\n  font: string;\n}> = ({ label, size, color, accent, font }) => {\n  const stage = useSlotStage(SLOT.kicker);\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.9,\n        opacity: stage.enter,\n        transform: `translateY(${interpolate(stage.enter, [0, 1], [-size * 0.6, 0])}px)`,\n      }}\n    >\n      <span style={{ width: size * 0.62, height: size * 0.62, backgroundColor: accent, flex: \"0 0 auto\" }} />\n      <span\n        style={{\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 500,\n          letterSpacing: size * 0.2,\n          textTransform: \"uppercase\",\n          color,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nconst Footer: React.FC<{\n  footnote?: string;\n  handle?: string;\n  size: number;\n  hair: number;\n  color: string;\n  rule: string;\n  font: string;\n}> = ({ footnote, handle, size, hair, color, rule, font }) => {\n  const stage = useSlotStage(SLOT.footer);\n  const label: React.CSSProperties = {\n    fontFamily: font,\n    fontSize: size,\n    fontWeight: 500,\n    letterSpacing: size * 0.16,\n    textTransform: \"uppercase\",\n    color,\n  };\n  return (\n    <div>\n      <div\n        style={{\n          height: hair,\n          backgroundColor: rule,\n          transform: `scaleX(${stage.enter})`,\n          transformOrigin: \"left center\",\n        }}\n      />\n      <div\n        style={{\n          marginTop: size * 1.3,\n          display: \"flex\",\n          justifyContent: \"space-between\",\n          alignItems: \"baseline\",\n          gap: size * 2,\n          opacity: stage.enter,\n        }}\n      >\n        <span style={label}>{footnote ?? \"\"}</span>\n        <span style={label}>{handle ?? \"\"}</span>\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"end-card-cta\",\n  name: \"End Card CTA\",\n  category: \"titles\",\n  summary: \"A closing frame: drawn geometric monogram, tagline, URL pill and a QR-shaped block, composed as a page\",\n  tags: [\"end card\", \"cta\", \"outro\", \"logo lockup\", \"qr\", \"closing frame\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: endCardCtaSchema,\n  defaults: {\n    wordmark: \"Motionstash\",\n    tagline: \"Every component in this reel is one file you can copy, retarget and render.\",\n    url: \"motion.registry/gallery\",\n    kicker: \"Thanks for watching\",\n    qrCaption: \"Scan to explore\",\n    footnote: \"Open source · MIT\",\n    handle: \"@motionregistry\",\n    qr: true,\n    seed: \"end-card\",\n    brand: \"slate\",\n  },\n  component: EndCardCta,\n  file: \"src/registry/items/end-card-cta.tsx\",\n  origin: {\n    prompt:\n      \"An end card for the last five seconds of a video: a geometric monogram drawn from shapes with the name beside it, a one-line tagline, a URL in a pill whose arrow nudges on a slow loop, and a QR-looking block of squares off to the right. Everything should settle into place rather than bounce, and the held frame has to look like a finished poster because it is the frame people screenshot.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An end card for the last five seconds of a video: a geometric monogram drawn from shapes with the name beside it, a one-line tagline, a URL in a pill whose arrow nudges on a slow loop, and a QR-looking block of squares off to the right. Everything should settle into place rather than bounce, and the held frame has to look like a finished poster because it is the frame people screenshot.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.356,
      "props": [
        {
          "name": "wordmark",
          "type": "string",
          "description": "The name in the lockup, set in the brand display face beside the drawn mark. One or two words; the type steps down as the name gets longer, so up to about 22 characters still fits on one line.",
          "required": true,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "tagline",
          "type": "string",
          "description": "One sentence under the lockup. Say what happens next, not what just happened. Two lines maximum — it wraps at a measure of roughly 34 characters.",
          "required": false,
          "defaultValue": "Every component in this reel is one file you can copy, re…",
          "options": null
        },
        {
          "name": "url",
          "type": "string",
          "description": "The destination, shown in the pill. Write it as you want it read — no protocol, lowercase, no trailing slash ('motion.registry/gallery').",
          "required": true,
          "defaultValue": "motion.registry/gallery",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "Small mono label in the top-left corner, e.g. 'Thanks for watching' or 'The end'. Omit to leave the top of the frame empty.",
          "required": false,
          "defaultValue": "Thanks for watching",
          "options": null
        },
        {
          "name": "qrCaption",
          "type": "string",
          "description": "Mono caption under the QR block. Tell people what scanning gets them. Ignored when `qr` is false.",
          "required": false,
          "defaultValue": "Scan to explore",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Bottom-left credit line above the closing rule — licence, studio, series name.",
          "required": false,
          "defaultValue": "Open source · MIT",
          "options": null
        },
        {
          "name": "handle",
          "type": "string",
          "description": "Bottom-right social handle. Include the '@' yourself; it is rendered verbatim.",
          "required": false,
          "defaultValue": "@motionregistry",
          "options": null
        },
        {
          "name": "qr",
          "type": "boolean",
          "description": "Show the QR-shaped block. It is decorative geometry, not a scannable code — set false if the frame needs to carry a real one composited over it.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seed for the QR block's module pattern. Change it to get a different-looking code; the same seed always renders the same pattern.",
          "required": false,
          "defaultValue": "end-card",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "etched-light-reveal",
      "name": "Etched Light Reveal",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "A wordmark cut into a brushed plate, invisible until a raking light travels across and catches the walls of the groove",
      "tags": [
        "logo",
        "engraved",
        "etched",
        "brushed metal",
        "light reveal",
        "material",
        "premium"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/etched-light-reveal.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const etchedLightRevealSchema = z.object({\n  wordmark: z\n    .string()\n    .describe(\"The engraved mark. One word reads best — the reveal depends on strokes wide enough to hold a highlight and a shadow.\"),\n  tagline: z\n    .string()\n    .optional()\n    .describe(\"Line under the mark, printed on the plate rather than engraved into it.\"),\n  depth: z\n    .number()\n    .min(0.2)\n    .max(1)\n    .default(0.6)\n    .describe(\"How deep the engraving is cut. Higher offsets the highlight and shadow further, which reads as a deeper groove.\"),\n  brushed: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the plate as brushed metal. Turn off for a matte machined surface.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof etchedLightRevealSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.9\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst EtchedLightReveal: React.FC<Props> = ({ wordmark, tagline, depth, brushed, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const size = width * (wordmark.length > 8 ? 0.11 : 0.155);\n\n  /**\n   * The light source.\n   *\n   * This is the whole component: there is no fade, no wipe and no scale on the\n   * wordmark. The mark is cut into the plate from frame zero, and it becomes\n   * visible only because a light travels across the surface and starts catching\n   * the walls of the groove. It rakes in from off frame left and parks at a low\n   * angle, which is where engraving reads best.\n   */\n  // Rakes in from off frame left, then keeps breathing around its rest angle\n  // rather than parking dead still: a light that stops moving takes the\n  // engraving with it, because the engraving IS the light.\n  const lightX =\n    interpolate(frame, [0, 46], [-0.6, 0.3], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.travel,\n    }) + Math.sin(time * 0.5) * 0.055;\n  const lightY = -0.42 + Math.cos(time * 0.37) * 0.05;\n\n  const strength = interpolate(frame, [4, 42], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  /**\n   * Direction from the mark to the light, and the two offsets it implies.\n   *\n   * An engraved letter is lit the opposite way round from a raised one: the wall\n   * facing the light is the near wall of the groove and falls into shadow, while\n   * the far wall catches the light. So the shadow goes TOWARD the light and the\n   * highlight goes away from it. Getting this backwards is what makes most\n   * \"engraved text\" look embossed instead.\n   */\n  const dx = lightX - 0.5;\n  const dy = lightY - 0.5;\n  const len = Math.hypot(dx, dy) || 1;\n  const lx = dx / len;\n  const ly = dy / len;\n  const off = size * 0.024 * depth;\n  const soft = size * 0.012;\n\n  /** One broad specular band across the plate, after the mark has resolved. */\n  const bandT = interpolate(frame, [40, 78], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const bandLive = bandT > 0 && bandT < 1;\n  // The band deepens the cut as it passes — a real highlight raises local\n  // contrast on both walls at once.\n  const bandBoost = bandLive ? Math.max(0, 1 - Math.abs(bandT - 0.5) * 2.6) : 0;\n\n  const tagIn = interpolate(frame, [52, 76], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.028]);\n\n  const shadowA = (0.5 + bandBoost * 0.3) * strength;\n  const lightA = (0.62 + bandBoost * 0.38) * strength;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.surface, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/*\n         * The plate, and why it is mid-tone.\n         *\n         * Engraving is legible only where BOTH a white highlight and a black\n         * shadow have somewhere to go. Painting the plate as `surface` breaks\n         * that on half the brands — on a light palette the highlight disappears\n         * into white and the cut reads as embossed. Mixing `ink` over `surface`\n         * at 36% lands in the middle of the range from either direction, since\n         * `ink` is dark on light brands and light on dark ones.\n         */}\n        <div style={{ position: \"absolute\", inset: 0, backgroundColor: alpha(t.ink, 0.36) }} />\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: `linear-gradient(168deg, ${alpha(\"#fff\", 0.2)} 0%, ${alpha(\n              \"#fff\",\n              0.08,\n            )} 22%, ${alpha(\"#fff\", 0.01)} 40%, ${alpha(\"#000\", 0.04)} 58%, ${alpha(\n              \"#000\",\n              0.14,\n            )} 78%, ${alpha(\"#000\", 0.26)} 100%)`,\n          }}\n        />\n        {/* A second, cross-axis falloff. One gradient across a whole plate uses\n            too little of the range and lands on a visible step. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: `radial-gradient(120% 80% at 22% 12%, ${alpha(\"#fff\", 0.14)} 0%, ${alpha(\n              \"#fff\",\n              0.04,\n            )} 32%, ${alpha(\"#000\", 0.02)} 58%, ${alpha(\"#000\", 0.12)} 100%)`,\n          }}\n        />\n\n        {/* Brushed grain: hairlines at sub-pixel alpha. Coarse enough to catch\n            the travelling light, fine enough not to read as stripes. */}\n        {brushed ? (\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              opacity: 0.55,\n              background: `repeating-linear-gradient(179deg, ${alpha(t.ink, 0.05)} 0px, ${alpha(\n                t.ink,\n                0,\n              )} 1px, ${alpha(\"#fff\", 0.035)} 2px, ${alpha(t.ink, 0)} 3px)`,\n            }}\n          />\n        ) : null}\n\n        {/* The light itself, thrown onto the plate. Without this the surface has\n            nothing to explain why the engraving is appearing. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: `radial-gradient(72% 90% at ${lightX * 100}% ${lightY * 100 + 18}%, ${alpha(\n              \"#fff\",\n              0.36 * strength,\n            )} 0%, ${alpha(\"#fff\", 0.16 * strength)} 26%, ${alpha(\"#fff\", 0.05 * strength)} 48%, ${alpha(\n              \"#fff\",\n              0,\n            )} 74%)`,\n            mixBlendMode: \"soft-light\",\n          }}\n        />\n\n        {/* the engraved mark */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: size,\n              fontWeight: 700,\n              lineHeight: 1,\n              letterSpacing: size * 0.02,\n              // The groove floor — the same material as the plate, a shade\n              // darker because it sits below the surface and catches less light.\n              // Everything that makes the letter legible is in the two shadows.\n              color: alpha(\"#000\", 0.07 + 0.06 * strength),\n              textShadow: `${lx * off}px ${ly * off}px ${soft}px ${alpha(\"#000\", shadowA)}, ${\n                -lx * off\n              }px ${-ly * off}px ${soft}px ${alpha(\"#fff\", lightA)}, ${-lx * off * 2.1}px ${\n                -ly * off * 2.1\n              }px ${soft * 3}px ${alpha(\"#fff\", lightA * 0.28)}`,\n            }}\n          >\n            {wordmark}\n          </div>\n\n          {tagline ? (\n            <div\n              style={{\n                marginTop: size * 0.42,\n                fontFamily: t.fontMono,\n                fontSize: size * 0.1,\n                fontWeight: 500,\n                letterSpacing: size * 0.026,\n                textTransform: \"uppercase\",\n                color: t.inkMuted,\n                opacity: tagIn * 0.9,\n                transform: `translateY(${interpolate(tagIn, [0, 1], [size * 0.06, 0])}px)`,\n              }}\n            >\n              {tagline}\n            </div>\n          ) : null}\n        </div>\n\n        {/* the band */}\n        {bandLive ? (\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              pointerEvents: \"none\",\n              mixBlendMode: \"soft-light\",\n              background: `linear-gradient(104deg, ${alpha(\"#fff\", 0)} ${bandT * 100 - 26}%, ${alpha(\n                \"#fff\",\n                0.28,\n              )} ${bandT * 100 - 6}%, ${alpha(\"#fff\", 0.5)} ${bandT * 100}%, ${alpha(\n                \"#fff\",\n                0.28,\n              )} ${bandT * 100 + 6}%, ${alpha(\"#fff\", 0)} ${bandT * 100 + 26}%)`,\n            }}\n          />\n        ) : null}\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(118% 84% at 42% 44%, ${alpha(\"#000\", 0)} 28%, ${alpha(\n            \"#000\",\n            0.1,\n          )} 58%, ${alpha(\"#000\", 0.3)} 86%, ${alpha(\"#000\", 0.44)} 100%)`,\n        }}\n      />\n      <Grain id=\"etched-light-grain\" frame={frame} opacity={0.09} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"etched-light-reveal\",\n  name: \"Etched Light Reveal\",\n  category: \"brand\",\n  summary:\n    \"A wordmark cut into a brushed plate, invisible until a raking light travels across and catches the walls of the groove\",\n  tags: [\"logo\", \"engraved\", \"etched\", \"brushed metal\", \"light reveal\", \"material\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: etchedLightRevealSchema,\n  defaults: {\n    wordmark: \"MOTIONSTASH\",\n    tagline: \"Registry\",\n    depth: 0.6,\n    brushed: true,\n    brand: \"slate\",\n  },\n  component: EtchedLightReveal,\n  file: \"src/registry/items/etched-light-reveal.tsx\",\n  origin: {\n    prompt:\n      \"A logo reveal with no animation on the logo at all. The wordmark is engraved into a brushed metal plate from the first frame and is only revealed because a light rakes across the surface and starts catching the walls of the cut. Get the physics right: on an engraved letter the shadow falls toward the light and the highlight away from it, which is the opposite of an embossed one. Park the light at a low angle where engraving reads best, then pass one broad specular band across to deepen the cut.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A logo reveal with no animation on the logo at all. The wordmark is engraved into a brushed metal plate from the first frame and is only revealed because a light rakes across the surface and starts catching the walls of the cut. Get the physics right: on an engraved letter the shadow falls toward the light and the highlight away from it, which is the opposite of an embossed one. Park the light at a low angle where engraving reads best, then pass one broad specular band across to deepen the cut.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4406,
      "props": [
        {
          "name": "wordmark",
          "type": "string",
          "description": "The engraved mark. One word reads best — the reveal depends on strokes wide enough to hold a highlight and a shadow.",
          "required": true,
          "defaultValue": "MOTIONSTASH",
          "options": null
        },
        {
          "name": "tagline",
          "type": "string",
          "description": "Line under the mark, printed on the plate rather than engraved into it.",
          "required": false,
          "defaultValue": "Registry",
          "options": null
        },
        {
          "name": "depth",
          "type": "number",
          "description": "How deep the engraving is cut. Higher offsets the highlight and shadow further, which reads as a deeper groove.",
          "required": false,
          "defaultValue": "0.6",
          "options": null
        },
        {
          "name": "brushed",
          "type": "boolean",
          "description": "Draw the plate as brushed metal. Turn off for a matte machined surface.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "fade-through",
      "name": "Fade Through",
      "category": "transition",
      "categoryLabel": "Transitions",
      "summary": "Two scenes hand off through a single frame of brand colour, giving the eye somewhere clean to reset instead of muddying both",
      "tags": [
        "transition",
        "cut",
        "flash",
        "handoff",
        "broadcast"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/fade-through.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, Sequence, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const fadeThroughSchema = z.object({\n  first: z.string().default(\"Before\").describe(\"Headline of the outgoing scene.\"),\n  second: z.string().default(\"After\").describe(\"Headline of the incoming scene.\"),\n  through: z\n    .enum([\"black\", \"white\", \"accent\"])\n    .default(\"accent\")\n    .describe(\"The colour the cut passes through at its midpoint.\"),\n  hold: z\n    .number()\n    .min(12)\n    .max(90)\n    .default(48)\n    .describe(\"Frames each scene holds before and after the transition.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof fadeThroughSchema>;\n\n/**\n * The handoff model, made visible.\n *\n * A straight cross-fade between two scenes muddies both at the midpoint, because\n * for a few frames the viewer sees two compositions at half strength and reads\n * neither. Routing the cut through a solid colour gives the eye one clean frame\n * to reset on, which is why broadcast has done it this way forever. The flash is\n * short — around a fifth of a second — and its peak is placed exactly on the cut.\n */\nconst FadeThrough: React.FC<Props> = ({ first, second, through, hold, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const flashFrames = 14;\n  const cut = hold + flashFrames / 2;\n  const total = hold * 2 + flashFrames;\n\n  const flashColor = through === \"black\" ? \"#000000\" : through === \"white\" ? \"#ffffff\" : t.accent;\n\n  // Peaks on the cut frame and is gone within a few frames either side.\n  const flash = interpolate(\n    stage.frame,\n    [cut - flashFrames / 2, cut, cut + flashFrames / 2],\n    [0, 1, 0],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.travel },\n  );\n\n  const showSecond = stage.frame >= cut;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <Sequence from={0} durationInFrames={Math.ceil(cut)} layout=\"none\">\n        <Panel label=\"01\" headline={first} tokens={t} width={width} accent={false} />\n      </Sequence>\n\n      {showSecond ? (\n        <Sequence from={Math.ceil(cut)} durationInFrames={total} layout=\"none\">\n          <Panel label=\"02\" headline={second} tokens={t} width={width} accent />\n        </Sequence>\n      ) : null}\n\n      <AbsoluteFill style={{ backgroundColor: flashColor, opacity: flash }} />\n    </AbsoluteFill>\n  );\n};\n\nconst Panel: React.FC<{\n  label: string;\n  headline: string;\n  tokens: BrandTokens;\n  width: number;\n  accent: boolean;\n}> = ({ label, headline, tokens, width, accent }) => (\n  <AbsoluteFill\n    style={{\n      backgroundColor: accent ? tokens.surface : tokens.bg,\n      justifyContent: \"center\",\n      paddingLeft: width * 0.1,\n    }}\n  >\n    <div\n      style={{\n        fontFamily: tokens.fontMono,\n        fontSize: width * 0.014,\n        letterSpacing: width * 0.0026,\n        textTransform: \"uppercase\",\n        color: accent ? tokens.accent : tokens.inkMuted,\n        marginBottom: width * 0.018,\n      }}\n    >\n      Scene {label}\n    </div>\n    <div\n      style={{\n        fontFamily: tokens.fontDisplay,\n        fontSize: width * 0.088,\n        fontWeight: 700,\n        lineHeight: 1,\n        letterSpacing: width * -0.002,\n        color: tokens.ink,\n      }}\n    >\n      {headline}\n    </div>\n    <div\n      style={{\n        marginTop: width * 0.022,\n        width: width * 0.09,\n        height: Math.max(2, width * 0.0022),\n        backgroundColor: accent ? tokens.accent : tokens.rule,\n      }}\n    />\n  </AbsoluteFill>\n);\n\nexport default defineMotionItem({\n  id: \"fade-through\",\n  name: \"Fade Through\",\n  category: \"transition\",\n  summary:\n    \"Two scenes hand off through a single frame of brand colour, giving the eye somewhere clean to reset instead of muddying both\",\n  tags: [\"transition\", \"cut\", \"flash\", \"handoff\", \"broadcast\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: fadeThroughSchema,\n  defaults: { first: \"Before\", second: \"After\", through: \"accent\", hold: 48, brand: \"slate\" },\n  component: FadeThrough,\n  file: \"src/registry/items/fade-through.tsx\",\n  origin: {\n    prompt:\n      \"A transition between two scenes that passes through a brief full-frame flash of the brand colour at the cut point, rather than a straight cross-fade.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A transition between two scenes that passes through a brief full-frame flash of the brand colour at the cut point, rather than a straight cross-fade.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2217,
      "props": [
        {
          "name": "first",
          "type": "string",
          "description": "Headline of the outgoing scene.",
          "required": false,
          "defaultValue": "Before",
          "options": null
        },
        {
          "name": "second",
          "type": "string",
          "description": "Headline of the incoming scene.",
          "required": false,
          "defaultValue": "After",
          "options": null
        },
        {
          "name": "through",
          "type": "\"black\" | \"white\" | \"accent\"",
          "description": "The colour the cut passes through at its midpoint.",
          "required": false,
          "defaultValue": "accent",
          "options": [
            "black",
            "white",
            "accent"
          ]
        },
        {
          "name": "hold",
          "type": "number",
          "description": "Frames each scene holds before and after the transition.",
          "required": false,
          "defaultValue": "48",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "film-grain-overlay",
      "name": "Film Grain Overlay",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "Deterministic per-frame grain, scanlines and a vignette, seeded on the frame index so it reproduces byte-for-byte",
      "tags": [
        "grain",
        "overlay",
        "texture",
        "deterministic",
        "film"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/film-grain-overlay.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const filmGrainOverlaySchema = z.object({\n  grain: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.5)\n    .describe(\"Grain strength. Above ~0.7 it reads as texture rather than film.\"),\n  cells: z\n    .number()\n    .min(24)\n    .max(72)\n    .default(48)\n    .describe(\"Grain cells across the frame. Higher is finer but costs render time.\"),\n  scanlines: z.boolean().default(false).describe(\"Add faint horizontal scanlines.\"),\n  vignette: z.number().min(0).max(1).default(0.55).describe(\"Corner darkening.\"),\n  headline: z\n    .string()\n    .default(\"Shot on code\")\n    .describe(\"Sample copy shown beneath the overlay, so the preview reads in context.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof filmGrainOverlaySchema>;\n\n/**\n * Grain in a deterministic renderer.\n *\n * The obvious implementation reaches for Math.random(), which is wrong here: the\n * renderer evaluates frames independently and in parallel, so the same frame can\n * be drawn twice with different noise, and a re-render never matches. Seeding on\n * `frame` plus the cell index instead gives noise that changes every frame and\n * still reproduces byte-for-byte. That is the whole trick, and it applies to any\n * \"random\" effect in a frame-addressable pipeline.\n */\nconst FilmGrainOverlay: React.FC<Props> = ({\n  grain,\n  cells,\n  scanlines,\n  vignette,\n  headline,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  const cell = width / cells;\n  const rows = Math.ceil(height / cell);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* A sample scene, so the overlay is judged in context rather than on grey. */}\n      <AbsoluteFill\n        style={{\n          backgroundImage: `linear-gradient(140deg, ${t.surface} 0%, ${t.bg} 55%, ${t.accent}22 100%)`,\n        }}\n      />\n      <AbsoluteFill style={{ justifyContent: \"center\", alignItems: \"center\" }}>\n        <div\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: width * 0.072,\n            fontWeight: 700,\n            letterSpacing: width * -0.0016,\n            color: t.ink,\n            opacity: stage.presence,\n          }}\n        >\n          {headline}\n        </div>\n      </AbsoluteFill>\n\n      {/* Grain */}\n      <svg width={width} height={height} style={{ position: \"absolute\", inset: 0, opacity: grain * 0.5 }}>\n        {Array.from({ length: rows }, (_, r) =>\n          Array.from({ length: cells }, (_, c) => {\n            const a = random(`g-${stage.frame}-${r}-${c}`);\n            if (a < 0.55) return null;\n            return (\n              <rect\n                key={`${r}-${c}`}\n                x={c * cell}\n                y={r * cell}\n                width={cell}\n                height={cell}\n                fill={a > 0.85 ? \"#ffffff\" : \"#000000\"}\n                opacity={(a - 0.55) * 0.5}\n              />\n            );\n          }),\n        )}\n      </svg>\n\n      {scanlines ? (\n        <AbsoluteFill\n          style={{\n            backgroundImage: `repeating-linear-gradient(180deg, rgba(0,0,0,0.13) 0px, rgba(0,0,0,0.13) 1px, transparent 1px, transparent ${Math.max(\n              2,\n              height * 0.0035,\n            )}px)`,\n            opacity: 0.7,\n          }}\n        />\n      ) : null}\n\n      <AbsoluteFill\n        style={{\n          backgroundImage: `radial-gradient(ellipse at 50% 50%, transparent 45%, #000000 118%)`,\n          opacity: vignette * 0.8,\n        }}\n      />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"film-grain-overlay\",\n  name: \"Film Grain Overlay\",\n  category: \"background\",\n  summary:\n    \"Deterministic per-frame grain, scanlines and a vignette, seeded on the frame index so it reproduces byte-for-byte\",\n  tags: [\"grain\", \"overlay\", \"texture\", \"deterministic\", \"film\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: filmGrainOverlaySchema,\n  defaults: {\n    grain: 0.5,\n    cells: 48,\n    scanlines: false,\n    vignette: 0.55,\n    headline: \"Shot on code\",\n    brand: \"slate\",\n  },\n  component: FilmGrainOverlay,\n  file: \"src/registry/items/film-grain-overlay.tsx\",\n  origin: {\n    prompt:\n      \"An animated film grain and vignette overlay that changes every frame but renders identically every time, shown over a sample headline so the effect reads in context.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An animated film grain and vignette overlay that changes every frame but renders identically every time, shown over a sample headline so the effect reads in context.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7517,
      "props": [
        {
          "name": "grain",
          "type": "number",
          "description": "Grain strength. Above ~0.7 it reads as texture rather than film.",
          "required": false,
          "defaultValue": "0.5",
          "options": null
        },
        {
          "name": "cells",
          "type": "number",
          "description": "Grain cells across the frame. Higher is finer but costs render time.",
          "required": false,
          "defaultValue": "48",
          "options": null
        },
        {
          "name": "scanlines",
          "type": "boolean",
          "description": "Add faint horizontal scanlines.",
          "required": false,
          "defaultValue": "false",
          "options": null
        },
        {
          "name": "vignette",
          "type": "number",
          "description": "Corner darkening.",
          "required": false,
          "defaultValue": "0.55",
          "options": null
        },
        {
          "name": "headline",
          "type": "string",
          "description": "Sample copy shown beneath the overlay, so the preview reads in context.",
          "required": false,
          "defaultValue": "Shot on code",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "focus-pull-lines",
      "name": "Focus Pull Lines",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "Focus racks down a stack of lines with real lens behaviour — blur, focus breathing, contrast loss and bloom",
      "tags": [
        "typography",
        "focus pull",
        "rack focus",
        "vertical",
        "depth of field",
        "premium"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/focus-pull-lines.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const focusPullLinesSchema = z.object({\n  lines: z\n    .array(z.string())\n    .min(2)\n    .max(4)\n    .describe(\"The lines, in order. Focus racks from the first to the last, so put the payoff at the end. Two to four lines; four is already a lot for five seconds.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small tracked label above the block. Stays sharp throughout.\"),\n  playhead: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw an accent bar that travels down the left edge to the line currently in focus.\"),\n  aperture: z\n    .number()\n    .min(0.2)\n    .max(1)\n    .default(0.62)\n    .describe(\"How shallow the depth of field is. Higher blurs the out-of-focus lines harder — 0.6 reads as a fast portrait lens, 0.25 as a phone camera.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof focusPullLinesSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.86\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * One line of copy, at some distance from the plane of focus.\n *\n * A real lens does four things at once to a defocused subject, and doing only\n * the first is why most \"focus pull\" effects look like a blur filter:\n *\n *  1. it blurs,\n *  2. it magnifies slightly — focus breathing, so out-of-focus text is BIGGER,\n *  3. it loses local contrast, so the subject sits back toward the ground, and\n *  4. it blooms, spreading light past the letterforms.\n *\n * `distance` is signed and in line-units, so the caller does not have to know\n * any of this.\n */\nconst Line: React.FC<{\n  text: string;\n  distance: number;\n  aperture: number;\n  size: number;\n  t: typeof DEFAULT_BRAND;\n}> = ({ text, distance, aperture, size, t }) => {\n  const d = Math.abs(distance);\n  const blur = Math.min(size * 0.14, d * size * 0.052 * aperture);\n  const magnify = 1 + Math.min(0.05, d * 0.021);\n  const focused = Math.max(0, 1 - d);\n  const ink = interpolate(focused, [0, 1], [0.34, 1]);\n\n  return (\n    <div style={{ position: \"relative\" }}>\n      {/* bloom pass — only meaningful while the line is soft */}\n      {d > 0.15 ? (\n        <div\n          aria-hidden\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            filter: `blur(${blur * 2.1}px)`,\n            opacity: Math.min(0.42, d * 0.3),\n            mixBlendMode: \"screen\",\n            fontFamily: t.fontDisplay,\n            fontSize: size,\n            fontWeight: 600,\n            lineHeight: 1.16,\n            letterSpacing: size * -0.028,\n            color: t.ink,\n            transform: `scale(${magnify})`,\n            transformOrigin: \"0% 50%\",\n          }}\n        >\n          {text}\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          fontFamily: t.fontDisplay,\n          fontSize: size,\n          fontWeight: 600,\n          lineHeight: 1.16,\n          letterSpacing: size * -0.028,\n          color: alpha(t.ink, ink),\n          filter: `blur(${blur}px)`,\n          transform: `scale(${magnify})`,\n          transformOrigin: \"0% 50%\",\n        }}\n      >\n        {text}\n      </div>\n    </div>\n  );\n};\n\nconst FocusPullLines: React.FC<Props> = ({ lines, eyebrow, playhead, aperture, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const size = width * (lines.length > 3 ? 0.088 : 0.105);\n  const gap = size * 0.52;\n\n  /**\n   * The rack.\n   *\n   * `focus` is a continuous position in line-units, not an index, so the lens\n   * travels *through* the intermediate lines rather than cutting between them —\n   * which is the whole point of a rack focus. It rests on each line for most of\n   * its dwell and moves between them quickly, the way a focus puller works.\n   */\n  const last = lines.length - 1;\n  const holdIn = 12;\n  const travel = 13;\n  const dwell = Math.max(\n    16,\n    Math.floor((BUDGET.enter + BUDGET.hold - holdIn - travel * last) / lines.length),\n  );\n\n  const stops: number[] = [];\n  const values: number[] = [];\n  for (let i = 0; i <= last; i += 1) {\n    const arrive = holdIn + i * (dwell + travel);\n    stops.push(arrive, arrive + dwell);\n    values.push(i, i);\n  }\n  const focus = interpolate(frame, stops, values, {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  // The block drifts up over the whole shot; the camera also pushes very\n  // slightly. Both are below the threshold of notice and both are why the frame\n  // does not feel like a slide.\n  const drift = interpolate(stage.progress, [0, 1], [height * 0.014, -height * 0.014]);\n  const push = interpolate(stage.progress, [0, 1], [1, 1.03]);\n\n  const blockIn = interpolate(frame, [0, 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const railTop = size * 0.2;\n  const playheadY = railTop + focus * (size * 1.16 + gap);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* A soft key from the upper left, so the type sits in a room. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(95% 62% at 18% 8%, ${alpha(t.surface, 0.85)} 0%, ${alpha(\n            t.surface,\n            0.32,\n          )} 32%, ${alpha(t.bg, 0.8)} 64%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(55% 38% at 88% 96%, ${alpha(t.accent, 0.14)} 0%, ${alpha(\n            t.accent,\n            0,\n          )} 68%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"center\",\n          padding: `0 ${width * 0.1}px`,\n          transform: `translateY(${drift}px) scale(${push})`,\n          transformOrigin: \"20% 50%\",\n          opacity: stage.presence * blockIn,\n        }}\n      >\n        {eyebrow ? (\n          <div\n            style={{\n              marginBottom: size * 0.72,\n              fontFamily: t.fontMono,\n              fontSize: size * 0.17,\n              fontWeight: 500,\n              letterSpacing: size * 0.05,\n              textTransform: \"uppercase\",\n              color: t.accent,\n            }}\n          >\n            {eyebrow}\n          </div>\n        ) : null}\n\n        <div style={{ position: \"relative\", paddingLeft: playhead ? size * 0.46 : 0 }}>\n          {playhead ? (\n            <>\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  top: railTop,\n                  width: Math.max(1, size * 0.018),\n                  height: (lines.length - 1) * (size * 1.16 + gap) + size * 0.8,\n                  backgroundColor: alpha(t.ink, 0.12),\n                }}\n              />\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  top: playheadY,\n                  width: Math.max(2, size * 0.038),\n                  height: size * 0.8,\n                  marginLeft: -size * 0.01,\n                  backgroundColor: t.accent,\n                  boxShadow: `0 0 ${size * 0.3}px ${alpha(t.accent, 0.6)}`,\n                }}\n              />\n            </>\n          ) : null}\n\n          <div style={{ display: \"flex\", flexDirection: \"column\", gap }}>\n            {lines.map((line, i) => (\n              <Line\n                key={`${line}-${i}`}\n                text={line}\n                distance={i - focus}\n                aperture={aperture}\n                size={size}\n                t={t}\n              />\n            ))}\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(118% 84% at 34% 44%, ${alpha(\"#000\", 0)} 30%, ${alpha(\n            \"#000\",\n            0.16,\n          )} 60%, ${alpha(\"#000\", 0.46)} 90%, ${alpha(\"#000\", 0.62)} 100%)`,\n        }}\n      />\n      <Grain id=\"focus-pull-grain\" frame={frame} opacity={0.08} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"focus-pull-lines\",\n  name: \"Focus Pull Lines\",\n  category: \"typography\",\n  summary:\n    \"Focus racks down a stack of lines with real lens behaviour — blur, focus breathing, contrast loss and bloom\",\n  tags: [\"typography\", \"focus pull\", \"rack focus\", \"vertical\", \"depth of field\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: focusPullLinesSchema,\n  defaults: {\n    lines: [\"Pick a component.\", \"Fill its props.\", \"Render the film.\"],\n    eyebrow: \"How it works\",\n    playhead: true,\n    aperture: 0.62,\n    brand: \"slate\",\n  },\n  component: FocusPullLines,\n  file: \"src/registry/items/focus-pull-lines.tsx\",\n  origin: {\n    prompt:\n      \"A vertical text sequence where the camera racks focus down three lines of copy, one at a time. It has to behave like a real lens, not a blur filter: out-of-focus lines should be slightly larger from focus breathing, lower in contrast, and bloom a little past their edges. Move the focus continuously through the lines instead of cutting between them, and mark the live line with a small accent playhead on the left.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical text sequence where the camera racks focus down three lines of copy, one at a time. It has to behave like a real lens, not a blur filter: out-of-focus lines should be slightly larger from focus breathing, lower in contrast, and bloom a little past their edges. Move the focus continuously through the lines instead of cutting between them, and mark the live line with a small accent playhead on the left.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6843,
      "props": [
        {
          "name": "lines",
          "type": "array",
          "description": "The lines, in order. Focus racks from the first to the last, so put the payoff at the end. Two to four lines; four is already a lot for five seconds.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small tracked label above the block. Stays sharp throughout.",
          "required": false,
          "defaultValue": "How it works",
          "options": null
        },
        {
          "name": "playhead",
          "type": "boolean",
          "description": "Draw an accent bar that travels down the left edge to the line currently in focus.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "aperture",
          "type": "number",
          "description": "How shallow the depth of field is. Higher blurs the out-of-focus lines harder — 0.6 reads as a fast portrait lens, 0.25 as a phone camera.",
          "required": false,
          "defaultValue": "0.62",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "funnel-steps",
      "name": "Funnel Steps",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Bands narrow downward as each opens out of the centre line, counts tick up inside them and the drop-off from the stage above sets beside each edge",
      "tags": [
        "funnel",
        "conversion",
        "drop-off",
        "counter",
        "stagger",
        "trapezoid"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/funnel-steps.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/* ------------------------------------------------------------------ schema */\n\nconst stageSchema = z.object({\n  label: z\n    .string()\n    .describe(\n      \"Stage name, set inside the band. Two words at most: 'Pricing page', 'Trial started'. The band is the narrowest part of the layout at the bottom of the funnel, so a long label is truncated with an ellipsis rather than wrapped — write the short form yourself.\",\n    ),\n  value: z\n    .number()\n    .min(0)\n    .describe(\n      \"How many reached this stage, in absolute units. Band width is proportional to this against the first stage, and the drop-off beside each band is computed from the stage above, so give real counts rather than percentages.\",\n    ),\n});\n\nexport const funnelStepsSchema = z.object({\n  stages: z\n    .array(stageSchema)\n    .min(2)\n    .max(6)\n    .describe(\n      \"The funnel, widest first. Order is the argument: the bands narrow downward in exactly this order and no sorting is applied, so a stage larger than the one above it reads as a gain (+%) rather than a drop. Four stages is the sweet spot; six is the ceiling before the bands thin out.\",\n    ),\n  title: z\n    .string()\n    .optional()\n    .describe(\n      \"Headline above the funnel, in the display face. Name the population being funnelled ('Trial to paid, last 30 days'), not the shape ('Our funnel'). One line reads best.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono label in the accent colour above the title. Omit for a barer frame.\"),\n  summaryLabel: z\n    .string()\n    .optional()\n    .describe(\n      \"Label for the end-to-end conversion figure in the top right, which counts up to last ÷ first as the funnel builds. Omit the label to drop that figure entirely and let the bands carry the story.\",\n    ),\n  dropoff: z\n    .enum([\"percent\", \"count\", \"both\", \"none\"])\n    .default(\"percent\")\n    .describe(\n      \"What the muted annotation beside each band says about the step above it: 'percent' the share lost, 'count' the absolute number lost, 'both' stacks the two, 'none' hides the annotation and re-centres the funnel in the frame.\",\n    ),\n  minWidth: z\n    .number()\n    .min(0.1)\n    .max(0.8)\n    .default(0.4)\n    .describe(\n      \"Width the smallest band keeps as a fraction of the widest, so the last stage stays legible when its count is a rounding error of the first. It flatters small stages: 0 makes the widths strictly proportional and honest, 0.4 keeps a real funnel silhouette.\",\n    ),\n  valuePrefix: z\n    .string()\n    .optional()\n    .describe(\"Set immediately before every figure, on the bands and the drop-off alike — a currency mark such as '$' or '€'.\"),\n  valueSuffix: z\n    .string()\n    .optional()\n    .describe(\n      \"Set immediately after every figure — 'MRR', 'sessions', 'k'. Rendered exactly as typed, so include a leading thin space yourself if the unit needs air.\",\n    ),\n  compact: z\n    .boolean()\n    .default(false)\n    .describe(\n      \"Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Grouped digits are more precise and read better in a band; switch this on once the figures pass six digits.\",\n    ),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono caption under a hairline at the foot of the frame: the source, the window, the qualifier. One short sentence.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof funnelStepsSchema>;\n\n/* ------------------------------------------------------------------ timing */\n\nconst BUDGET = BUDGETS.scene;\n\n/*\n * Beats, in frames from this component's own frame 0. The funnel is read top to\n * bottom, so it is built top to bottom: each band opens horizontally out of the\n * centre line, its type is revealed by the opening band itself, its figure\n * counts up, and only then does the drop-off annotation arrive beside it — the\n * loss cannot be stated before the number it is a loss from.\n *\n * With four stages the last annotation settles around frame 65 of a 150-frame\n * budget, well inside the hold. That is the contract: presence is spent in\n * `enter`, but a counter is the canonical piece of internal motion the hold is\n * allowed to carry.\n */\nconst HEAD_START = 0;\nconst BANDS_START = 12;\n/** Frames one band takes to open to full width. */\nconst BAND_GROW = 22;\n/** Fraction of a band's opening that passes before its type starts to appear. */\nconst TEXT_LEAD = 0.34;\nconst TEXT_FADE = 12;\n/** Fraction of a band's opening that passes before its figure starts counting. */\nconst COUNT_LEAD = 0.3;\n/** Frames the counter runs on past its own band, so the figure settles last. */\nconst COUNT_TAIL = 8;\n/** Fraction of the lower band's opening that passes before its connector shows. */\nconst CONNECT_LEAD = 0.45;\nconst CONNECT_FADE = 16;\n/** Frames after a band has landed before its drop-off annotation arrives. */\nconst DROP_DELAY = BAND_GROW + 4;\nconst DROP_FADE = 13;\n\n/**\n * Per-band delay. Held inside the readable stagger range (roughly 150–300ms at\n * 30fps — slower than a list, because each band is a sentence rather than an\n * item) and tightened as the funnel grows, so six stages still read as one\n * gesture narrowing down the frame.\n */\nconst rowStagger = (count: number) => Math.min(9, Math.max(4.5, 30 / count));\n\nconst CLAMP = { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" } as const;\n\n/* --------------------------------------------------------------- numbers */\n\n/** Typographic thousands separator. A comma is punctuation; this is spacing. */\nconst THIN_SPACE = \"\\u2009\";\n/** A real minus sign, not a hyphen. At 30px the difference is the whole design. */\nconst MINUS = \"\\u2212\";\n\nconst groupDigits = (digits: string) => digits.replace(/\\B(?=(\\d{3})+(?!\\d))/g, THIN_SPACE);\n\nconst trimZero = (n: number, places: number) => n.toFixed(places).replace(/\\.0$/, \"\");\n\n/**\n * Decimal places implied by a number — 0 for integers, 1 for tenths, 2 beyond.\n * Derived rather than configured so every band agrees, and so a funnel of whole\n * counts never grows a trailing \".0\" halfway through a count-up.\n */\nconst decimalsFor = (n: number): number => {\n  if (Math.abs(n - Math.round(n)) < 1e-9) return 0;\n  if (Math.abs(n * 10 - Math.round(n * 10)) < 1e-9) return 1;\n  return 2;\n};\n\nconst formatFigure = (value: number, decimals: number, compact: boolean): string => {\n  const abs = Math.abs(value);\n  if (compact && abs >= 1e6) return `${trimZero(abs / 1e6, 1)}M`;\n  if (compact && abs >= 1e3) return `${trimZero(abs / 1e3, abs >= 1e4 ? 0 : 1)}k`;\n\n  const fixed = abs.toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const whole = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fraction = dot === -1 ? \"\" : fixed.slice(dot);\n  return `${groupDigits(whole)}${fraction}`;\n};\n\n/**\n * Percent, with the decimal count fixed by the *settled* value rather than the\n * animated one — so a figure counting up to 5.7% keeps its decimal place for\n * every frame of the count instead of shedding it at 3.0. That shimmer is the\n * classic tell of an animated number.\n */\nconst formatPercent = (value: number, settled: number): string =>\n  `${Math.abs(value).toFixed(Math.abs(settled) >= 10 ? 0 : 1)}%`;\n\n/* ------------------------------------------------------------------ layout */\n\n/** Type sizes in frame pixels. */\ntype Scale = {\n  eyebrow: number;\n  title: number;\n  kpiLabel: number;\n  kpiValue: number;\n  name: number;\n  count: number;\n  drop: number;\n  foot: number;\n};\n\n/** One band, fully resolved: geometry, colour and its own place in time. */\ntype Row = {\n  label: string;\n  value: number;\n  /** Final band width in px. The annotation anchors to this, not to `cw`. */\n  w: number;\n  /** Width at the current frame. Drives the band and its connector alike. */\n  cw: number;\n  top: number;\n  /** Frame this band starts opening. */\n  start: number;\n  isLast: boolean;\n  fill: string;\n  ink: string;\n  /** Share lost against the stage above, in percent. Null for the first stage. */\n  dropPercent: number | null;\n  /** Absolute number lost against the stage above. Null for the first stage. */\n  dropCount: number | null;\n};\n\n/* --------------------------------------------------------------- component */\n\nconst FunnelSteps: React.FC<Props> = ({\n  stages,\n  title,\n  eyebrow,\n  summaryLabel,\n  dropoff,\n  minWidth,\n  valuePrefix,\n  valueSuffix,\n  compact,\n  footnote,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const f = stage.frame;\n\n  /*\n   * Everything is sized off `base`, one length that survives every aspect: in\n   * landscape the height is the binding constraint, in portrait the width is.\n   * Sizing type off `width` alone would give a 9:16 cut 18px band labels.\n   */\n  const base = Math.min(width, height * 1.6);\n\n  const scale: Scale = {\n    eyebrow: base * 0.0125,\n    title: base * 0.0345,\n    kpiLabel: base * 0.0115,\n    kpiValue: base * 0.0315,\n    name: base * 0.0168,\n    count: base * 0.0195,\n    drop: base * 0.0135,\n    foot: base * 0.0118,\n  };\n\n  const n = stages.length;\n  const gaps = Math.max(1, n - 1);\n  const hair = Math.max(1, base * 0.0007);\n\n  const padX = width * 0.072;\n  const padTop = height * 0.082;\n  const padBottom = height * 0.072;\n  const contentW = width - padX * 2;\n\n  // The header is as tall as whichever of its two columns is taller, so the\n  // funnel below never has to know which of them the caller asked for.\n  const headerH = Math.max(\n    (eyebrow ? scale.eyebrow * 2.15 : 0) + (title ? scale.title * 1.16 : 0),\n    summaryLabel ? scale.kpiLabel * 2.0 + scale.kpiValue * 1.1 : 0,\n  );\n  const footH = footnote ? scale.foot * 2.7 : 0;\n\n  /*\n   * The annotations live in a reserved column at the right, and the funnel is\n   * centred in what is left rather than in the frame: a symmetrical funnel with\n   * type down only one side is not actually centred, it just looks off-axis.\n   * Drop the annotations and the reserved column goes with them.\n   */\n  const annoW = dropoff === \"none\" ? 0 : contentW * 0.17;\n  const fieldW = contentW - annoW;\n  const centreX = padX + fieldW / 2;\n  const widest = fieldW * 0.9;\n\n  const plotTop = padTop + headerH + height * 0.055;\n  // The footnote gets clear air above its rule as well as below it: the funnel\n  // ending flush on that hairline reads as the band resting on a baseline it\n  // has nothing to do with.\n  const plotBottom = height - padBottom - footH - (footnote ? height * 0.035 : 0);\n  const plotH = Math.max(base * 0.2, plotBottom - plotTop);\n\n  /*\n   * Band height comes from the ideal band-to-connector ratio, then gets capped\n   * so a tall 9:16 frame does not inflate four bands into four billboards; the\n   * connector gap absorbs whatever slack the cap leaves, within limits, and the\n   * stack sits a little above the middle of what is left — dead centre under a\n   * top-weighted header optically reads low. One rule, four aspects.\n   */\n  const bandH = Math.min(plotH / (n + 0.5 * gaps), base * 0.11);\n  const gapY = Math.min(bandH * 1.1, Math.max(bandH * 0.34, (plotH - bandH * n) / gaps));\n  const funnelH = bandH * n + gapY * gaps;\n  const funnelTop = plotTop + Math.max(0, (plotH - funnelH) * 0.42);\n  const radius = Math.min(bandH * 0.22, Math.max(2, t.radius * 1.4));\n\n  const first = stages[0]?.value ?? 0;\n  const peak = Math.max(0, ...stages.map((s) => s.value));\n  const step = rowStagger(n);\n  const lastLanding = BANDS_START + step * (n - 1) + BAND_GROW;\n\n  const decimals = stages.reduce((acc, s) => Math.max(acc, decimalsFor(s.value)), 0);\n  const unit = (figure: string) => `${valuePrefix ?? \"\"}${figure}${valueSuffix ?? \"\"}`;\n  const fmt = (v: number) => unit(formatFigure(v, decimals, compact));\n\n  const rows: Row[] = stages.map((s, i) => {\n    const isLast = i === n - 1;\n    const start = BANDS_START + step * i;\n\n    /*\n     * The one accent gesture in the frame: the final band — the one the whole\n     * funnel is an argument about — opens on the overshoot curve while every\n     * band above it uses the plain entrance. Weight, not bounce.\n     */\n    const grow = interpolate(f, [start, start + BAND_GROW], [0, 1], {\n      ...CLAMP,\n      easing: isLast ? EASING.accent : EASING.enter,\n    });\n\n    const share = peak > 0 ? Math.max(0, s.value) / peak : 1;\n    const w = widest * (minWidth + (1 - minWidth) * share);\n\n    const above = i > 0 ? stages[i - 1].value : null;\n    const lost = above === null ? null : above - s.value;\n\n    return {\n      label: s.label,\n      value: s.value,\n      w,\n      cw: w * grow,\n      top: funnelTop + (bandH + gapY) * i,\n      start,\n      isLast,\n      /*\n       * Fills ramp from `surface` toward `rule` down the funnel, so the stack\n       * reads as intensifying into the accent band rather than as four\n       * identical panels. Stopping at 0.75 keeps the last muted band distinct\n       * from the connectors, which are drawn in `rule` itself.\n       */\n      fill: isLast\n        ? t.accent\n        : interpolateColors((0.75 * i) / gaps, [0, 1], [t.surface, t.rule]),\n      ink: isLast ? t.accentInk : t.ink,\n      dropPercent: above === null || above <= 0 || lost === null ? null : (lost / above) * 100,\n      dropCount: lost,\n    };\n  });\n\n  const headIn = interpolate(f, [HEAD_START, HEAD_START + 12], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const titleIn = interpolate(f, [HEAD_START + 2, HEAD_START + 17], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const footRule = interpolate(f, [lastLanding - 8, lastLanding + 10], [0, 1], { ...CLAMP, easing: EASING.travel });\n  const footIn = interpolate(f, [lastLanding, lastLanding + 14], [0, 1], { ...CLAMP, easing: EASING.enter });\n\n  const lift = (1 - stage.enter) * height * 0.016;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, fontFamily: t.fontBody }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: stage.presence,\n          transform: `translateY(${lift}px)`,\n        }}\n      >\n        {/* Header, hard left. The funnel is the composition; the title labels it. */}\n        <div style={{ position: \"absolute\", left: padX, top: padTop, width: fieldW * 0.72 }}>\n          {eyebrow ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: scale.eyebrow,\n                fontWeight: 600,\n                letterSpacing: scale.eyebrow * 0.18,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                opacity: headIn,\n                transform: `translateY(${(1 - headIn) * scale.eyebrow * 0.7}px)`,\n              }}\n            >\n              {eyebrow}\n            </div>\n          ) : null}\n\n          {title ? (\n            <span\n              style={{\n                display: \"block\",\n                overflow: \"hidden\",\n                marginTop: eyebrow ? scale.eyebrow * 1.1 : 0,\n                paddingBottom: scale.title * 0.08,\n              }}\n            >\n              <span\n                style={{\n                  display: \"block\",\n                  fontFamily: t.fontDisplay,\n                  fontSize: scale.title,\n                  fontWeight: 600,\n                  lineHeight: 1.06,\n                  letterSpacing: scale.title * -0.02,\n                  color: t.ink,\n                  transform: `translateY(${(1 - titleIn) * 108}%)`,\n                }}\n              >\n                {title}\n              </span>\n            </span>\n          ) : null}\n        </div>\n\n        {summaryLabel ? (\n          <Summary\n            label={summaryLabel}\n            percent={first > 0 ? ((stages[n - 1]?.value ?? 0) / first) * 100 : 0}\n            frame={f}\n            landing={lastLanding}\n            right={padX}\n            top={padTop}\n            width={contentW * 0.3}\n            scale={scale}\n            t={t}\n          />\n        ) : null}\n\n        {/* Connectors first, so the bands paint over them. */}\n        <Connectors rows={rows} frame={f} centreX={centreX} bandH={bandH} frameW={width} frameH={height} t={t} />\n\n        {rows.map((row, i) => (\n          <Band\n            key={`${row.label}-${i}`}\n            row={row}\n            frame={f}\n            centreX={centreX}\n            bandH={bandH}\n            radius={radius}\n            hair={hair}\n            scale={scale}\n            fmt={fmt}\n            t={t}\n          />\n        ))}\n\n        {dropoff === \"none\"\n          ? null\n          : rows.map((row, i) =>\n              row.dropPercent === null ? null : (\n                <Dropoff\n                  key={`drop-${row.label}-${i}`}\n                  row={row}\n                  mode={dropoff}\n                  frame={f}\n                  left={centreX + row.w / 2 + width * 0.018}\n                  width={width - padX - (centreX + row.w / 2 + width * 0.018)}\n                  bandH={bandH}\n                  scale={scale}\n                  fmt={fmt}\n                  t={t}\n                />\n              ),\n            )}\n\n        {footnote ? (\n          <div style={{ position: \"absolute\", left: padX, top: height - padBottom - footH, width: contentW }}>\n            <div\n              style={{\n                height: hair,\n                backgroundColor: t.rule,\n                transform: `scaleX(${footRule})`,\n                transformOrigin: \"left\",\n              }}\n            />\n            <div\n              style={{\n                marginTop: scale.foot * 0.85,\n                fontFamily: t.fontMono,\n                fontSize: scale.foot,\n                fontWeight: 500,\n                letterSpacing: scale.foot * 0.02,\n                color: t.inkMuted,\n                opacity: footIn,\n              }}\n            >\n              {footnote}\n            </div>\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------ pieces */\n\n/**\n * One band. It opens by animating its own width symmetrically about the centre\n * line rather than by `scaleX`, which keeps the corner radius circular instead\n * of stretching it into an ellipse, and keeps the growth reading as a measured\n * quantity rather than a squash.\n *\n * The type inside is laid out at the band's FINAL width and pinned to the\n * centre line, so the opening band is itself a hard clipping mask over its own\n * label and figure — a real overflow box, not a gradient fade, which keeps\n * glyph edges crisp at 4K.\n */\nconst Band: React.FC<{\n  row: Row;\n  frame: number;\n  centreX: number;\n  bandH: number;\n  radius: number;\n  hair: number;\n  scale: Scale;\n  fmt: (v: number) => string;\n  t: BrandTokens;\n}> = ({ row, frame, centreX, bandH, radius, hair, scale, fmt, t }) => {\n  const textFrom = row.start + BAND_GROW * TEXT_LEAD;\n  const textIn = interpolate(frame, [textFrom, textFrom + TEXT_FADE], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const count = interpolate(frame, [row.start + BAND_GROW * COUNT_LEAD, row.start + BAND_GROW + COUNT_TAIL], [0, 1], {\n    ...CLAMP,\n    easing: EASING.travel,\n  });\n  const fade = interpolate(frame, [row.start, row.start + 5], [0, 1], CLAMP);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: centreX - row.cw / 2,\n        top: row.top,\n        width: row.cw,\n        height: bandH,\n        borderRadius: radius,\n        backgroundColor: row.fill,\n        opacity: fade,\n        overflow: \"hidden\",\n        // An inset ring instead of a border: it gives the light brands, where\n        // `surface` sits a hair off `bg`, an edge to read — and costs no layout.\n        boxShadow: row.isLast ? undefined : `inset 0 0 0 ${hair}px ${t.rule}`,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: (row.cw - row.w) / 2,\n          top: 0,\n          width: row.w,\n          height: bandH,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          gap: bandH * 0.24,\n          padding: `0 ${bandH * 0.34}px`,\n          boxSizing: \"border-box\",\n          opacity: textIn,\n        }}\n      >\n        <span\n          style={{\n            flex: \"1 1 auto\",\n            minWidth: 0,\n            fontFamily: t.fontBody,\n            fontSize: scale.name,\n            fontWeight: 600,\n            letterSpacing: scale.name * 0.002,\n            color: row.ink,\n            whiteSpace: \"nowrap\",\n            overflow: \"hidden\",\n            textOverflow: \"ellipsis\",\n          }}\n        >\n          {row.label}\n        </span>\n        <span\n          style={{\n            flex: \"0 0 auto\",\n            fontFamily: t.fontMono,\n            fontSize: scale.count,\n            fontWeight: 600,\n            // Tabular figures: the count must not shuffle the label sideways\n            // as its digits change.\n            fontVariantNumeric: \"tabular-nums\",\n            letterSpacing: scale.count * -0.01,\n            color: row.ink,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {fmt(row.value * count)}\n        </span>\n      </div>\n    </div>\n  );\n};\n\n/**\n * The trapezoids between bands, drawn as polygons from the bands' CURRENT\n * widths rather than their final ones, so a connector never reaches past the\n * band it hangs from while that band is still opening. They sit behind the\n * bands and stay quiet: this is the funnel's silhouette, not its data.\n */\nconst Connectors: React.FC<{\n  rows: Row[];\n  frame: number;\n  centreX: number;\n  bandH: number;\n  frameW: number;\n  frameH: number;\n  t: BrandTokens;\n}> = ({ rows, frame, centreX, bandH, frameW, frameH, t }) => (\n  <svg\n    width={frameW}\n    height={frameH}\n    viewBox={`0 0 ${frameW} ${frameH}`}\n    style={{ position: \"absolute\", inset: 0 }}\n  >\n    {rows.slice(1).map((lower, i) => {\n      const upper = rows[i];\n      const yTop = upper.top + bandH;\n      const yBottom = lower.top;\n      const from = lower.start + BAND_GROW * CONNECT_LEAD;\n      const fadeIn = interpolate(frame, [from, from + CONNECT_FADE], [0, 1], { ...CLAMP, easing: EASING.enter });\n      const points = [\n        [centreX - upper.cw / 2, yTop],\n        [centreX + upper.cw / 2, yTop],\n        [centreX + lower.cw / 2, yBottom],\n        [centreX - lower.cw / 2, yBottom],\n      ]\n        .map(([x, y]) => `${x.toFixed(2)},${y.toFixed(2)}`)\n        .join(\" \");\n\n      return (\n        <polygon\n          key={`connector-${i}`}\n          points={points}\n          fill={lower.isLast ? t.accent : t.rule}\n          // The last connector is accent held right down: it prefigures the\n          // band it feeds without competing with it.\n          opacity={fadeIn * (lower.isLast ? 0.22 : 0.55)}\n        />\n      );\n    })}\n  </svg>\n);\n\n/**\n * The drop-off beside a band. It arrives after its own band's figure has\n * settled — the loss cannot be stated before the number it is a loss from —\n * and slides the last few pixels outward from the band edge it belongs to.\n */\nconst Dropoff: React.FC<{\n  row: Row;\n  mode: \"percent\" | \"count\" | \"both\";\n  frame: number;\n  left: number;\n  width: number;\n  bandH: number;\n  scale: Scale;\n  fmt: (v: number) => string;\n  t: BrandTokens;\n}> = ({ row, mode, frame, left, width, bandH, scale, fmt, t }) => {\n  const from = row.start + DROP_DELAY;\n  const show = interpolate(frame, [from, from + DROP_FADE], [0, 1], { ...CLAMP, easing: EASING.enter });\n  const percent = row.dropPercent ?? 0;\n  const lost = row.dropCount ?? 0;\n  // A gain reads as a gain: the sign is the annotation's whole content.\n  const sign = lost < 0 ? \"+\" : MINUS;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left,\n        top: row.top,\n        width: Math.max(0, width),\n        height: bandH,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"center\",\n        gap: scale.drop * 0.32,\n        opacity: show,\n        transform: `translateX(${(1 - show) * scale.drop * -0.8}px)`,\n      }}\n    >\n      {mode === \"percent\" || mode === \"both\" ? (\n        <div\n          style={{\n            fontFamily: t.fontMono,\n            fontSize: scale.drop,\n            fontWeight: 500,\n            fontVariantNumeric: \"tabular-nums\",\n            letterSpacing: scale.drop * 0.01,\n            color: t.inkMuted,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {`${sign}${formatPercent(percent, percent)}`}\n        </div>\n      ) : null}\n\n      {mode === \"count\" || mode === \"both\" ? (\n        <div\n          style={{\n            fontFamily: t.fontMono,\n            fontSize: mode === \"both\" ? scale.drop * 0.82 : scale.drop,\n            fontWeight: 500,\n            fontVariantNumeric: \"tabular-nums\",\n            letterSpacing: scale.drop * 0.01,\n            color: t.inkMuted,\n            opacity: mode === \"both\" ? 0.72 : 1,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {`${sign}${fmt(Math.abs(lost))}`}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/**\n * End-to-end conversion, top right, counting up across the whole build so the\n * frame's one big figure lands with the last band rather than before it.\n */\nconst Summary: React.FC<{\n  label: string;\n  percent: number;\n  frame: number;\n  landing: number;\n  right: number;\n  top: number;\n  width: number;\n  scale: Scale;\n  t: BrandTokens;\n}> = ({ label, percent, frame, landing, right, top, width, scale, t }) => {\n  const show = interpolate(frame, [HEAD_START + 6, HEAD_START + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASING.enter,\n  });\n  const count = interpolate(frame, [BANDS_START + 6, landing + 10], [0, 1], { ...CLAMP, easing: EASING.travel });\n\n  return (\n    <div style={{ position: \"absolute\", right, top, width, textAlign: \"right\", opacity: show }}>\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: scale.kpiLabel,\n          fontWeight: 600,\n          letterSpacing: scale.kpiLabel * 0.16,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          whiteSpace: \"nowrap\",\n          overflow: \"hidden\",\n          textOverflow: \"ellipsis\",\n        }}\n      >\n        {label}\n      </div>\n      <div\n        style={{\n          marginTop: scale.kpiLabel * 0.7,\n          fontFamily: t.fontDisplay,\n          fontSize: scale.kpiValue,\n          fontWeight: 600,\n          lineHeight: 1.0,\n          fontVariantNumeric: \"tabular-nums\",\n          letterSpacing: scale.kpiValue * -0.022,\n          color: t.accent,\n          transform: `translateY(${(1 - show) * scale.kpiValue * 0.14}px)`,\n        }}\n      >\n        {formatPercent(percent * count, percent)}\n      </div>\n    </div>\n  );\n};\n\n/* ---------------------------------------------------------------- registry */\n\nexport default defineMotionItem({\n  id: \"funnel-steps\",\n  name: \"Funnel Steps\",\n  category: \"data\",\n  summary:\n    \"Bands narrow downward as each opens out of the centre line, counts tick up inside them and the drop-off from the stage above sets beside each edge\",\n  tags: [\"funnel\", \"conversion\", \"drop-off\", \"counter\", \"stagger\", \"trapezoid\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: funnelStepsSchema,\n  defaults: {\n    stages: [\n      { label: \"Pricing page\", value: 128400 },\n      { label: \"Trial started\", value: 41260 },\n      { label: \"Activated\", value: 18740 },\n      { label: \"Paid plan\", value: 7320 },\n    ],\n    title: \"Trial to paid, last 30 days\",\n    eyebrow: \"Conversion\",\n    summaryLabel: \"End to end\",\n    dropoff: \"percent\",\n    minWidth: 0.4,\n    valuePrefix: undefined,\n    valueSuffix: undefined,\n    compact: false,\n    footnote: \"Self-serve signups only · activation means one workspace with two or more members\",\n    brand: \"slate\",\n  },\n  component: FunnelSteps,\n  file: \"src/registry/items/funnel-steps.tsx\",\n  origin: {\n    prompt:\n      \"A conversion funnel that narrows downward: four horizontal bands, each narrower than the last, each scaling in horizontally from the centre with a stagger down the frame. On every band put the stage name, the absolute count counting up, and to the right of the band the drop-off from the previous stage in muted type. Trapezoid connectors between the bands drawn as SVG polygons that fade in behind them. Final band in the accent colour. Clean, symmetrical, legible.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A conversion funnel that narrows downward: four horizontal bands, each narrower than the last, each scaling in horizontally from the centre with a stagger down the frame. On every band put the stage name, the absolute count counting up, and to the right of the band the drop-off from the previous stage in muted type. Trapezoid connectors between the bands drawn as SVG polygons that fade in behind them. Final band in the accent colour. Clean, symmetrical, legible.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.27,
      "props": [
        {
          "name": "stages",
          "type": "array",
          "description": "The funnel, widest first. Order is the argument: the bands narrow downward in exactly this order and no sorting is applied, so a stage larger than the one above it reads as a gain (+%) rather than a drop. Four stages is the sweet spot; six is the ceiling before the bands thin out.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "Headline above the funnel, in the display face. Name the population being funnelled ('Trial to paid, last 30 days'), not the shape ('Our funnel'). One line reads best.",
          "required": false,
          "defaultValue": "Trial to paid, last 30 days",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label in the accent colour above the title. Omit for a barer frame.",
          "required": false,
          "defaultValue": "Conversion",
          "options": null
        },
        {
          "name": "summaryLabel",
          "type": "string",
          "description": "Label for the end-to-end conversion figure in the top right, which counts up to last ÷ first as the funnel builds. Omit the label to drop that figure entirely and let the bands carry the story.",
          "required": false,
          "defaultValue": "End to end",
          "options": null
        },
        {
          "name": "dropoff",
          "type": "\"percent\" | \"count\" | \"both\" | \"none\"",
          "description": "What the muted annotation beside each band says about the step above it: 'percent' the share lost, 'count' the absolute number lost, 'both' stacks the two, 'none' hides the annotation and re-centres the funnel in the frame.",
          "required": false,
          "defaultValue": "percent",
          "options": [
            "percent",
            "count",
            "both",
            "none"
          ]
        },
        {
          "name": "minWidth",
          "type": "number",
          "description": "Width the smallest band keeps as a fraction of the widest, so the last stage stays legible when its count is a rounding error of the first. It flatters small stages: 0 makes the widths strictly proportional and honest, 0.4 keeps a real funnel silhouette.",
          "required": false,
          "defaultValue": "0.4",
          "options": null
        },
        {
          "name": "valuePrefix",
          "type": "string",
          "description": "Set immediately before every figure, on the bands and the drop-off alike — a currency mark such as '$' or '€'.",
          "required": false,
          "defaultValue": null,
          "options": null
        },
        {
          "name": "valueSuffix",
          "type": "string",
          "description": "Set immediately after every figure — 'MRR', 'sessions', 'k'. Rendered exactly as typed, so include a leading thin space yourself if the unit needs air.",
          "required": false,
          "defaultValue": null,
          "options": null
        },
        {
          "name": "compact",
          "type": "boolean",
          "description": "Abbreviate thousands and millions ('1.2k', '3.4M') instead of grouping digits with thin spaces. Grouped digits are more precise and read better in a band; switch this on once the figures pass six digits.",
          "required": false,
          "defaultValue": "false",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Quiet mono caption under a hairline at the foot of the frame: the source, the window, the qualifier. One short sentence.",
          "required": false,
          "defaultValue": "Self-serve signups only · activation means one workspace …",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "glass-stack-reveal",
      "name": "Glass Stack Reveal",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "Frosted glass panes rise into a stack over a drifting light field, each with its own mass so the deck settles front-last",
      "tags": [
        "glass",
        "frosted",
        "vertical",
        "stack",
        "follow-through",
        "light field",
        "premium"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/glass-stack-reveal.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const glassStackRevealSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"Line above the stack. Two to five words.\"),\n  panels: z\n    .array(z.object({ label: z.string(), value: z.string() }))\n    .min(2)\n    .max(4)\n    .describe(\"The panels, back to front. The LAST one is the hero and is drawn larger, so put the payoff there. Two to four; more than four and the stack stops reading.\"),\n  frost: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.6)\n    .describe(\"How frosted the glass is. 0 is clear glass over the light field; 1 is heavy frost that hides it.\"),\n  brand: z\n    .string()\n    .default(\"signal\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof glassStackRevealSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.88\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * A pane of frosted glass.\n *\n * Four things have to be true at once or it reads as a translucent rectangle:\n * the fill is barely tinted, the blur samples what is BEHIND it, the top edge\n * carries a bright one-pixel specular where light catches the chamfer, and it\n * casts a real shadow onto whatever it covers. The translucent fill is also the\n * fallback — if a renderer drops `backdrop-filter`, the pane still reads.\n */\nconst Pane: React.FC<{\n  w: number;\n  h: number;\n  frost: number;\n  t: typeof DEFAULT_BRAND;\n  children?: React.ReactNode;\n  lift: number;\n  /** Position, 0–1, of the highlight travelling across this pane. */\n  gleam: number;\n}> = ({ w, h, frost, t, children, lift, gleam }) => (\n  <div\n    style={{\n      width: w,\n      height: h,\n      borderRadius: t.radius * 2.4,\n      backgroundColor: alpha(t.surface, 0.3 + frost * 0.34),\n      backdropFilter: `blur(${8 + frost * 30}px) saturate(140%)`,\n      WebkitBackdropFilter: `blur(${8 + frost * 30}px) saturate(140%)`,\n      border: `1px solid ${alpha(\"#fff\", 0.11)}`,\n      boxShadow: `inset 0 1px 0 0 ${alpha(\"#fff\", 0.2)}, inset 0 0 ${h * 0.4}px ${alpha(\n        \"#fff\",\n        0.03,\n      )}, 0 ${h * 0.1 * lift}px ${h * 0.3 * lift}px ${alpha(\"#000\", 0.42)}`,\n      overflow: \"hidden\",\n      position: \"relative\",\n    }}\n  >\n    {/* the chamfer highlight, brightest at the centre of the top edge */}\n    <div\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: \"8%\",\n        right: \"8%\",\n        height: 1,\n        background: `linear-gradient(90deg, ${alpha(\"#fff\", 0)} 0%, ${alpha(\"#fff\", 0.45)} 42%, ${alpha(\n          \"#fff\",\n          0.28,\n        )} 70%, ${alpha(\"#fff\", 0)} 100%)`,\n      }}\n    />\n    {/* a raking sheen across the pane */}\n    <div\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        background: `linear-gradient(128deg, ${alpha(\"#fff\", 0.085)} 0%, ${alpha(\"#fff\", 0.02)} 30%, ${alpha(\n          \"#fff\",\n          0,\n        )} 54%)`,\n      }}\n    />\n    {/* A highlight travelling across the pane, continuously and out of phase\n        with its neighbours. Glass in front of a moving light never holds still,\n        and this is what keeps the stack alive through the hold instead of\n        becoming a screenshot the moment it settles. */}\n    <div\n      aria-hidden\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        pointerEvents: \"none\",\n        mixBlendMode: \"overlay\",\n        background: `linear-gradient(104deg, ${alpha(\"#fff\", 0)} ${gleam * 100 - 30}%, ${alpha(\n          \"#fff\",\n          0.18,\n        )} ${gleam * 100 - 8}%, ${alpha(\"#fff\", 0.4)} ${gleam * 100}%, ${alpha(\"#fff\", 0.18)} ${\n          gleam * 100 + 8\n        }%, ${alpha(\"#fff\", 0)} ${gleam * 100 + 30}%)`,\n      }}\n    />\n    {children}\n  </div>\n);\n\nconst GlassStackReveal: React.FC<Props> = ({ headline, panels, frost, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const paneW = width * 0.74;\n  const paneH = paneW * 0.42;\n  const heroH = paneW * 0.6;\n  const step = paneH * 0.34;\n\n  const headIn = interpolate(frame, [0, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const push = interpolate(stage.progress, [0, 1], [1.02, 1]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* The light field. It exists for the glass to blur — frosted glass over a\n          flat colour is indistinguishable from a grey box. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: `-${height * 0.2}px`,\n          filter: `blur(${width * 0.07}px)`,\n          mixBlendMode: \"screen\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * (0.24 + Math.sin(time * 0.31) * 0.06),\n            top: height * (0.3 + Math.cos(time * 0.23) * 0.05),\n            width: width * 0.9,\n            height: width * 0.9,\n            marginLeft: -width * 0.45,\n            marginTop: -width * 0.45,\n            background: `radial-gradient(circle, ${alpha(t.accent, 0.9)} 0%, ${alpha(\n              t.accent,\n              0.5,\n            )} 22%, ${alpha(t.accent, 0.22)} 42%, ${alpha(t.accent, 0.06)} 62%, ${alpha(\n              t.accent,\n              0,\n            )} 84%)`,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * (0.82 + Math.cos(time * 0.27) * 0.05),\n            top: height * (0.72 + Math.sin(time * 0.19) * 0.04),\n            width: width * 0.78,\n            height: width * 0.78,\n            marginLeft: -width * 0.39,\n            marginTop: -width * 0.39,\n            background: `radial-gradient(circle, ${alpha(t.accentAlt, 0.8)} 0%, ${alpha(\n              t.accentAlt,\n              0.42,\n            )} 24%, ${alpha(t.accentAlt, 0.16)} 46%, ${alpha(t.accentAlt, 0)} 78%)`,\n          }}\n        />\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          transform: `scale(${push})`,\n          opacity: stage.presence,\n        }}\n      >\n        <div\n          style={{\n            width: paneW,\n            marginBottom: height * 0.05,\n            fontFamily: t.fontDisplay,\n            fontSize: width * 0.088,\n            fontWeight: 700,\n            lineHeight: 1.06,\n            letterSpacing: width * -0.0032,\n            color: t.ink,\n            opacity: headIn,\n            transform: `translateY(${interpolate(headIn, [0, 1], [width * 0.03, 0])}px)`,\n            filter: `blur(${interpolate(headIn, [0, 1], [width * 0.005, 0])}px)`,\n          }}\n        >\n          {headline}\n        </div>\n\n        <div style={{ position: \"relative\", width: paneW, height: heroH + step * (panels.length - 1) }}>\n          {panels.map((panel, i) => {\n            const isHero = i === panels.length - 1;\n            const depth = panels.length - 1 - i;\n\n            /**\n             * Follow-through by mass.\n             *\n             * Each pane gets a slightly longer travel than the one behind it, so\n             * they do not arrive as a block. Panes further back are also *lighter*\n             * — shorter travel, less overshoot — which is why the stack settles\n             * front-last and reads as one physical gesture rather than four.\n             */\n            const delay = 6 + i * 4;\n            const dur = 26 + i * 5;\n            const e = interpolate(frame, [delay, delay + dur], [0, 1], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n              easing: EASING.enter,\n            });\n\n            const y = interpolate(e, [0, 1], [height * 0.22 + depth * step * 0.4, 0]);\n            const scale = interpolate(e, [0, 1], [0.9, 1]);\n            const rot = interpolate(e, [0, 1], [4 - depth * 1.2, 0]);\n\n            return (\n              <div\n                key={`${panel.label}-${i}`}\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  bottom: depth * step,\n                  display: \"flex\",\n                  justifyContent: \"center\",\n                  transform: `translateY(${y}px) scale(${scale}) rotate(${rot}deg)`,\n                  transformOrigin: \"50% 100%\",\n                  opacity: e,\n                  zIndex: i,\n                }}\n              >\n                <Pane\n                  w={paneW - depth * paneW * 0.075}\n                  h={isHero ? heroH : paneH}\n                  frost={frost}\n                  t={t}\n                  lift={e}\n                  gleam={((time * 0.13 + i * 0.29) % 1.7) - 0.35}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      inset: 0,\n                      padding: paneW * 0.07,\n                      display: \"flex\",\n                      flexDirection: \"column\",\n                      justifyContent: \"space-between\",\n                    }}\n                  >\n                    <div\n                      style={{\n                        fontFamily: t.fontMono,\n                        fontSize: paneW * 0.032,\n                        fontWeight: 500,\n                        letterSpacing: paneW * 0.005,\n                        textTransform: \"uppercase\",\n                        color: isHero ? t.accent : alpha(t.ink, 0.5),\n                      }}\n                    >\n                      {panel.label}\n                    </div>\n                    <div\n                      style={{\n                        display: \"flex\",\n                        alignItems: \"flex-end\",\n                        justifyContent: \"space-between\",\n                        gap: paneW * 0.05,\n                      }}\n                    >\n                      <div\n                        style={{\n                          fontFamily: t.fontDisplay,\n                          fontSize: isHero ? paneW * 0.175 : paneW * 0.1,\n                          fontWeight: 700,\n                          lineHeight: 0.94,\n                          letterSpacing: paneW * -0.005,\n                          color: t.ink,\n                        }}\n                      >\n                        {panel.value}\n                      </div>\n                      {isHero ? (\n                        <div style={{ display: \"flex\", alignItems: \"flex-end\", gap: paneW * 0.012 }}>\n                          {[0.35, 0.55, 0.44, 0.72, 0.58, 0.9, 1].map((v, k) => (\n                            <div\n                              key={k}\n                              style={{\n                                width: paneW * 0.022,\n                                height:\n                                  paneW *\n                                  0.13 *\n                                  v *\n                                  interpolate(e, [0.3 + k * 0.07, 0.85 + k * 0.07], [0, 1], {\n                                    extrapolateLeft: \"clamp\",\n                                    extrapolateRight: \"clamp\",\n                                  }),\n                                borderRadius: paneW * 0.008,\n                                background: `linear-gradient(180deg, ${alpha(t.accent, 0.95)} 0%, ${alpha(\n                                  t.accent,\n                                  0.35,\n                                )} 100%)`,\n                              }}\n                            />\n                          ))}\n                        </div>\n                      ) : null}\n                    </div>\n                  </div>\n                </Pane>\n              </div>\n            );\n          })}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(115% 80% at 50% 42%, ${alpha(\"#000\", 0)} 30%, ${alpha(\n            \"#000\",\n            0.16,\n          )} 60%, ${alpha(\"#000\", 0.46)} 90%, ${alpha(\"#000\", 0.64)} 100%)`,\n        }}\n      />\n      <Grain id=\"glass-stack-grain\" frame={frame} opacity={0.07} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"glass-stack-reveal\",\n  name: \"Glass Stack Reveal\",\n  category: \"product\",\n  summary:\n    \"Frosted glass panes rise into a stack over a drifting light field, each with its own mass so the deck settles front-last\",\n  tags: [\"glass\", \"frosted\", \"vertical\", \"stack\", \"follow-through\", \"light field\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: glassStackRevealSchema,\n  defaults: {\n    headline: \"Everything, in one pass\",\n    panels: [\n      { label: \"Rendered\", value: \"76\" },\n      { label: \"Categories\", value: \"11\" },\n      { label: \"Typed props\", value: \"533\" },\n    ],\n    frost: 0.6,\n    brand: \"signal\",\n  },\n  component: GlassStackReveal,\n  file: \"src/registry/items/glass-stack-reveal.tsx\",\n  origin: {\n    prompt:\n      \"A vertical frame where frosted glass panels rise up and settle into a stack, with the front one larger as the hero. The glass has to be real: a barely tinted fill, a blur that samples the moving coloured light behind it, a bright one-pixel specular along the top edge and a shadow cast onto the pane behind. Give each panel a slightly different weight so the stack does not arrive as one block.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical frame where frosted glass panels rise up and settle into a stack, with the front one larger as the hero. The glass has to be real: a barely tinted fill, a blur that samples the moving coloured light behind it, a bright one-pixel specular along the top edge and a shadow cast onto the pane behind. Give each panel a slightly different weight so the stack does not arrive as one block.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7136,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "Line above the stack. Two to five words.",
          "required": true,
          "defaultValue": "Everything, in one pass",
          "options": null
        },
        {
          "name": "panels",
          "type": "array",
          "description": "The panels, back to front. The LAST one is the hero and is drawn larger, so put the payoff there. Two to four; more than four and the stack stops reading.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "frost",
          "type": "number",
          "description": "How frosted the glass is. 0 is clear glass over the light field; 1 is heavy frost that hides it.",
          "required": false,
          "defaultValue": "0.6",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "signal",
          "options": null
        }
      ]
    },
    {
      "id": "gradient-card-collage",
      "name": "Gradient Card Collage",
      "category": "social",
      "categoryLabel": "Social formats",
      "summary": "App cards float at true depth over a drifting saturated ground, parallaxing against a camera that never stops moving",
      "tags": [
        "social proof",
        "light",
        "collage",
        "parallax",
        "perspective",
        "gradient",
        "cards"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/gradient-card-collage.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const gradientCardCollageSchema = z.object({\n  message: z\n    .string()\n    .describe(\"The hero message in the front window. This is the thing the frame is bragging about.\"),\n  reply: z\n    .string()\n    .describe(\"The reply under it, in the received bubble.\"),\n  statA: z\n    .string()\n    .default(\"1.4K\")\n    .describe(\"First engagement figure on the post card.\"),\n  statB: z\n    .string()\n    .default(\"207K\")\n    .describe(\"Second engagement figure on the post card.\"),\n  reactions: z\n    .string()\n    .default(\"You and 1,902 others\")\n    .describe(\"Reaction line under the stacked avatars.\"),\n  windowTitle: z\n    .string()\n    .default(\"Mom\")\n    .describe(\"Title in the front window's chrome.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\"Brand token id — one of the keys of BRANDS. The ground is built from accent and accentAlt, so vivid brands read best.\"),\n});\n\ntype Props = z.infer<typeof gradientCardCollageSchema>;\n\nconst BUDGET = BUDGETS.scene;\nconst SEED = \"gradient-card-collage\";\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.86\" numOctaves={3} seed={Math.floor(frame / 2) % 64} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * One floating card.\n *\n * Every card is placed in real perspective — a z translation, not a scale — so\n * that the camera's drift produces true parallax between them. Cards nearer the\n * lens also carry a wider, softer shadow and a touch of defocus, which is what\n * separates a stack of divs from a pile of objects in a room.\n */\nconst Card: React.FC<{\n  i: number;\n  x: number;\n  y: number;\n  z: number;\n  w: number;\n  rot: number;\n  cam: number;\n  frame: number;\n  t: typeof DEFAULT_BRAND;\n  children: React.ReactNode;\n}> = ({ i, x, y, z, w, rot, cam, frame, t, children }) => {\n  const delay = 4 + i * 4;\n  const e = interpolate(frame, [delay, delay + 28], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // Each card breathes on its own phase, so the pile never looks pinned.\n  const drift = Math.sin(frame / 30 + i * 1.7) * (6 + z * 0.02);\n  const near = (z + 300) / 600;\n  const blur = Math.abs(z) > 210 ? Math.min(4, (Math.abs(z) - 210) * 0.02) : 0;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: y,\n        width: w,\n        opacity: e,\n        filter: blur > 0.2 ? `blur(${blur}px)` : \"none\",\n        transform: `\n          translate3d(${cam * z * 0.06}px, ${drift + interpolate(e, [0, 1], [70, 0])}px, ${z}px)\n          rotate(${rot}deg)\n          scale(${interpolate(e, [0, 1], [0.94, 1])})`,\n        boxShadow: `0 ${28 * near}px ${70 * near}px ${alpha(\"#000\", 0.2 * near)}, 0 ${\n          4 * near\n        }px ${12 * near}px ${alpha(\"#000\", 0.12 * near)}`,\n        borderRadius: t.radius * 1.6,\n        overflow: \"hidden\",\n        backgroundColor: t.surface,\n      }}\n    >\n      {children}\n    </div>\n  );\n};\n\nconst GradientCardCollage: React.FC<Props> = ({\n  message,\n  reply,\n  statA,\n  statB,\n  reactions,\n  windowTitle,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const cam = Math.sin(time * 0.26) * 0.9;\n  const push = interpolate(stage.progress, [0, 1], [1.04, 1]);\n  const fs = width * 0.0145;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* The ground. Three saturated lobes on their own drifts, blurred as a\n          group — the same construction as the dark light-fields, except here it\n          is the frame's colour rather than its light. */}\n      <div style={{ position: \"absolute\", inset: `-${height * 0.3}px`, filter: `blur(${width * 0.032}px)` }}>\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            /* Hard-ish stops. A long blend between two mid-saturation tokens\n               averages into mud across a whole frame; holding each hue over a\n               third of the ground and letting the lobes do the mixing is what\n               keeps it reading as colour rather than as sludge. */\n            background: `linear-gradient(152deg, ${t.accent} 0%, ${t.accent} 26%, ${alpha(\n              t.accentAlt,\n              0.96,\n            )} 54%, ${alpha(t.accentAlt, 0.9)} 72%, ${alpha(t.accent, 0.85)} 100%)`,\n          }}\n        />\n        {[\n          { c: t.accentAlt, x: 0.22, y: 0.28, s: 0.72, p: 0 },\n          { c: t.accent, x: 0.78, y: 0.2, s: 0.62, p: 2.2 },\n          { c: t.accentAlt, x: 0.6, y: 0.86, s: 0.8, p: 4.1 },\n        ].map((b, i) => (\n          <div\n            key={i}\n            style={{\n              position: \"absolute\",\n              left: `${(b.x + Math.sin(time * 0.21 + b.p) * 0.05) * 100}%`,\n              top: `${(b.y + Math.cos(time * 0.17 + b.p) * 0.05) * 100}%`,\n              width: width * b.s,\n              height: width * b.s,\n              marginLeft: -width * b.s * 0.5,\n              marginTop: -width * b.s * 0.5,\n              background: `radial-gradient(circle, ${alpha(b.c, 0.95)} 0%, ${alpha(b.c, 0.5)} 30%, ${alpha(\n                b.c,\n                0.15,\n              )} 56%, ${alpha(b.c, 0)} 78%)`,\n            }}\n          />\n        ))}\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          perspective: width * 1.4,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* back — the post card */}\n        <Card i={0} x={width * 0.07} y={height * 0.14} z={-260} w={width * 0.25} rot={-4.5} cam={cam} frame={frame} t={t}>\n          <div style={{ padding: fs * 1.4 }}>\n            <div style={{ height: fs * 4.4, borderRadius: t.radius, backgroundColor: alpha(t.accent, 0.85) }} />\n            <div style={{ height: fs * 3, marginTop: fs * 0.7, borderRadius: t.radius, backgroundColor: alpha(t.accentAlt, 0.9) }} />\n            <div style={{ height: fs * 2.2, marginTop: fs * 0.7, borderRadius: t.radius, backgroundColor: alpha(t.ink, 0.07) }} />\n            <div style={{ display: \"flex\", gap: fs * 1.6, marginTop: fs * 1.3, alignItems: \"center\" }}>\n              <Stat glyph=\"heart\" value={statA} fs={fs} t={t} />\n              <Stat glyph=\"bars\" value={statB} fs={fs} t={t} />\n            </div>\n          </div>\n        </Card>\n\n        {/* back right — the player */}\n        <Card i={1} x={width * 0.64} y={height * 0.1} z={-210} w={width * 0.27} rot={3.6} cam={cam} frame={frame} t={t}>\n          <div style={{ backgroundColor: t.ink, padding: fs * 1.5, minHeight: fs * 13 }}>\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: fs * 3.4,\n                fontWeight: 700,\n                letterSpacing: fs * -0.1,\n                color: t.surface,\n                lineHeight: 1.05,\n              }}\n            >\n              I am\n            </div>\n            <div style={{ display: \"flex\", alignItems: \"center\", gap: fs * 0.7, marginTop: fs * 3.4 }}>\n              <div\n                style={{\n                  width: 0,\n                  height: 0,\n                  borderTop: `${fs * 0.5}px solid transparent`,\n                  borderBottom: `${fs * 0.5}px solid transparent`,\n                  borderLeft: `${fs * 0.8}px solid ${alpha(t.surface, 0.9)}`,\n                }}\n              />\n              <div style={{ flex: 1, height: fs * 0.22, borderRadius: 999, backgroundColor: alpha(t.surface, 0.3) }}>\n                <div\n                  style={{\n                    width: `${interpolate(frame, [20, 130], [8, 62], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" })}%`,\n                    height: \"100%\",\n                    borderRadius: 999,\n                    backgroundColor: t.surface,\n                  }}\n                />\n              </div>\n            </div>\n          </div>\n        </Card>\n\n        {/* front — the window */}\n        <Card i={2} x={width * 0.28} y={height * 0.2} z={40} w={width * 0.42} rot={-1.2} cam={cam} frame={frame} t={t}>\n          <div style={{ backgroundColor: alpha(t.ink, 0.06), padding: `${fs * 0.9}px ${fs * 1.1}px`, display: \"flex\", alignItems: \"center\" }}>\n            <div style={{ display: \"flex\", gap: fs * 0.42 }}>\n              {[0.62, 0.42, 0.3].map((o, i) => (\n                <div key={i} style={{ width: fs * 0.72, height: fs * 0.72, borderRadius: \"50%\", backgroundColor: alpha(t.ink, o * 0.5) }} />\n              ))}\n            </div>\n            <div style={{ flex: 1, textAlign: \"center\", fontFamily: t.fontBody, fontSize: fs * 1.05, fontWeight: 600, color: alpha(t.ink, 0.7) }}>\n              {windowTitle}\n            </div>\n            <div style={{ width: fs * 3 }} />\n          </div>\n          <div style={{ backgroundColor: t.ink, padding: fs * 1.5, display: \"flex\", flexDirection: \"column\", gap: fs * 0.8, minHeight: fs * 15 }}>\n            <div style={{ flex: 1 }} />\n            <Bubble text={message} side=\"out\" fs={fs} fill={t.accent} colour={t.accentInk} font={t.fontBody} frame={frame} at={30} />\n            <Bubble text={reply} side=\"in\" fs={fs} fill={alpha(t.surface, 0.16)} colour={t.surface} font={t.fontBody} frame={frame} at={48} />\n          </div>\n        </Card>\n\n        {/* front right — the reaction bar */}\n        <Card i={3} x={width * 0.58} y={height * 0.66} z={110} w={width * 0.29} rot={2.2} cam={cam} frame={frame} t={t}>\n          <div style={{ padding: `${fs * 1.1}px ${fs * 1.3}px`, display: \"flex\", alignItems: \"center\", gap: fs * 0.9 }}>\n            <div style={{ display: \"flex\" }}>\n              {[t.accent, t.accentAlt, t.ink].map((c, i) => (\n                <div\n                  key={i}\n                  style={{\n                    width: fs * 1.7,\n                    height: fs * 1.7,\n                    borderRadius: \"50%\",\n                    backgroundColor: c,\n                    border: `${fs * 0.14}px solid ${t.surface}`,\n                    marginLeft: i === 0 ? 0 : -fs * 0.55,\n                    transform: `scale(${interpolate(frame, [40 + i * 4, 56 + i * 4], [0, 1], {\n                      extrapolateLeft: \"clamp\",\n                      extrapolateRight: \"clamp\",\n                      easing: EASING.accent,\n                    })})`,\n                  }}\n                />\n              ))}\n            </div>\n            <span style={{ fontFamily: t.fontBody, fontSize: fs * 1.05, color: alpha(t.ink, 0.68) }}>{reactions}</span>\n          </div>\n        </Card>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(120% 88% at 50% 46%, ${alpha(\"#000\", 0)} 34%, ${alpha(\"#000\", 0.1)} 66%, ${alpha(\n            \"#000\",\n            0.3,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"gradient-collage-grain\" frame={frame} opacity={0.075} />\n    </AbsoluteFill>\n  );\n};\n\nconst Stat: React.FC<{ glyph: \"heart\" | \"bars\"; value: string; fs: number; t: typeof DEFAULT_BRAND }> = ({\n  glyph,\n  value,\n  fs,\n  t,\n}) => (\n  <div style={{ display: \"flex\", alignItems: \"center\", gap: fs * 0.45 }}>\n    {glyph === \"heart\" ? (\n      <svg width={fs * 1.2} height={fs * 1.2} viewBox=\"0 0 24 24\" aria-hidden>\n        <path\n          d=\"M12 21s-8-5.2-8-11a4.6 4.6 0 018-3 4.6 4.6 0 018 3c0 5.8-8 11-8 11z\"\n          fill={t.accent}\n        />\n      </svg>\n    ) : (\n      <svg width={fs * 1.2} height={fs * 1.2} viewBox=\"0 0 24 24\" aria-hidden>\n        {[0, 1, 2].map((i) => (\n          <rect key={i} x={4 + i * 6} y={16 - i * 5} width=\"4\" height={5 + i * 5} rx=\"1\" fill={alpha(t.ink, 0.6)} />\n        ))}\n      </svg>\n    )}\n    <span style={{ fontFamily: t.fontBody, fontSize: fs * 1.1, fontWeight: 600, color: alpha(t.ink, 0.72) }}>{value}</span>\n  </div>\n);\n\nconst Bubble: React.FC<{\n  text: string;\n  side: \"in\" | \"out\";\n  fs: number;\n  fill: string;\n  colour: string;\n  font: string;\n  frame: number;\n  at: number;\n}> = ({ text, side, fs, fill, colour, font, frame, at }) => {\n  const e = interpolate(frame, [at, at + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n  return (\n    <div\n      style={{\n        alignSelf: side === \"out\" ? \"flex-end\" : \"flex-start\",\n        maxWidth: \"82%\",\n        padding: `${fs * 0.65}px ${fs * 0.95}px`,\n        borderRadius: fs * 1.3,\n        backgroundColor: fill,\n        fontFamily: font,\n        fontSize: fs * 1.15,\n        lineHeight: 1.34,\n        color: colour,\n        opacity: Math.min(1, e * 2),\n        transform: `scale(${interpolate(e, [0, 1], [0.84, 1])})`,\n        transformOrigin: side === \"out\" ? \"100% 100%\" : \"0% 100%\",\n      }}\n    >\n      {text}\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"gradient-card-collage\",\n  name: \"Gradient Card Collage\",\n  category: \"social\",\n  summary:\n    \"App cards float at true depth over a drifting saturated ground, parallaxing against a camera that never stops moving\",\n  tags: [\"social proof\", \"light\", \"collage\", \"parallax\", \"perspective\", \"gradient\", \"cards\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: gradientCardCollageSchema,\n  defaults: {\n    message: \"Mom I hit 500k views across both the platforms!!\",\n    reply: \"I'm so proud of you!\",\n    statA: \"1.4K\",\n    statB: \"207K\",\n    reactions: \"You and 1,902 others\",\n    windowTitle: \"Mom\",\n    brand: \"paper\",\n  },\n  component: GradientCardCollage,\n  file: \"src/registry/items/gradient-card-collage.tsx\",\n  origin: {\n    prompt:\n      \"An opening frame where app windows and social cards float in front of a big saturated colour field. Place the cards at real depths with a z translation rather than by scaling them, so a slow camera drift makes them parallax against each other. Nearer cards get wider softer shadows, far ones a touch of defocus. The colour ground should drift on its own, and everything sits under grain and a vignette.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An opening frame where app windows and social cards float in front of a big saturated colour field. Place the cards at real depths with a z translation rather than by scaling them, so a slow camera drift makes them parallax against each other. Nearer cards get wider softer shadows, far ones a touch of defocus. The colour ground should drift on its own, and everything sits under grain and a vignette.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.872,
      "props": [
        {
          "name": "message",
          "type": "string",
          "description": "The hero message in the front window. This is the thing the frame is bragging about.",
          "required": true,
          "defaultValue": "Mom I hit 500k views across both the platforms!!",
          "options": null
        },
        {
          "name": "reply",
          "type": "string",
          "description": "The reply under it, in the received bubble.",
          "required": true,
          "defaultValue": "I'm so proud of you!",
          "options": null
        },
        {
          "name": "statA",
          "type": "string",
          "description": "First engagement figure on the post card.",
          "required": false,
          "defaultValue": "1.4K",
          "options": null
        },
        {
          "name": "statB",
          "type": "string",
          "description": "Second engagement figure on the post card.",
          "required": false,
          "defaultValue": "207K",
          "options": null
        },
        {
          "name": "reactions",
          "type": "string",
          "description": "Reaction line under the stacked avatars.",
          "required": false,
          "defaultValue": "You and 1,902 others",
          "options": null
        },
        {
          "name": "windowTitle",
          "type": "string",
          "description": "Title in the front window's chrome.",
          "required": false,
          "defaultValue": "Mom",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. The ground is built from accent and accentAlt, so vivid brands read best.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "gradient-mesh-drift",
      "name": "Gradient Mesh Drift",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "Blurred brand-coloured gradient blobs drifting on closed sine paths, loop-exact at the last frame",
      "tags": [
        "background",
        "gradient mesh",
        "seamless loop",
        "ambient",
        "blur",
        "underlay"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/gradient-mesh-drift.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\n/**\n * Gradient Mesh Drift — an ambient bed, not a subject.\n *\n * Five heavily blurred radial gradients drift behind everything on closed\n * sine/cosine paths. Two properties do all the work:\n *\n *  1. **The loop is exact.** Every blob's position and radius is a sine of\n *     `k * progress` where `progress` is `frame / total` (straight from the\n *     stage) and `k` is an *integer*. At the last frame the argument has\n *     completed a whole number of turns, so frame `total` is pixel-identical to\n *     frame 0 and the field can be looped or repeated with no jump. This is why\n *     `drift` is an enum rather than a number: speed has to stay an integer\n *     multiplier of those harmonics or the loop stops closing.\n *\n *  2. **It stays readable.** Alpha is low, blur is huge, and the whole field is\n *     composited as one group so overlapping blobs never stack into a bright\n *     hotspot. The default `scrim` lifts the bottom edge where type usually\n *     lands. A headline in `ink` sits on the default settings at well over\n *     10:1 contrast — the sample `headline` prop is there to prove it.\n */\n\nconst TAU = Math.PI * 2;\n\n/* ------------------------------------------------------------------ colour */\n\n/** `#abc` / `#aabbcc` → `[r, g, b]`. Tokens are always hex; anything else reads as black. */\nconst parseHex = (hex: string): [number, number, number] => {\n  const raw = hex.replace(\"#\", \"\").trim();\n  const full = raw.length === 3 ? `${raw[0]}${raw[0]}${raw[1]}${raw[1]}${raw[2]}${raw[2]}` : raw.slice(0, 6);\n  const n = Number.parseInt(full, 16);\n  if (!Number.isFinite(n)) return [0, 0, 0];\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\n\n/** A token colour at an explicit alpha. Keeps the component token-only — no literal colour. */\nconst withAlpha = (hex: string, alpha: number): string => {\n  const [r, g, b] = parseHex(hex);\n  return `rgba(${r}, ${g}, ${b}, ${Number(Math.max(0, Math.min(1, alpha)).toFixed(4))})`;\n};\n\n/**\n * Whether the brand's canvas is dark. This picks the blend mode: `screen` adds\n * light on a dark canvas, `multiply` tints a light one like an ink overlay.\n * Getting this wrong is the difference between a mesh and a grey smear.\n */\nconst isDarkCanvas = (hex: string): boolean => {\n  const [r, g, b] = parseHex(hex);\n  return (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255 < 0.45;\n};\n\n/* -------------------------------------------------------------------- field */\n\ntype BlobRole = \"accent\" | \"accentAlt\" | \"ink\";\n\ntype BlobSpec = {\n  role: BlobRole;\n  /** Centre of the drift path, as a fraction of frame width / height. */\n  cx: number;\n  cy: number;\n  /** Drift amplitude, as a fraction of frame width / height. */\n  ax: number;\n  ay: number;\n  /** Integer harmonics — one full turn per `k` cycles. Integers are why the loop closes. */\n  kx: number;\n  ky: number;\n  /** Phase offsets, in turns, so no two blobs peak together. */\n  px: number;\n  py: number;\n  /** Radius, as a fraction of the mesh unit ((width + height) / 2). */\n  r: number;\n  /** Radius breathing depth and its own integer harmonic. */\n  rb: number;\n  kr: number;\n  /** Relative opacity weight. The anchor is 1; everything else defers to it. */\n  w: number;\n};\n\n/**\n * Ordered so that a lower `blobs` count is still a composed frame: the first\n * three carry the diagonal, the rest are detail.\n */\nconst FIELD: BlobSpec[] = [\n  // The anchor. Accent mass upper-left, one slow turn per loop — the calmest thing in frame.\n  { role: \"accent\", cx: 0.26, cy: 0.34, ax: 0.075, ay: 0.055, kx: 1, ky: 1, px: 0.0, py: 0.12, r: 0.4, rb: 0.1, kr: 1, w: 1.0 },\n  // The counterweight, lower-right and out of phase, so the frame breathes along the diagonal.\n  { role: \"accentAlt\", cx: 0.76, cy: 0.68, ax: 0.085, ay: 0.06, kx: 1, ky: 2, px: 0.42, py: 0.58, r: 0.36, rb: 0.12, kr: 2, w: 0.92 },\n  // The haze: desaturated ink across the top. Lifts the sky off flat without adding a third hue.\n  { role: \"ink\", cx: 0.54, cy: 0.1, ax: 0.06, ay: 0.07, kx: 2, ky: 1, px: 0.7, py: 0.3, r: 0.42, rb: 0.08, kr: 1, w: 0.78 },\n  // Warmth in the bottom-left corner, where type lands — deliberately the dimmest of the accents.\n  { role: \"accentAlt\", cx: 0.1, cy: 0.88, ax: 0.09, ay: 0.065, kx: 2, ky: 3, px: 0.55, py: 0.8, r: 0.26, rb: 0.16, kr: 2, w: 0.62 },\n  // The glint: smallest and fastest, three turns per loop, top-right where nothing is read.\n  { role: \"accent\", cx: 0.92, cy: 0.14, ax: 0.07, ay: 0.085, kx: 3, ky: 2, px: 0.18, py: 0.05, r: 0.22, rb: 0.18, kr: 3, w: 0.7 },\n  // Sixth, only at blobs=6: ink low centre, to close the composition underneath.\n  { role: \"ink\", cx: 0.4, cy: 0.96, ax: 0.065, ay: 0.045, kx: 1, ky: 3, px: 0.86, py: 0.44, r: 0.3, rb: 0.1, kr: 2, w: 0.5 },\n];\n\n/** Integer speed multipliers. Non-integers would break the closed loop, so they aren't offered. */\nconst DRIFT_RATE = { still: 0, slow: 1, steady: 2, brisk: 3 } as const;\n\nconst roleColor = (role: BlobRole, t: BrandTokens): string =>\n  role === \"accent\" ? t.accent : role === \"accentAlt\" ? t.accentAlt : t.inkMuted;\n\n/* ------------------------------------------------------------------ schema */\n\nexport const gradientMeshDriftSchema = z.object({\n  blobs: z\n    .number()\n    .int()\n    .min(3)\n    .max(6)\n    .default(5)\n    .describe(\n      \"How many gradient circles make up the mesh. 3 is an open, airy field; 5 is the balanced default; 6 closes the bottom of the frame and reads denser. They are added in a fixed order, so every count is a composed frame rather than a random handful.\",\n    ),\n  intensity: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.62)\n    .describe(\n      \"How strongly the colour reads, 0 to 1. Around 0.6 is the sweet spot: clearly branded, still quiet enough for a headline to sit on. Past 0.85 the field starts competing with type — use that only when nothing is layered over it.\",\n    ),\n  drift: z\n    .enum([\"still\", \"slow\", \"steady\", \"brisk\"])\n    .default(\"slow\")\n    .describe(\n      \"Drift speed. Quantised on purpose: each step is an integer multiple of the blobs' harmonics, which is what keeps the loop exact at the last frame. 'still' freezes the mesh into a static gradient wallpaper; 'slow' is barely perceptible and right behind dialogue or type; 'brisk' is for a title card you only hold for a beat.\",\n    ),\n  phase: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.0)\n    .describe(\n      \"Where in the drift cycle the field starts, in turns (0 to 1). Nudge it to pick a different arrangement of the same mesh, or to de-sync two scenes that both use this background so the cut doesn't repeat itself.\",\n    ),\n  vignette: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.4)\n    .describe(\n      \"Corner falloff, 0 to 1. Kept subtle by design — it pulls the eye to the middle third and hides the blur seams at the frame edge. 0 for a perfectly even field; 1 is still gentle, roughly a 30% darkening in the corners.\",\n    ),\n  scrim: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.35)\n    .describe(\n      \"Strength of a gradient rising from the bottom edge, 0 to 1. This is the insurance policy for type placed low in the frame: it flattens whatever the mesh happens to be doing down there. Set 0 when exporting the bare background, or when your content sits in the upper half.\",\n    ),\n  grain: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Overlay a fixed, very fine noise texture. Large soft gradients band badly once they're encoded to 8-bit video; this dithers the ramps away. Leave it on unless you are compositing the frame further downstream.\",\n    ),\n  headline: z\n    .string()\n    .optional()\n    .describe(\n      \"Optional line of display type set over the mesh, lower-left — proof that the field stays readable, and a usable title card in its own right. Two to six words. Omit it to export the bare background as an underlay.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\n      \"Small uppercase mono label above the headline, in the accent colour. One or two words. Ignored when there is no headline.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof gradientMeshDriftSchema>;\n\n/* --------------------------------------------------------------- component */\n\nconst GradientMeshDrift: React.FC<Props> = ({\n  blobs,\n  intensity,\n  drift,\n  phase,\n  vignette,\n  scrim,\n  grain,\n  headline,\n  eyebrow,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n  const dark = isDarkCanvas(t.bg);\n\n  // One unit for every size in the mesh. Averaging the two sides means the\n  // blobs grow with a wide frame instead of being sized off the short edge.\n  const unit = (width + height) / 2;\n\n  // `stage.progress` is exactly frame / total, so integer harmonics of `cycle`\n  // return to their starting value on the final frame. That is the whole trick.\n  const cycle = stage.progress + phase;\n  const rate = DRIFT_RATE[drift];\n\n  // Light canvases take ink rather than light, and take much less of it — a\n  // multiply at full strength turns paper into mud.\n  const blend: React.CSSProperties[\"mixBlendMode\"] = dark ? \"screen\" : \"multiply\";\n  const alphaScale = dark ? 1 : 0.52;\n\n  // The mesh is drawn into a box larger than the frame so that each blob's blur\n  // falls off outside the visible area, never as a soft edge inside it.\n  const bleedX = width * 0.16;\n  const bleedY = height * 0.16;\n\n  const specs = FIELD.slice(0, Math.max(3, Math.min(FIELD.length, Math.round(blobs))));\n\n  const typeUnit = Math.min(width, height);\n  const headlineSize = typeUnit * 0.085;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/*\n        The whole field is one composited group: its opacity is the stage's\n        presence, and it carries its own copy of the canvas colour so the blend\n        modes have something to act on. Fading the group therefore fades only\n        the colour — the canvas underneath it is the identical token.\n      */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: -bleedX,\n          top: -bleedY,\n          width: width + bleedX * 2,\n          height: height + bleedY * 2,\n          isolation: \"isolate\",\n          opacity: stage.presence,\n        }}\n      >\n        <div style={{ position: \"absolute\", inset: 0, backgroundColor: t.bg }} />\n        <div style={{ position: \"absolute\", left: bleedX, top: bleedY, width, height }}>\n          {specs.map((spec, i) => (\n            <Blob\n              key={`blob-${i}`}\n              spec={spec}\n              color={roleColor(spec.role, t)}\n              cycle={cycle}\n              rate={rate}\n              width={width}\n              height={height}\n              unit={unit}\n              alpha={(0.13 + intensity * 0.34) * spec.w * alphaScale}\n              blend={blend}\n            />\n          ))}\n        </div>\n      </div>\n\n      {grain ? <Grain opacity={(dark ? 0.055 : 0.042) * stage.presence} /> : null}\n\n      {vignette > 0 ? (\n        <AbsoluteFill\n          style={{\n            background: `radial-gradient(118% 88% at 50% 42%, rgba(0,0,0,0) 40%, rgba(0,0,0,${Number(\n              (vignette * 0.14 * stage.presence).toFixed(4),\n            )}) 76%, rgba(0,0,0,${Number((vignette * 0.3 * stage.presence).toFixed(4))}) 100%)`,\n          }}\n        />\n      ) : null}\n\n      {scrim > 0 ? (\n        <AbsoluteFill\n          style={{\n            background: `linear-gradient(to top, ${lift(dark, scrim * 0.5 * stage.presence)} 0%, ${lift(\n              dark,\n              scrim * 0.19 * stage.presence,\n            )} 26%, ${lift(dark, 0)} 58%)`,\n          }}\n        />\n      ) : null}\n\n      {headline ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * 0.075,\n            bottom: height * 0.11,\n            width: Math.min(width * 0.62, headlineSize * 11),\n          }}\n        >\n          {eyebrow ? (\n            <Overlay index={0} rise={headlineSize * 0.5}>\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: headlineSize * 0.19,\n                  fontWeight: 600,\n                  letterSpacing: headlineSize * 0.038,\n                  textTransform: \"uppercase\",\n                  color: t.accent,\n                  marginBottom: headlineSize * 0.42,\n                }}\n              >\n                {eyebrow}\n              </div>\n            </Overlay>\n          ) : null}\n\n          <Overlay index={eyebrow ? 1 : 0} rise={headlineSize * 0.62}>\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: headlineSize,\n                fontWeight: 700,\n                lineHeight: 1.04,\n                letterSpacing: headlineSize * -0.022,\n                color: t.ink,\n              }}\n            >\n              {headline}\n            </div>\n          </Overlay>\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n};\n\n/**\n * A scrim that reads as \"more canvas\" on either polarity: black on a dark\n * brand, white on a light one. The two pure literals the design genuinely needs.\n */\nconst lift = (dark: boolean, alpha: number): string =>\n  dark\n    ? `rgba(0,0,0,${Number(Math.max(0, alpha).toFixed(4))})`\n    : `rgba(255,255,255,${Number(Math.max(0, alpha).toFixed(4))})`;\n\n/**\n * One drifting circle.\n *\n * The gradient itself is soft-edged and reaches full transparency at 72% of the\n * box, well inside the element, so the blur on top has nothing hard to smear.\n * Blur scales with the blob, not with the frame: a small glint stays legible as\n * a glint while the anchor dissolves into weather.\n */\nconst Blob: React.FC<{\n  spec: BlobSpec;\n  color: string;\n  cycle: number;\n  rate: number;\n  width: number;\n  height: number;\n  unit: number;\n  alpha: number;\n  blend: React.CSSProperties[\"mixBlendMode\"];\n}> = ({ spec, color, cycle, rate, width, height, unit, alpha, blend }) => {\n  const x = width * (spec.cx + spec.ax * Math.sin(TAU * (spec.kx * rate * cycle + spec.px)));\n  const y = height * (spec.cy + spec.ay * Math.cos(TAU * (spec.ky * rate * cycle + spec.py)));\n  const r = unit * spec.r * (1 + spec.rb * Math.sin(TAU * (spec.kr * rate * cycle + spec.px + 0.25)));\n  const blur = Math.max(unit * 0.02, r * 0.16);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x - r,\n        top: y - r,\n        width: r * 2,\n        height: r * 2,\n        background: `radial-gradient(circle at 50% 50%, ${withAlpha(color, alpha)} 0%, ${withAlpha(\n          color,\n          alpha * 0.58,\n        )} 42%, ${withAlpha(color, 0)} 72%)`,\n        filter: `blur(${blur.toFixed(1)}px)`,\n        mixBlendMode: blend,\n      }}\n    />\n  );\n};\n\n/**\n * Fixed fractal noise, rendered once as an SVG filter. `seed` is a constant, so\n * this is identical on every frame and every machine — a texture, not an\n * animation. Its only job is to dither the gradient ramps so they survive\n * 8-bit encoding without banding.\n */\nconst Grain: React.FC<{ opacity: number }> = ({ opacity }) => (\n  <AbsoluteFill style={{ opacity, mixBlendMode: \"overlay\" }}>\n    <svg width=\"100%\" height=\"100%\" aria-hidden>\n      <filter id=\"gradient-mesh-drift-grain\" x=\"0\" y=\"0\" width=\"100%\" height=\"100%\">\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.85\"\n          numOctaves={3}\n          seed={11}\n          stitchTiles=\"stitch\"\n          result=\"noise\"\n        />\n        <feColorMatrix in=\"noise\" type=\"saturate\" values=\"0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter=\"url(#gradient-mesh-drift-grain)\" />\n    </svg>\n  </AbsoluteFill>\n);\n\n/**\n * The overlay type's arrival: a short rise and fade on the shared stagger, so\n * the sample headline lands on the same beat as the field it sits on rather\n * than inventing a second timing.\n */\nconst Overlay: React.FC<{ index: number; rise: number; children: React.ReactNode }> = ({\n  index,\n  rise,\n  children,\n}) => {\n  const stage = useStaggeredStage(BUDGETS.feature, index, 4);\n  const y = interpolate(stage.enter, [0, 1], [rise, 0]);\n  return <div style={{ opacity: stage.presence, transform: `translateY(${y}px)` }}>{children}</div>;\n};\n\nexport default defineMotionItem({\n  id: \"gradient-mesh-drift\",\n  name: \"Gradient Mesh Drift\",\n  category: \"background\",\n  summary:\n    \"Blurred brand-coloured gradient blobs drifting on closed sine paths, loop-exact at the last frame\",\n  tags: [\"background\", \"gradient mesh\", \"seamless loop\", \"ambient\", \"blur\", \"underlay\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: gradientMeshDriftSchema,\n  defaults: {\n    blobs: 5,\n    intensity: 0.62,\n    drift: \"slow\",\n    phase: 0.18,\n    vignette: 0.4,\n    scrim: 0.35,\n    grain: true,\n    headline: \"Quiet enough to read on\",\n    eyebrow: \"Ambient bed\",\n    brand: \"slate\",\n  },\n  component: GradientMeshDrift,\n  file: \"src/registry/items/gradient-mesh-drift.tsx\",\n  origin: {\n    prompt:\n      \"A soft gradient mesh background — four or five big blurred blobs in the brand colours drifting slowly behind everything, seamlessly looping, dim enough that a headline can sit on top of it and still be readable. Give me a knob for how strong it is.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A soft gradient mesh background — four or five big blurred blobs in the brand colours drifting slowly behind everything, seamlessly looping, dim enough that a headline can sit on top of it and still be readable. Give me a knob for how strong it is.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6267,
      "props": [
        {
          "name": "blobs",
          "type": "number",
          "description": "How many gradient circles make up the mesh. 3 is an open, airy field; 5 is the balanced default; 6 closes the bottom of the frame and reads denser. They are added in a fixed order, so every count is a composed frame rather than a random handful.",
          "required": false,
          "defaultValue": "5",
          "options": null
        },
        {
          "name": "intensity",
          "type": "number",
          "description": "How strongly the colour reads, 0 to 1. Around 0.6 is the sweet spot: clearly branded, still quiet enough for a headline to sit on. Past 0.85 the field starts competing with type — use that only when nothing is layered over it.",
          "required": false,
          "defaultValue": "0.62",
          "options": null
        },
        {
          "name": "drift",
          "type": "\"still\" | \"slow\" | \"steady\" | \"brisk\"",
          "description": "Drift speed. Quantised on purpose: each step is an integer multiple of the blobs' harmonics, which is what keeps the loop exact at the last frame. 'still' freezes the mesh into a static gradient wallpaper; 'slow' is barely perceptible and right behind dialogue or type; 'brisk' is for a title card you only hold for a beat.",
          "required": false,
          "defaultValue": "slow",
          "options": [
            "still",
            "slow",
            "steady",
            "brisk"
          ]
        },
        {
          "name": "phase",
          "type": "number",
          "description": "Where in the drift cycle the field starts, in turns (0 to 1). Nudge it to pick a different arrangement of the same mesh, or to de-sync two scenes that both use this background so the cut doesn't repeat itself.",
          "required": false,
          "defaultValue": "0.18",
          "options": null
        },
        {
          "name": "vignette",
          "type": "number",
          "description": "Corner falloff, 0 to 1. Kept subtle by design — it pulls the eye to the middle third and hides the blur seams at the frame edge. 0 for a perfectly even field; 1 is still gentle, roughly a 30% darkening in the corners.",
          "required": false,
          "defaultValue": "0.4",
          "options": null
        },
        {
          "name": "scrim",
          "type": "number",
          "description": "Strength of a gradient rising from the bottom edge, 0 to 1. This is the insurance policy for type placed low in the frame: it flattens whatever the mesh happens to be doing down there. Set 0 when exporting the bare background, or when your content sits in the upper half.",
          "required": false,
          "defaultValue": "0.35",
          "options": null
        },
        {
          "name": "grain",
          "type": "boolean",
          "description": "Overlay a fixed, very fine noise texture. Large soft gradients band badly once they're encoded to 8-bit video; this dithers the ramps away. Leave it on unless you are compositing the frame further downstream.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "headline",
          "type": "string",
          "description": "Optional line of display type set over the mesh, lower-left — proof that the field stays readable, and a usable title card in its own right. Two to six words. Omit it to export the bare background as an underlay.",
          "required": false,
          "defaultValue": "Quiet enough to read on",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label above the headline, in the accent colour. One or two words. Ignored when there is no headline.",
          "required": false,
          "defaultValue": "Ambient bed",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "gridpaper-editorial",
      "name": "Gridpaper Editorial",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "A cream graph-paper spread: a taped card drops onto the sheet, a highlighter swipes under the masthead and mono specs type on",
      "tags": [
        "editorial",
        "light",
        "paper",
        "grid",
        "polaroid",
        "print",
        "masthead"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/gridpaper-editorial.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const gridpaperEditorialSchema = z.object({\n  titleTop: z\n    .string()\n    .describe(\"First title line. Gets the highlighter swipe behind it, so keep it short — one word is ideal.\"),\n  titleBottom: z\n    .string()\n    .describe(\"Second title line, set in the accent colour. The subject itself.\"),\n  specs: z\n    .array(z.object({ key: z.string(), value: z.string() }))\n    .min(1)\n    .max(5)\n    .describe(\"Monospace spec rows printed under the pinned card. Two to four reads best; this is a caption, not a table.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\"Small label above the title, printed in mono.\"),\n  tape: z\n    .boolean()\n    .default(true)\n    .describe(\"Stick a torn tape strip across the top edge of the pinned card.\"),\n  brand: z\n    .string()\n    .default(\"press\")\n    .describe(\"Brand token id — one of the keys of BRANDS. Light brands (press, paper, field) are what this layout is for.\"),\n});\n\ntype Props = z.infer<typeof gridpaperEditorialSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\n/**\n * Paper grain.\n *\n * The same turbulence used on the dark components, but read as fibre rather\n * than as film: on a cream stock the noise is what stops the ground looking\n * like `#F2F0EC` and starts it looking like paper. Multiply rather than overlay,\n * because paper texture darkens where the fibre sits proud.\n */\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"multiply\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.95\"\n        numOctaves={4}\n        seed={Math.floor(frame / 3) % 48}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * The graph rule.\n *\n * Two superimposed grids — a fine cell and a heavier fifth line — because that\n * is how engineering paper is actually printed, and a single uniform grid reads\n * as a CSS background rather than as a sheet.\n */\nconst GraphPaper: React.FC<{ ink: string; cell: number; reveal: number; rule: number }> = ({\n  ink,\n  cell,\n  reveal,\n  rule,\n}) => (\n  <div\n    style={{\n      position: \"absolute\",\n      inset: 0,\n      opacity: reveal,\n      /*\n       * Rule weight is derived from the frame, not fixed at 1px.\n       *\n       * A 1px line on a 1920-wide composition is a quarter of a pixel once the\n       * gallery renders its preview at 0.4 scale, and a quarter-pixel line\n       * averages into the paper and disappears. Deriving the weight means the\n       * grid survives every output size — which for a component whose entire\n       * identity is \"drawn on graph paper\" is not a detail.\n       */\n      backgroundImage: `\n        linear-gradient(${alpha(ink, 0.26)} ${rule}px, transparent ${rule}px),\n        linear-gradient(90deg, ${alpha(ink, 0.26)} ${rule}px, transparent ${rule}px),\n        linear-gradient(${alpha(ink, 0.13)} ${rule}px, transparent ${rule}px),\n        linear-gradient(90deg, ${alpha(ink, 0.13)} ${rule}px, transparent ${rule}px)`,\n      backgroundSize: `${cell * 5}px ${cell * 5}px, ${cell * 5}px ${cell * 5}px, ${cell}px ${cell}px, ${cell}px ${cell}px`,\n    }}\n  />\n);\n\nconst GridpaperEditorial: React.FC<Props> = ({ titleTop, titleBottom, specs, caption, tape, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const size = width * 0.072;\n  const cardW = width * 0.3;\n\n  /**\n   * The pinned card lands first and everything else is printed onto the sheet\n   * after it, so the shot reads as a page being made rather than as elements\n   * arriving. It drops from slightly above with a small rotation settling out —\n   * a physical object being placed, not a div fading in.\n   */\n  const drop = interpolate(frame, [4, 34], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const dropY = interpolate(drop, [0, 1], [-height * 0.14, 0]);\n  const dropRot = interpolate(drop, [0, 1], [-7.5, -2.2]);\n\n  const gridIn = interpolate(frame, [0, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  // The highlighter is a stroke, so it wipes on left to right rather than fading.\n  const swipe = interpolate(frame, [26, 44], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const topIn = interpolate(frame, [22, 40], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const bottomIn = interpolate(frame, [30, 50], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const capIn = interpolate(frame, [16, 32], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const tapeIn = interpolate(frame, [30, 40], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.035]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"48% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        <GraphPaper ink={t.ink} cell={width * 0.0125} reveal={gridIn * 0.95} rule={Math.max(1, width * 0.0013)} />\n\n        {/* A warm print light falling from the upper left. Even a flat sheet is\n            never one value across a whole frame. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: `radial-gradient(105% 80% at 20% 4%, ${alpha(\"#fff\", 0.24)} 0%, ${alpha(\n              \"#fff\",\n              0.07,\n            )} 34%, ${alpha(t.ink, 0.02)} 68%, ${alpha(t.ink, 0.08)} 100%)`,\n          }}\n        />\n\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: width * 0.055,\n            padding: `0 ${width * 0.085}px`,\n          }}\n        >\n          {/* ------------------------------------------------- the pinned card */}\n          <div\n            style={{\n              position: \"relative\",\n              flexShrink: 0,\n              width: cardW,\n              transform: `translateY(${dropY}px) rotate(${dropRot}deg)`,\n              opacity: drop,\n            }}\n          >\n            <div\n              style={{\n                backgroundColor: t.surface,\n                padding: cardW * 0.055,\n                paddingBottom: cardW * 0.075,\n                boxShadow: `0 ${cardW * 0.03}px ${cardW * 0.07}px ${alpha(t.ink, 0.16)}, 0 ${\n                  cardW * 0.005\n                }px ${cardW * 0.012}px ${alpha(t.ink, 0.1)}`,\n              }}\n            >\n              {/* The image well. Drawn, not loaded — the registry ships no assets,\n                  so a \"photograph\" here is a lit studio ground with a subject\n                  massed on it. It reads at this size and it always renders. */}\n              <div\n                style={{\n                  position: \"relative\",\n                  width: \"100%\",\n                  aspectRatio: \"1 / 1\",\n                  overflow: \"hidden\",\n                  border: `1px solid ${alpha(t.ink, 0.12)}`,\n                  background: `radial-gradient(80% 70% at 50% 30%, ${alpha(t.ink, 0.06)} 0%, ${alpha(\n                    t.ink,\n                    0.13,\n                  )} 55%, ${alpha(t.ink, 0.2)} 100%)`,\n                }}\n              >\n                <Subject ink={t.ink} accent={t.accent} progress={drop} />\n              </div>\n\n              <div style={{ marginTop: cardW * 0.055 }}>\n                {specs.map((s, i) => (\n                  <SpecRow\n                    key={`${s.key}-${i}`}\n                    index={i}\n                    label={`${s.key}: ${s.value}`}\n                    size={cardW * 0.035}\n                    colour={alpha(t.ink, 0.72)}\n                    font={t.fontMono}\n                  />\n                ))}\n              </div>\n            </div>\n\n            {tape ? (\n              <div\n                style={{\n                  position: \"absolute\",\n                  top: -cardW * 0.028,\n                  left: \"50%\",\n                  width: cardW * 0.3 * tapeIn,\n                  height: cardW * 0.055,\n                  marginLeft: -cardW * 0.09,\n                  transform: \"rotate(-3.5deg)\",\n                  backgroundColor: alpha(t.accentAlt, 0.62),\n                  boxShadow: `0 1px 2px ${alpha(t.ink, 0.14)}`,\n                }}\n              />\n            ) : null}\n          </div>\n\n          {/* ----------------------------------------------------- the masthead */}\n          <div style={{ minWidth: 0, flex: 1 }}>\n            {caption ? (\n              <div\n                style={{\n                  marginBottom: size * 0.34,\n                  fontFamily: t.fontMono,\n                  fontSize: size * 0.17,\n                  fontWeight: 500,\n                  letterSpacing: size * 0.055,\n                  textTransform: \"uppercase\",\n                  color: alpha(t.ink, 0.5),\n                  opacity: capIn,\n                  transform: `translateY(${interpolate(capIn, [0, 1], [size * 0.1, 0])}px)`,\n                }}\n              >\n                {caption}\n              </div>\n            ) : null}\n\n            <div style={{ position: \"relative\", display: \"inline-block\" }}>\n              {/* The highlighter sits UNDER the type and is drawn as a slightly\n                  rotated block that wipes across — a marker stroke, not a\n                  background colour. */}\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: -size * 0.06,\n                  right: -size * 0.06,\n                  top: size * 0.2,\n                  bottom: size * 0.08,\n                  transform: `rotate(-1.1deg) scaleX(${swipe})`,\n                  transformOrigin: \"left center\",\n                  backgroundColor: alpha(t.accentAlt, 0.42),\n                }}\n              />\n              <div\n                style={{\n                  position: \"relative\",\n                  fontFamily: t.fontDisplay,\n                  fontSize: size,\n                  fontWeight: 700,\n                  lineHeight: 0.98,\n                  letterSpacing: size * -0.018,\n                  textTransform: \"uppercase\",\n                  color: t.ink,\n                  opacity: topIn,\n                  transform: `translateY(${interpolate(topIn, [0, 1], [size * 0.14, 0])}px)`,\n                }}\n              >\n                {titleTop}\n              </div>\n            </div>\n\n            <div\n              style={{\n                marginTop: size * 0.08,\n                fontFamily: t.fontDisplay,\n                fontSize: size,\n                fontWeight: 700,\n                lineHeight: 0.98,\n                letterSpacing: size * -0.018,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                opacity: bottomIn,\n                transform: `translateY(${interpolate(bottomIn, [0, 1], [size * 0.16, 0])}px)`,\n              }}\n            >\n              {titleBottom}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <Grain id=\"gridpaper-editorial-grain\" frame={frame} opacity={0.16} />\n    </AbsoluteFill>\n  );\n};\n\n/** One spec line, typed on rather than faded in. */\nconst SpecRow: React.FC<{ index: number; label: string; size: number; colour: string; font: string }> = ({\n  index,\n  label,\n  size,\n  colour,\n  font,\n}) => {\n  const stage = useStaggeredStage(BUDGET, index, 3);\n  const on = interpolate(stage.frame, [30, 40], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const shown = Math.round(label.length * on);\n  return (\n    <div\n      style={{\n        fontFamily: font,\n        fontSize: size,\n        letterSpacing: size * 0.06,\n        lineHeight: 1.9,\n        textTransform: \"uppercase\",\n        color: colour,\n        whiteSpace: \"pre\",\n      }}\n    >\n      {label.slice(0, shown)}\n    </div>\n  );\n};\n\n/**\n * The stand-in subject inside the image well.\n *\n * A dial and two hands, massed centre-left under a key light. Abstract on\n * purpose: a recognisable object drawn in divs looks like clip art, whereas a\n * silhouette under a believable light reads as a photograph at thumbnail size,\n * which is all this well has to do.\n */\nconst Subject: React.FC<{ ink: string; accent: string; progress: number }> = ({ ink, accent, progress }) => (\n  <div style={{ position: \"absolute\", inset: 0, opacity: progress }}>\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        width: \"54%\",\n        height: \"54%\",\n        marginLeft: \"-27%\",\n        marginTop: \"-27%\",\n        borderRadius: \"50%\",\n        background: `linear-gradient(148deg, ${alpha(\"#fff\", 0.9)} 0%, ${alpha(\"#fff\", 0.5)} 38%, ${alpha(\n          ink,\n          0.22,\n        )} 72%, ${alpha(ink, 0.4)} 100%)`,\n        boxShadow: `0 6px 22px ${alpha(ink, 0.28)}, inset 0 1px 0 ${alpha(\"#fff\", 0.8)}`,\n      }}\n    />\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        width: \"36%\",\n        height: \"36%\",\n        marginLeft: \"-18%\",\n        marginTop: \"-18%\",\n        borderRadius: \"50%\",\n        backgroundColor: alpha(\"#fff\", 0.86),\n        boxShadow: `inset 0 0 0 1px ${alpha(ink, 0.14)}`,\n      }}\n    />\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        width: \"1.6%\",\n        height: \"15%\",\n        marginLeft: \"-0.8%\",\n        marginTop: \"-14%\",\n        backgroundColor: alpha(ink, 0.8),\n        transformOrigin: \"50% 93%\",\n        transform: `rotate(${interpolate(progress, [0, 1], [-40, 22])}deg)`,\n      }}\n    />\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        width: \"1.2%\",\n        height: \"11%\",\n        marginLeft: \"-0.6%\",\n        marginTop: \"-10%\",\n        backgroundColor: accent,\n        transformOrigin: \"50% 91%\",\n        transform: `rotate(${interpolate(progress, [0, 1], [60, 194])}deg)`,\n      }}\n    />\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"gridpaper-editorial\",\n  name: \"Gridpaper Editorial\",\n  category: \"titles\",\n  summary:\n    \"A cream graph-paper spread: a taped card drops onto the sheet, a highlighter swipes under the masthead and mono specs type on\",\n  tags: [\"editorial\", \"light\", \"paper\", \"grid\", \"polaroid\", \"print\", \"masthead\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: gridpaperEditorialSchema,\n  defaults: {\n    titleTop: \"Deep\",\n    titleBottom: \"Watches\",\n    specs: [\n      { key: \"REF\", value: \"116500LN\" },\n      { key: \"CAL\", value: \"4130 CHRONO\" },\n      { key: \"MAT\", value: \"OYSTERSTEEL\" },\n    ],\n    caption: \"Field notes — 03\",\n    tape: true,\n    brand: \"press\",\n  },\n  component: GridpaperEditorial,\n  file: \"src/registry/items/gridpaper-editorial.tsx\",\n  origin: {\n    prompt:\n      \"An editorial title card on cream graph paper, like a magazine spread laid out on an engineer's pad. A card should drop onto the sheet slightly rotated and settle, with a strip of tape across its top corner. Next to it a two-line masthead: the first word gets a highlighter stroke wiping in underneath it, the second is set in the accent colour. Monospace spec lines type on under the card. Print texture over everything.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An editorial title card on cream graph paper, like a magazine spread laid out on an engineer's pad. A card should drop onto the sheet slightly rotated and settle, with a strip of tape across its top corner. Next to it a two-line masthead: the first word gets a highlighter stroke wiping in underneath it, the second is set in the accent colour. Monospace spec lines type on under the card. Print texture over everything.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.7058,
      "props": [
        {
          "name": "titleTop",
          "type": "string",
          "description": "First title line. Gets the highlighter swipe behind it, so keep it short — one word is ideal.",
          "required": true,
          "defaultValue": "Deep",
          "options": null
        },
        {
          "name": "titleBottom",
          "type": "string",
          "description": "Second title line, set in the accent colour. The subject itself.",
          "required": true,
          "defaultValue": "Watches",
          "options": null
        },
        {
          "name": "specs",
          "type": "array",
          "description": "Monospace spec rows printed under the pinned card. Two to four reads best; this is a caption, not a table.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Small label above the title, printed in mono.",
          "required": false,
          "defaultValue": "Field notes — 03",
          "options": null
        },
        {
          "name": "tape",
          "type": "boolean",
          "description": "Stick a torn tape strip across the top edge of the pinned card.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. Light brands (press, paper, field) are what this layout is for.",
          "required": false,
          "defaultValue": "press",
          "options": null
        }
      ]
    },
    {
      "id": "hot-path-scan",
      "name": "Hot Path Scan",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "A defocused listing scrolls behind a scan front while one symbol resolves out of blur and locks sharp",
      "tags": [
        "technical",
        "code",
        "scan",
        "vertical",
        "focus",
        "atmosphere",
        "developer"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/hot-path-scan.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const hotPathScanSchema = z.object({\n  symbol: z\n    .string()\n    .describe(\"The string that resolves out of defocus. Reads best as an identifier — a symbol, address or key.\"),\n  label: z\n    .string()\n    .optional()\n    .describe(\"Small label above the symbol, printed in the accent colour.\"),\n  rows: z\n    .number()\n    .int()\n    .min(10)\n    .max(60)\n    .default(34)\n    .describe(\"How many listing rows scroll behind. More rows read as a dense log; fewer as a sparse trace.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof hotPathScanSchema>;\n\nconst BUDGET = BUDGETS.scene;\nconst SEED = \"hot-path-scan\";\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.9\" numOctaves={3} seed={Math.floor(frame / 2) % 64} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst HotPathScan: React.FC<Props> = ({ symbol, label, rows, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const rowH = height / rows;\n  const scroll = (time * height * 0.16) % rowH;\n\n  /**\n   * The symbol resolves rather than fades.\n   *\n   * It starts far out of focus and lands sharp, on the same long ease the rest\n   * of the grammar uses — the reading is \"a scanner locking onto an address\",\n   * and focus is the only thing that says that. It also picks up a small\n   * negative scale so the resolve has a direction.\n   */\n  const lock = interpolate(frame, [12, 46], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  // Sized to fit rather than fixed: a monospace glyph is about 0.6em wide, so a\n  // long identifier has to come down or it runs off both edges of a 9:16 frame.\n  const size = Math.min(width * 0.075, (width * 0.86) / Math.max(6, symbol.length * 0.6));\n  const blur = interpolate(lock, [0, 1], [size * 0.42, 0]);\n\n  // The scan front, sweeping down and lighting rows as it passes.\n  const scanY = ((time * 0.32) % 1.6) - 0.3;\n\n  const labelIn = interpolate(frame, [4, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const push = interpolate(stage.progress, [0, 1], [1, 1.035]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(78% 44% at 50% 48%, ${alpha(t.surface, 0.8)} 0%, ${alpha(\n            t.surface,\n            0.2,\n          )} 38%, ${alpha(t.bg, 0.94)} 72%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* ----------------------------------------------------- the listing */}\n        <div style={{ position: \"absolute\", inset: 0, transform: `translateY(${-scroll}px)` }}>\n          {Array.from({ length: rows + 2 }).map((_, i) => {\n            const w = 0.12 + random(`${SEED}-w-${i}`) * 0.72;\n            const x = random(`${SEED}-x-${i}`) * 0.16;\n            const base = 0.12 + random(`${SEED}-o-${i}`) * 0.3;\n            const y = i / rows;\n            // Rows brighten as the scan front reaches them, then decay behind it.\n            const d = (y - scanY) * 5.5;\n            const lit = Math.exp(-d * d);\n            return (\n              <div\n                key={i}\n                style={{\n                  position: \"absolute\",\n                  left: `${8 + x * 100}%`,\n                  top: i * rowH,\n                  width: `${w * 100}%`,\n                  height: Math.max(2, rowH * 0.26),\n                  borderRadius: 999,\n                  backgroundColor: alpha(t.accentAlt, base + lit * 0.62),\n                  filter: lit > 0.4 ? `blur(${(1 - lit) * 2}px)` : `blur(${1.6 + (1 - lit) * 1.6}px)`,\n                }}\n              />\n            );\n          })}\n        </div>\n\n        {/* the scan front itself */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: `${scanY * 100}%`,\n            height: height * 0.22,\n            marginTop: -height * 0.11,\n            background: `linear-gradient(180deg, ${alpha(t.accentAlt, 0)} 0%, ${alpha(\n              t.accentAlt,\n              0.12,\n            )} 46%, ${alpha(t.accentAlt, 0.2)} 50%, ${alpha(t.accentAlt, 0.12)} 54%, ${alpha(t.accentAlt, 0)} 100%)`,\n            mixBlendMode: \"screen\",\n          }}\n        />\n\n        {/* --------------------------------------------------- the symbol */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            padding: `0 ${width * 0.06}px`,\n          }}\n        >\n          {label ? (\n            <div\n              style={{\n                marginBottom: size * 0.42,\n                fontFamily: t.fontMono,\n                fontSize: size * 0.26,\n                fontWeight: 600,\n                letterSpacing: size * 0.09,\n                textTransform: \"uppercase\",\n                color: t.accentAlt,\n                opacity: labelIn,\n              }}\n            >\n              {label}\n            </div>\n          ) : null}\n\n          <div style={{ position: \"relative\" }}>\n            {/* Bloom, so the symbol spills light onto the listing behind it as\n                it locks — a bright thing in a dark frame is never contained by\n                its own edges. */}\n            <div\n              aria-hidden\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                filter: `blur(${size * 0.28}px)`,\n                opacity: 0.5 * lock,\n                mixBlendMode: \"screen\",\n                fontFamily: t.fontMono,\n                fontSize: size,\n                fontWeight: 700,\n                letterSpacing: size * 0.01,\n                color: t.ink,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {symbol}\n            </div>\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: size,\n                fontWeight: 700,\n                letterSpacing: size * 0.01,\n                color: t.ink,\n                whiteSpace: \"nowrap\",\n                filter: `blur(${blur}px)`,\n                transform: `scale(${interpolate(lock, [0, 1], [1.08, 1])})`,\n                opacity: Math.min(1, lock * 1.6),\n              }}\n            >\n              {symbol}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(108% 70% at 50% 50%, ${alpha(\"#000\", 0)} 28%, ${alpha(\"#000\", 0.2)} 58%, ${alpha(\n            \"#000\",\n            0.62,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"hot-path-grain\" frame={frame} opacity={0.08} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"hot-path-scan\",\n  name: \"Hot Path Scan\",\n  category: \"background\",\n  summary:\n    \"A defocused listing scrolls behind a scan front while one symbol resolves out of blur and locks sharp\",\n  tags: [\"technical\", \"code\", \"scan\", \"vertical\", \"focus\", \"atmosphere\", \"developer\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: hotPathScanSchema,\n  defaults: {\n    symbol: \"0xFF8A_L1_HOT_PATH\",\n    label: \"Trace locked\",\n    rows: 34,\n    brand: \"circuit\",\n  },\n  component: HotPathScan,\n  file: \"src/registry/items/hot-path-scan.tsx\",\n  origin: {\n    prompt:\n      \"A vertical developer-flavoured frame: rows of out-of-focus code scrolling upward behind a soft scan front that brightens each row as it passes. In the middle, one monospace identifier starts heavily defocused and resolves to razor sharp, blooming light onto the listing behind it as it locks. Nothing should ever come fully to rest.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical developer-flavoured frame: rows of out-of-focus code scrolling upward behind a soft scan front that brightens each row as it passes. In the middle, one monospace identifier starts heavily defocused and resolves to razor sharp, blooming light onto the listing behind it as it locks. Nothing should ever come fully to rest.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7531,
      "props": [
        {
          "name": "symbol",
          "type": "string",
          "description": "The string that resolves out of defocus. Reads best as an identifier — a symbol, address or key.",
          "required": true,
          "defaultValue": "0xFF8A_L1_HOT_PATH",
          "options": null
        },
        {
          "name": "label",
          "type": "string",
          "description": "Small label above the symbol, printed in the accent colour.",
          "required": false,
          "defaultValue": "Trace locked",
          "options": null
        },
        {
          "name": "rows",
          "type": "number",
          "description": "How many listing rows scroll behind. More rows read as a dense log; fewer as a sparse trace.",
          "required": false,
          "defaultValue": "34",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "idle-loops",
      "name": "Idle Loops",
      "category": "primitive",
      "categoryLabel": "Primitives",
      "summary": "Float, pulse, breathe and wobble at deliberately tiny amplitudes, each closing its loop exactly because the cycle count is an integer",
      "tags": [
        "primitive",
        "idle",
        "loop",
        "subtle",
        "documentation"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/idle-loops.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const idleLoopsSchema = z.object({\n  amplitude: z\n    .number()\n    .min(0.3)\n    .max(2)\n    .default(1)\n    .describe(\"Multiplier on every amplitude. Above ~1.4 these stop being subtle and start distracting.\"),\n  cycles: z\n    .number()\n    .min(1)\n    .max(4)\n    .default(2)\n    .describe(\"Whole cycles across the shot. Integers only, or the loop will not close.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof idleLoopsSchema>;\n\nconst LOOPS = [\n  { key: \"float\", label: \"float\", note: \"±6px vertical\" },\n  { key: \"pulse\", label: \"pulse\", note: \"opacity 0.72 → 1\" },\n  { key: \"breathe\", label: \"breathe\", note: \"scale ±2%\" },\n  { key: \"wobble\", label: \"wobble\", note: \"rotate ±1.4°\" },\n] as const;\n\n/**\n * The motions that keep a held shot from looking frozen.\n *\n * Two rules. Amplitudes are deliberately tiny — the intended range is ±6px,\n * ±2% scale and ±1.4°; if you can consciously notice one of these while reading\n * the frame, it is too strong and it is stealing attention from the content.\n * And every one is driven by sin(2π · cycles · progress) with an integer cycle\n * count, so the last frame equals the first exactly and the loop is seamless by\n * construction rather than by tolerance.\n */\nconst loopStyle = (\n  kind: (typeof LOOPS)[number][\"key\"],\n  phase: number,\n  amp: number,\n  unit: number,\n): React.CSSProperties => {\n  const s = Math.sin(phase);\n  switch (kind) {\n    case \"float\":\n      return { transform: `translateY(${s * unit * 0.055 * amp}px)` };\n    case \"pulse\":\n      return { opacity: 0.86 + s * 0.14 * amp };\n    case \"breathe\":\n      return { transform: `scale(${1 + s * 0.02 * amp})` };\n    case \"wobble\":\n      return { transform: `rotate(${s * 1.4 * amp}deg)` };\n    default:\n      return {};\n  }\n};\n\nconst IdleLoops: React.FC<Props> = ({ amplitude, cycles, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const phase = stage.progress * Math.PI * 2 * Math.round(cycles);\n  const gap = width * 0.026;\n  const tileW = (width * 0.8 - gap * 3) / 4;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        opacity: stage.presence,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          marginBottom: height * 0.055,\n        }}\n      >\n        Idle loops · seamless by construction\n      </div>\n\n      <div style={{ display: \"flex\", gap, width: width * 0.8 }}>\n        {LOOPS.map((loop, i) => (\n          <Tile\n            key={loop.key}\n            loop={loop}\n            phase={phase + i * 0.4}\n            amp={amplitude}\n            w={tileW}\n            h={height * 0.3}\n            tokens={t}\n            accent={i === 2}\n          />\n        ))}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Tile: React.FC<{\n  loop: (typeof LOOPS)[number];\n  phase: number;\n  amp: number;\n  w: number;\n  h: number;\n  tokens: BrandTokens;\n  accent: boolean;\n}> = ({ loop, phase, amp, w, h, tokens, accent }) => (\n  <div style={{ width: w }}>\n    <div\n      style={{\n        height: h,\n        borderRadius: tokens.radius,\n        border: `1px solid ${tokens.rule}`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          width: w * 0.46,\n          height: w * 0.46,\n          borderRadius: tokens.radius * 0.8,\n          backgroundColor: accent ? tokens.accent : tokens.surface,\n          border: `1px solid ${accent ? tokens.accent : tokens.rule}`,\n          ...loopStyle(loop.key, phase, amp, w),\n        }}\n      />\n    </div>\n    <div\n      style={{\n        marginTop: h * 0.08,\n        fontFamily: tokens.fontMono,\n        fontSize: w * 0.078,\n        letterSpacing: w * 0.006,\n        color: tokens.ink,\n      }}\n    >\n      {loop.label}\n    </div>\n    <div\n      style={{\n        marginTop: h * 0.022,\n        fontFamily: tokens.fontBody,\n        fontSize: w * 0.068,\n        color: tokens.inkMuted,\n      }}\n    >\n      {loop.note}\n    </div>\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"idle-loops\",\n  name: \"Idle Loops\",\n  category: \"primitive\",\n  summary:\n    \"Float, pulse, breathe and wobble at deliberately tiny amplitudes, each closing its loop exactly because the cycle count is an integer\",\n  tags: [\"primitive\", \"idle\", \"loop\", \"subtle\", \"documentation\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: idleLoopsSchema,\n  defaults: { amplitude: 1, cycles: 2, brand: \"slate\" },\n  component: IdleLoops,\n  file: \"src/registry/items/idle-loops.tsx\",\n  origin: {\n    prompt:\n      \"Four subtle idle animations side by side — float, pulse, breathe and wobble — all perfectly loop-safe so the last frame matches the first, with amplitudes tasteful enough not to distract.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Four subtle idle animations side by side — float, pulse, breathe and wobble — all perfectly loop-safe so the last frame matches the first, with amplitudes tasteful enough not to distract.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2412,
      "props": [
        {
          "name": "amplitude",
          "type": "number",
          "description": "Multiplier on every amplitude. Above ~1.4 these stop being subtle and start distracting.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "cycles",
          "type": "number",
          "description": "Whole cycles across the shot. Integers only, or the loop will not close.",
          "required": false,
          "defaultValue": "2",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "imessage-share",
      "name": "iMessage Share",
      "category": "social",
      "categoryLabel": "Social formats",
      "summary": "A light message thread that grows from the bottom, with a video attachment card and a tapback on the last bubble",
      "tags": [
        "messages",
        "light",
        "vertical",
        "social proof",
        "thread",
        "attachment",
        "ios"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/imessage-share.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const imessageShareSchema = z.object({\n  timestamp: z\n    .string()\n    .default(\"Today 9:41 AM\")\n    .describe(\"Thread timestamp, centred above the first message.\"),\n  messages: z\n    .array(\n      z.object({\n        text: z.string(),\n        side: z.enum([\"in\", \"out\"]),\n        kind: z.enum([\"text\", \"video\"]).default(\"text\"),\n      }),\n    )\n    .min(2)\n    .max(6)\n    .describe(\"The thread, in order. 'in' is the grey received bubble, 'out' is the accent-coloured sent one. A 'video' entry renders an attachment card instead of a bubble, using its text as the duration label.\"),\n  reaction: z\n    .string()\n    .optional()\n    .describe(\"Tapback that lands on the last message, e.g. an exclamation or a heart character.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\"Brand token id — one of the keys of BRANDS. Built for the light brands.\"),\n});\n\ntype Props = z.infer<typeof imessageShareSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"multiply\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.92\" numOctaves={3} seed={Math.floor(frame / 3) % 48} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * The bubble tail.\n *\n * Drawn as a path rather than a rotated square, because the shape that reads as\n * iOS is a curve that flows out of the bubble's corner and hooks back — a\n * triangle looks like a speech balloon from a different decade.\n */\nconst Tail: React.FC<{ side: \"in\" | \"out\"; size: number; fill: string }> = ({ side, size, fill }) => (\n  <svg\n    width={size}\n    height={size}\n    viewBox=\"0 0 16 16\"\n    style={{\n      position: \"absolute\",\n      bottom: -1,\n      [side === \"out\" ? \"right\" : \"left\"]: -size * 0.34,\n      transform: side === \"out\" ? \"none\" : \"scaleX(-1)\",\n    }}\n    aria-hidden\n  >\n    <path d=\"M0 16 C6 16 10 12 10 6 L10 16 Z M10 6 C10 12 13 15 16 16 C11 16.4 6 15 3 12 Z\" fill={fill} />\n  </svg>\n);\n\nconst ImessageShare: React.FC<Props> = ({ timestamp, messages, reaction, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const pad = width * 0.07;\n  const fontSize = width * 0.045;\n  const radius = fontSize * 1.05;\n\n  const stampIn = interpolate(frame, [2, 16], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const GAP = 15;\n  const START = 12;\n\n  /**\n   * Every message that has already landed pushes the thread up.\n   *\n   * A real thread does not lay itself out and then fill in — it grows from the\n   * bottom and shoves its own history off the top. Measuring each entry's height\n   * and summing what has arrived is what produces that, and it is the difference\n   * between this reading as a conversation and reading as a list.\n   */\n  const heightOf = (m: Props[\"messages\"][number]) =>\n    m.kind === \"video\" ? width * 0.52 : fontSize * 1.5 * Math.ceil(m.text.length / 26) + fontSize * 1.5;\n\n  const arrivedShift = messages.reduce((acc, m, i) => {\n    const landed = interpolate(frame, [START + i * GAP, START + i * GAP + 12], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.enter,\n    });\n    return acc + heightOf(m) * landed * 0.5;\n  }, 0);\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.025]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(90% 55% at 50% 8%, ${alpha(\"#fff\", 0.9)} 0%, ${alpha(\n            \"#fff\",\n            0.35,\n          )} 38%, ${alpha(t.ink, 0.02)} 72%, ${alpha(t.ink, 0.06)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"flex-end\",\n          padding: `0 ${pad}px ${height * 0.12}px`,\n          transform: `translateY(${-arrivedShift * 0.25}px) scale(${push})`,\n          transformOrigin: \"50% 100%\",\n          opacity: stage.presence,\n        }}\n      >\n        <div\n          style={{\n            textAlign: \"center\",\n            marginBottom: fontSize * 1.1,\n            fontFamily: t.fontBody,\n            fontSize: fontSize * 0.62,\n            fontWeight: 500,\n            color: alpha(t.ink, 0.42),\n            opacity: stampIn,\n          }}\n        >\n          {timestamp}\n        </div>\n\n        {messages.map((m, i) => {\n          const at = START + i * GAP;\n          /**\n           * iOS bubbles do not fade — they scale up from about 80% with a small\n           * overshoot, anchored at the corner the tail is on. Anchoring matters:\n           * scaled from the centre it looks like a modal opening.\n           */\n          const e = spring({ frame: frame - at, fps, config: SPRINGS.snap, durationInFrames: 16 });\n          const out = m.side === \"out\";\n          const fill = out ? t.accent : alpha(t.ink, 0.09);\n          const colour = out ? t.accentInk : t.ink;\n\n          if (m.kind === \"video\") {\n            return (\n              <div\n                key={i}\n                style={{\n                  alignSelf: out ? \"flex-end\" : \"flex-start\",\n                  marginBottom: fontSize * 0.5,\n                  width: width * 0.56,\n                  opacity: Math.min(1, e * 2),\n                  transform: `scale(${interpolate(e, [0, 1], [0.82, 1])})`,\n                  transformOrigin: out ? \"100% 100%\" : \"0% 100%\",\n                }}\n              >\n                <div\n                  style={{\n                    position: \"relative\",\n                    width: \"100%\",\n                    aspectRatio: \"16 / 10\",\n                    borderRadius: radius,\n                    overflow: \"hidden\",\n                    background: `linear-gradient(148deg, ${alpha(t.accent, 0.95)} 0%, ${alpha(\n                      t.accentAlt,\n                      0.85,\n                    )} 58%, ${alpha(t.ink, 0.85)} 100%)`,\n                    boxShadow: `0 ${fontSize * 0.24}px ${fontSize * 0.7}px ${alpha(t.ink, 0.18)}`,\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      left: \"50%\",\n                      top: \"50%\",\n                      width: fontSize * 1.9,\n                      height: fontSize * 1.9,\n                      marginLeft: -fontSize * 0.95,\n                      marginTop: -fontSize * 0.95,\n                      borderRadius: \"50%\",\n                      backgroundColor: alpha(\"#fff\", 0.22),\n                      border: `2px solid ${alpha(\"#fff\", 0.75)}`,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      justifyContent: \"center\",\n                    }}\n                  >\n                    <div\n                      style={{\n                        marginLeft: fontSize * 0.14,\n                        width: 0,\n                        height: 0,\n                        borderTop: `${fontSize * 0.34}px solid transparent`,\n                        borderBottom: `${fontSize * 0.34}px solid transparent`,\n                        borderLeft: `${fontSize * 0.56}px solid #fff`,\n                      }}\n                    />\n                  </div>\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      right: fontSize * 0.4,\n                      bottom: fontSize * 0.4,\n                      padding: `${fontSize * 0.12}px ${fontSize * 0.34}px`,\n                      borderRadius: 999,\n                      backgroundColor: alpha(\"#000\", 0.5),\n                      fontFamily: t.fontBody,\n                      fontSize: fontSize * 0.52,\n                      fontWeight: 600,\n                      color: \"#fff\",\n                    }}\n                  >\n                    {m.text}\n                  </div>\n                </div>\n              </div>\n            );\n          }\n\n          const isLast = i === messages.length - 1;\n          const reactionE = spring({\n            frame: frame - (at + 14),\n            fps,\n            config: SPRINGS.pop,\n            durationInFrames: 18,\n          });\n\n          return (\n            <div\n              key={i}\n              style={{\n                position: \"relative\",\n                alignSelf: out ? \"flex-end\" : \"flex-start\",\n                maxWidth: \"78%\",\n                marginBottom: fontSize * 0.42,\n                opacity: Math.min(1, e * 2),\n                transform: `scale(${interpolate(e, [0, 1], [0.8, 1])})`,\n                transformOrigin: out ? \"100% 100%\" : \"0% 100%\",\n              }}\n            >\n              <div\n                style={{\n                  position: \"relative\",\n                  padding: `${fontSize * 0.44}px ${fontSize * 0.62}px`,\n                  borderRadius: radius,\n                  backgroundColor: fill,\n                  fontFamily: t.fontBody,\n                  fontSize,\n                  lineHeight: 1.32,\n                  color: colour,\n                }}\n              >\n                {m.text}\n                <Tail side={m.side} size={fontSize * 0.85} fill={fill} />\n              </div>\n\n              {isLast && reaction ? (\n                <div\n                  style={{\n                    position: \"absolute\",\n                    top: -fontSize * 0.85,\n                    [out ? \"left\" : \"right\"]: -fontSize * 0.3,\n                    width: fontSize * 1.5,\n                    height: fontSize * 1.5,\n                    borderRadius: \"50%\",\n                    backgroundColor: t.surface,\n                    border: `${Math.max(1, fontSize * 0.045)}px solid ${alpha(t.ink, 0.1)}`,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    justifyContent: \"center\",\n                    fontFamily: t.fontBody,\n                    fontSize: fontSize * 0.78,\n                    fontWeight: 700,\n                    color: t.accent,\n                    boxShadow: `0 ${fontSize * 0.1}px ${fontSize * 0.3}px ${alpha(t.ink, 0.14)}`,\n                    opacity: Math.min(1, reactionE * 2),\n                    transform: `scale(${reactionE})`,\n                  }}\n                >\n                  {reaction}\n                </div>\n              ) : null}\n            </div>\n          );\n        })}\n      </div>\n\n      <Grain id=\"imessage-share-grain\" frame={frame} opacity={0.07} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"imessage-share\",\n  name: \"iMessage Share\",\n  category: \"social\",\n  summary:\n    \"A light message thread that grows from the bottom, with a video attachment card and a tapback on the last bubble\",\n  tags: [\"messages\", \"light\", \"vertical\", \"social proof\", \"thread\", \"attachment\", \"ios\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: imessageShareSchema,\n  defaults: {\n    timestamp: \"Today 9:41 AM\",\n    messages: [\n      { text: \"Launch is in 5 mins!! Where's the video?!\", side: \"in\", kind: \"text\" },\n      { text: \"0:30\", side: \"out\", kind: \"video\" },\n      { text: \"wait how did you make it so fast??\", side: \"in\", kind: \"text\" },\n      { text: \"Made it with Motion\", side: \"out\", kind: \"text\" },\n    ],\n    reaction: \"!\",\n    brand: \"paper\",\n  },\n  component: ImessageShare,\n  file: \"src/registry/items/imessage-share.tsx\",\n  origin: {\n    prompt:\n      \"A vertical light-mode message thread as social proof. Bubbles should arrive one at a time and scale up from their tail corner with a small overshoot, the way iOS does — never a fade. The thread has to grow from the bottom and push its own history upward as messages land. One entry is a video attachment card with a play button and a duration pill instead of a bubble, and a tapback pops onto the final message.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical light-mode message thread as social proof. Bubbles should arrive one at a time and scale up from their tail corner with a small overshoot, the way iOS does — never a fade. The thread has to grow from the bottom and push its own history upward as messages land. One entry is a video attachment card with a play button and a duration pill instead of a bubble, and a tapback pops onto the final message.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.6888,
      "props": [
        {
          "name": "timestamp",
          "type": "string",
          "description": "Thread timestamp, centred above the first message.",
          "required": false,
          "defaultValue": "Today 9:41 AM",
          "options": null
        },
        {
          "name": "messages",
          "type": "array",
          "description": "The thread, in order. 'in' is the grey received bubble, 'out' is the accent-coloured sent one. A 'video' entry renders an attachment card instead of a bubble, using its text as the duration label.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "reaction",
          "type": "string",
          "description": "Tapback that lands on the last message, e.g. an exclamation or a heart character.",
          "required": false,
          "defaultValue": "!",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. Built for the light brands.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "impact-title-rules",
      "name": "Impact Title Rules",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "Heavy uppercase lines whip in from the left with true directional smear, each stepping in and drawing its own accent rule",
      "tags": [
        "title",
        "impact",
        "vertical",
        "motion blur",
        "rules",
        "stagger",
        "caption"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/impact-title-rules.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const impactTitleRulesSchema = z.object({\n  lines: z\n    .array(z.string())\n    .min(1)\n    .max(4)\n    .describe(\"The stacked lines, top to bottom. One word each hits hardest — this is set enormous and each line gets its own rule.\"),\n  indent: z\n    .number()\n    .min(0)\n    .max(0.3)\n    .default(0.09)\n    .describe(\"How far each line steps in from the one above, as a fraction of frame width. 0 stacks them flush left.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\"Subtitle pill at the bottom of the frame, in the burned-in caption style.\"),\n  rules: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw an accent rule under each line, wiping in as the line lands.\"),\n  brand: z\n    .string()\n    .default(\"signal\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof impactTitleRulesSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.84\" numOctaves={3} seed={Math.floor(frame / 2) % 64} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * One line of the stack.\n *\n * The word arrives fast from the left and is smeared while it travels, using a\n * horizontal-only SVG blur driven by the distance covered between this frame and\n * the last. That is why it reads as a whip rather than a slide: at speed the\n * letterforms genuinely stretch, and at rest the smear is exactly zero. CSS\n * `blur()` cannot do this — it is isotropic and would fog the line vertically\n * too, which looks like fog rather than speed.\n */\nconst Line: React.FC<{\n  text: string;\n  index: number;\n  size: number;\n  indent: number;\n  rules: boolean;\n  t: typeof DEFAULT_BRAND;\n}> = ({ text, index, size, indent, rules, t }) => {\n  const stage = useStaggeredStage(BUDGET, index, 7);\n  const frame = stage.frame;\n\n  const travel = (f: number) =>\n    interpolate(f, [2, 18], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.enter,\n    });\n\n  const e = travel(frame);\n  const ePrev = travel(frame - 1);\n  const distance = size * 1.5;\n  const x = interpolate(e, [0, 1], [-distance, 0]);\n  const speed = Math.abs(e - ePrev) * distance;\n  const smear = Math.min(size * 0.55, speed * 0.75);\n  const filterId = `impact-smear-${index}`;\n\n  const rule = interpolate(frame, [14, 30], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <div style={{ marginLeft: indent * index, marginBottom: size * 0.1 }}>\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <filter id={filterId} x=\"-40%\" y=\"-20%\" width=\"180%\" height=\"140%\">\n          <feGaussianBlur stdDeviation={`${smear} 0`} />\n        </filter>\n      </svg>\n      <div\n        style={{\n          fontFamily: t.fontDisplay,\n          fontSize: size,\n          fontWeight: 700,\n          lineHeight: 0.92,\n          letterSpacing: size * -0.038,\n          textTransform: \"uppercase\",\n          color: t.ink,\n          opacity: Math.min(1, e * 2.5),\n          transform: `translateX(${x}px)`,\n          filter: smear > 0.5 ? `url(#${filterId})` : \"none\",\n        }}\n      >\n        {text}\n      </div>\n      {rules ? (\n        <div\n          style={{\n            marginTop: size * 0.07,\n            height: Math.max(3, size * 0.055),\n            width: size * 2.1,\n            backgroundColor: t.accentAlt,\n            transform: `scaleX(${rule})`,\n            transformOrigin: \"left center\",\n          }}\n        />\n      ) : null}\n    </div>\n  );\n};\n\nconst ImpactTitleRules: React.FC<Props> = ({ lines, indent, caption, rules, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  /**\n   * Sized against the longest line AND the indent it will carry.\n   *\n   * The stagger pushes the last line right by `indent × (lines − 1)`, so sizing\n   * on the type alone puts the bottom line through the right edge — which is\n   * exactly what it did before this.\n   */\n  const longest = Math.max(...lines.map((l) => l.length));\n  const budget = width * 0.86 - width * indent * (lines.length - 1);\n  const size = Math.min(width * (lines.length > 3 ? 0.15 : 0.185), budget / (longest * 0.62));\n\n  const capIn = interpolate(frame, [8 + lines.length * 7, 22 + lines.length * 7], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.04]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(80% 48% at 24% 26%, ${alpha(t.surface, 0.7)} 0%, ${alpha(\n            t.surface,\n            0.18,\n          )} 40%, ${alpha(t.bg, 0.9)} 74%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          justifyContent: \"center\",\n          padding: `0 ${width * 0.075}px`,\n          transform: `scale(${push})`,\n          transformOrigin: \"26% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {lines.map((line, i) => (\n          <Line\n            key={`${line}-${i}`}\n            text={line}\n            index={i}\n            size={size}\n            indent={width * indent}\n            rules={rules}\n            t={t}\n          />\n        ))}\n      </div>\n\n      {caption ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            bottom: height * 0.09,\n            display: \"flex\",\n            justifyContent: \"center\",\n            opacity: capIn,\n          }}\n        >\n          <span\n            style={{\n              backgroundColor: t.ink,\n              color: t.bg,\n              padding: `${width * 0.012}px ${width * 0.026}px`,\n              borderRadius: width * 0.008,\n              fontFamily: t.fontBody,\n              fontSize: width * 0.033,\n              fontWeight: 500,\n            }}\n          >\n            {caption}\n          </span>\n        </div>\n      ) : null}\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(118% 80% at 34% 44%, ${alpha(\"#000\", 0)} 32%, ${alpha(\"#000\", 0.14)} 62%, ${alpha(\n            \"#000\",\n            0.46,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"impact-title-grain\" frame={frame} opacity={0.085} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"impact-title-rules\",\n  name: \"Impact Title Rules\",\n  category: \"typography\",\n  summary:\n    \"Heavy uppercase lines whip in from the left with true directional smear, each stepping in and drawing its own accent rule\",\n  tags: [\"title\", \"impact\", \"vertical\", \"motion blur\", \"rules\", \"stagger\", \"caption\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: impactTitleRulesSchema,\n  defaults: {\n    lines: [\"Launch\", \"Video\", \"Tool\"],\n    indent: 0.09,\n    caption: \"it's an engine for everything\",\n    rules: true,\n    brand: \"signal\",\n  },\n  component: ImpactTitleRules,\n  file: \"src/registry/items/impact-title-rules.tsx\",\n  origin: {\n    prompt:\n      \"A vertical title card with two or three enormous uppercase words stacked and stepping in to the right, each with a coloured rule wiping in underneath. The words should whip in from the left fast enough to smear — sample the actual per-frame distance and apply a horizontal-only blur so the letterforms stretch while travelling and are perfectly sharp on arrival. Burned-in caption pill at the bottom.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical title card with two or three enormous uppercase words stacked and stepping in to the right, each with a coloured rule wiping in underneath. The words should whip in from the left fast enough to smear — sample the actual per-frame distance and apply a horizontal-only blur so the letterforms stretch while travelling and are perfectly sharp on arrival. Burned-in caption pill at the bottom.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.8065,
      "props": [
        {
          "name": "lines",
          "type": "array",
          "description": "The stacked lines, top to bottom. One word each hits hardest — this is set enormous and each line gets its own rule.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "indent",
          "type": "number",
          "description": "How far each line steps in from the one above, as a fraction of frame width. 0 stacks them flush left.",
          "required": false,
          "defaultValue": "0.09",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Subtitle pill at the bottom of the frame, in the burned-in caption style.",
          "required": false,
          "defaultValue": "it's an engine for everything",
          "options": null
        },
        {
          "name": "rules",
          "type": "boolean",
          "description": "Draw an accent rule under each line, wiping in as the line lands.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "signal",
          "options": null
        }
      ]
    },
    {
      "id": "integration-orbit",
      "name": "Integration Orbit",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "Partner marks arrive outward from a glowing hub onto two counter-rotating orbits, each counter-spun to stay upright",
      "tags": [
        "integrations",
        "orbit",
        "logos",
        "loop",
        "ecosystem"
      ],
      "aspect": "1:1",
      "width": 1080,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/integration-orbit.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const integrationOrbitSchema = z.object({\n  label: z\n    .string()\n    .default(\"Connects to everything\")\n    .describe(\"Small mono label above the orbit. Keep it to a few words.\"),\n  inner: z.number().min(3).max(8).default(5).describe(\"Marks on the inner ring.\"),\n  outer: z.number().min(4).max(10).default(7).describe(\"Marks on the outer ring.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof integrationOrbitSchema>;\n\n/** Four abstract mark shapes, drawn rather than fetched, so the item stays self-contained. */\nconst MARK_KINDS = 4;\n\nconst IntegrationOrbit: React.FC<Props> = ({ label, inner, outer, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const side = Math.min(width, height);\n  const rInner = side * 0.24;\n  const rOuter = side * 0.38;\n  const hub = side * 0.11;\n\n  // Exactly one revolution across the budget, so the last frame matches the first.\n  const spin = stage.progress * 360;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, justifyContent: \"center\", alignItems: \"center\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          top: height * 0.11,\n          fontFamily: t.fontMono,\n          fontSize: side * 0.022,\n          letterSpacing: side * 0.004,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          opacity: stage.presence,\n        }}\n      >\n        {label}\n      </div>\n\n      <div style={{ position: \"relative\", width: side * 0.86, height: side * 0.86, opacity: stage.presence }}>\n        <Rings rInner={rInner} rOuter={rOuter} side={side} tokens={t} />\n\n        <Orbit count={inner} radius={rInner} spin={spin} side={side} tokens={t} tier={0} />\n        <Orbit count={outer} radius={rOuter} spin={-spin} side={side} tokens={t} tier={1} />\n\n        {/* Hub sits above the rings so connectors read as running underneath it. */}\n        <Hub size={hub} tokens={t} />\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Rings: React.FC<{\n  rInner: number;\n  rOuter: number;\n  side: number;\n  tokens: typeof DEFAULT_BRAND;\n}> = ({ rInner, rOuter, side, tokens }) => {\n  const stage = useStage(BUDGETS.feature);\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      viewBox={`${-side * 0.43} ${-side * 0.43} ${side * 0.86} ${side * 0.86}`}\n      style={{ position: \"absolute\", inset: 0 }}\n    >\n      <circle cx={0} cy={0} r={rInner} fill=\"none\" stroke={tokens.rule} strokeWidth={Math.max(1, side * 0.0016)} opacity={stage.enter} />\n      <circle cx={0} cy={0} r={rOuter} fill=\"none\" stroke={tokens.rule} strokeWidth={Math.max(1, side * 0.0016)} opacity={stage.enter * 0.75} />\n    </svg>\n  );\n};\n\nconst Orbit: React.FC<{\n  count: number;\n  radius: number;\n  spin: number;\n  side: number;\n  tokens: typeof DEFAULT_BRAND;\n  tier: number;\n}> = ({ count, radius, spin, side, tokens, tier }) => (\n  <>\n    {Array.from({ length: count }, (_, i) => (\n      <OrbitMark\n        key={i}\n        index={i}\n        count={count}\n        radius={radius}\n        spin={spin}\n        side={side}\n        tokens={tokens}\n        tier={tier}\n      />\n    ))}\n  </>\n);\n\nconst OrbitMark: React.FC<{\n  index: number;\n  count: number;\n  radius: number;\n  spin: number;\n  side: number;\n  tokens: typeof DEFAULT_BRAND;\n  tier: number;\n}> = ({ index, count, radius, spin, side, tokens, tier }) => {\n  const stage = useStaggeredStage(BUDGETS.feature, index + tier * 3, 3);\n  const angle = (360 / count) * index + spin;\n  const size = side * (tier === 0 ? 0.062 : 0.05);\n  // Marks scale outward from the hub as they arrive.\n  const grow = interpolate(stage.enter, [0, 1], [0.2, 1]);\n  const kind = Math.floor(random(`orbit-${tier}-${index}`) * MARK_KINDS);\n  const isAccent = random(`accent-${tier}-${index}`) > 0.72;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        // Rotate out to the orbit, then counter-rotate so the mark stays upright.\n        transform: `rotate(${angle}deg) translateX(${radius * grow}px) rotate(${-angle}deg)`,\n        opacity: stage.enter,\n      }}\n    >\n      <div style={{ transform: `translate(-50%, -50%) scale(${grow})` }}>\n        <Mark kind={kind} size={size} tokens={tokens} accent={isAccent} />\n      </div>\n    </div>\n  );\n};\n\nconst Mark: React.FC<{\n  kind: number;\n  size: number;\n  tokens: typeof DEFAULT_BRAND;\n  accent: boolean;\n}> = ({ kind, size, tokens, accent }) => {\n  const stroke = accent ? tokens.accent : tokens.inkMuted;\n  const w = Math.max(1.5, size * 0.09);\n  return (\n    <svg width={size} height={size} viewBox=\"0 0 24 24\">\n      <rect\n        x={0.75}\n        y={0.75}\n        width={22.5}\n        height={22.5}\n        rx={6}\n        fill={tokens.surface}\n        stroke={tokens.rule}\n        strokeWidth={1}\n      />\n      {kind === 0 ? <circle cx={12} cy={12} r={5} fill=\"none\" stroke={stroke} strokeWidth={w} /> : null}\n      {kind === 1 ? (\n        <path d=\"M7 16 L12 7 L17 16 Z\" fill=\"none\" stroke={stroke} strokeWidth={w} strokeLinejoin=\"round\" />\n      ) : null}\n      {kind === 2 ? <rect x={7.5} y={7.5} width={9} height={9} rx={2} fill=\"none\" stroke={stroke} strokeWidth={w} /> : null}\n      {kind === 3 ? (\n        <path d=\"M7 12 A5 5 0 0 1 17 12\" fill=\"none\" stroke={stroke} strokeWidth={w} strokeLinecap=\"round\" />\n      ) : null}\n    </svg>\n  );\n};\n\nconst Hub: React.FC<{ size: number; tokens: typeof DEFAULT_BRAND }> = ({ size, tokens }) => {\n  const stage = useStage(BUDGETS.feature);\n  // A slow breathe keeps the held centre from reading as a frozen frame.\n  const breathe = 1 + Math.sin(stage.progress * Math.PI * 2) * 0.02;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: \"50%\",\n        transform: `translate(-50%, -50%) scale(${stage.enter * breathe})`,\n      }}\n    >\n      <div\n        style={{\n          width: size,\n          height: size,\n          borderRadius: size * 0.3,\n          backgroundColor: tokens.accent,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          boxShadow: `0 0 ${size * 0.8}px ${tokens.accent}55`,\n        }}\n      >\n        <svg width={size * 0.52} height={size * 0.52} viewBox=\"0 0 24 24\">\n          <path\n            d=\"M5 18V6.5l7 7.5 7-7.5V18\"\n            fill=\"none\"\n            stroke={tokens.accentInk}\n            strokeWidth={2.6}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"integration-orbit\",\n  name: \"Integration Orbit\",\n  category: \"brand\",\n  summary:\n    \"Partner marks arrive outward from a glowing hub onto two counter-rotating orbits, each counter-spun to stay upright\",\n  tags: [\"integrations\", \"orbit\", \"logos\", \"loop\", \"ecosystem\"],\n  budget: BUDGETS.feature,\n  aspect: \"1:1\",\n  schema: integrationOrbitSchema,\n  defaults: { label: \"Connects to everything\", inner: 5, outer: 7, brand: \"slate\" },\n  component: IntegrationOrbit,\n  file: \"src/registry/items/integration-orbit.tsx\",\n  origin: {\n    prompt:\n      \"Partner logos orbiting a central product mark on two hairline rings turning in opposite directions, marks staying upright as they travel, arriving outward from the hub with a stagger.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Partner logos orbiting a central product mark on two hairline rings turning in opposite directions, marks staying upright as they travel, arriving outward from the hub with a stagger.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6036,
      "props": [
        {
          "name": "label",
          "type": "string",
          "description": "Small mono label above the orbit. Keep it to a few words.",
          "required": false,
          "defaultValue": "Connects to everything",
          "options": null
        },
        {
          "name": "inner",
          "type": "number",
          "description": "Marks on the inner ring.",
          "required": false,
          "defaultValue": "5",
          "options": null
        },
        {
          "name": "outer",
          "type": "number",
          "description": "Marks on the outer ring.",
          "required": false,
          "defaultValue": "7",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "karaoke-captions",
      "name": "Karaoke Captions",
      "category": "social",
      "categoryLabel": "Social formats",
      "summary": "Word-timed captions where the spoken word pops onto an accent block and its neighbours dim, in a fixed-height row that never reflows",
      "tags": [
        "social",
        "captions",
        "karaoke",
        "vertical",
        "subtitles"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/karaoke-captions.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, SPRINGS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const karaokeCaptionsSchema = z.object({\n  words: z\n    .array(z.string())\n    .min(3)\n    .describe(\"The spoken words, in order. One caption word each.\"),\n  perWord: z\n    .number()\n    .min(4)\n    .max(24)\n    .default(9)\n    .describe(\"Frames per word. Around 9 at 30fps matches natural speech pace.\"),\n  window: z\n    .number()\n    .min(1)\n    .max(5)\n    .default(3)\n    .describe(\"Words visible at once. Three is the readable maximum on a phone.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof karaokeCaptionsSchema>;\n\n/**\n * Word-timed captions in the social style.\n *\n * The active word is scaled up on an accent block and its neighbours are muted,\n * which is what lets a viewer track the audio without reading ahead. Two details\n * do the heavy lifting: a hard text shadow so the type survives any background it\n * lands on, and a fixed-height row so the caption block never reflows as word\n * widths change — jitter there is the single most common tell of a cheap caption.\n */\nconst KaraokeCaptions: React.FC<Props> = ({ words, perWord, window: win, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const active = Math.min(words.length - 1, Math.floor(stage.frame / perWord));\n  const half = Math.floor(win / 2);\n  const start = Math.max(0, Math.min(active - half, words.length - win));\n  const visible = words.slice(start, start + win);\n\n  const size = width * 0.092;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          backgroundImage: `linear-gradient(200deg, ${t.surface} 0%, ${t.bg} 60%, ${t.accent}1f 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          bottom: height * 0.26,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: size * 0.18,\n          height: size * 3.4,\n          opacity: stage.presence,\n        }}\n      >\n        {visible.map((word, i) => {\n          const index = start + i;\n          const isActive = index === active;\n          return (\n            <Word\n              key={`${word}-${index}`}\n              word={word}\n              isActive={isActive}\n              enteredAt={index * perWord}\n              frame={stage.frame}\n              fps={fps}\n              size={size}\n              tokens={t}\n            />\n          );\n        })}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Word: React.FC<{\n  word: string;\n  isActive: boolean;\n  enteredAt: number;\n  frame: number;\n  fps: number;\n  size: number;\n  tokens: BrandTokens;\n}> = ({ word, isActive, enteredAt, frame, fps, size, tokens }) => {\n  const pop = spring({\n    frame: Math.max(0, frame - enteredAt),\n    fps,\n    config: SPRINGS.snap,\n    durationInFrames: 10,\n  });\n  const scale = isActive ? 0.94 + pop * 0.16 : 0.78;\n  const dim = interpolate(isActive ? 1 : 0, [0, 1], [0.42, 1]);\n\n  return (\n    <div\n      style={{\n        transform: `scale(${scale})`,\n        opacity: dim,\n        backgroundColor: isActive ? tokens.accent : \"transparent\",\n        borderRadius: size * 0.16,\n        padding: isActive ? `${size * 0.06}px ${size * 0.18}px` : `${size * 0.06}px 0`,\n      }}\n    >\n      <span\n        style={{\n          fontFamily: tokens.fontDisplay,\n          fontSize: size,\n          fontWeight: 700,\n          letterSpacing: size * -0.028,\n          lineHeight: 1,\n          color: isActive ? tokens.accentInk : tokens.ink,\n          textShadow: isActive ? \"none\" : `0 ${size * 0.03}px ${size * 0.06}px rgba(0,0,0,0.55)`,\n          textTransform: \"uppercase\",\n        }}\n      >\n        {word}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"karaoke-captions\",\n  name: \"Karaoke Captions\",\n  category: \"social\",\n  summary:\n    \"Word-timed captions where the spoken word pops onto an accent block and its neighbours dim, in a fixed-height row that never reflows\",\n  tags: [\"social\", \"captions\", \"karaoke\", \"vertical\", \"subtitles\"],\n  budget: BUDGETS.feature,\n  aspect: \"9:16\",\n  schema: karaokeCaptionsSchema,\n  defaults: {\n    words: [\n      \"Motion\",\n      \"design\",\n      \"that\",\n      \"ships\",\n      \"as\",\n      \"code\",\n      \"you\",\n      \"actually\",\n      \"own\",\n      \"and\",\n      \"can\",\n      \"edit\",\n    ],\n    perWord: 9,\n    window: 3,\n    brand: \"slate\",\n  },\n  component: KaraokeCaptions,\n  file: \"src/registry/items/karaoke-captions.tsx\",\n  origin: {\n    prompt:\n      \"TikTok-style word-timed captions in the lower third, where the current word is scaled up in the accent colour with a block behind it and the neighbouring words are smaller and muted.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "TikTok-style word-timed captions in the lower third, where the current word is scaled up in the accent colour with a block behind it and the neighbouring words are smaller and muted.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2096,
      "props": [
        {
          "name": "words",
          "type": "array",
          "description": "The spoken words, in order. One caption word each.",
          "required": true,
          "defaultValue": "12 items",
          "options": null
        },
        {
          "name": "perWord",
          "type": "number",
          "description": "Frames per word. Around 9 at 30fps matches natural speech pace.",
          "required": false,
          "defaultValue": "9",
          "options": null
        },
        {
          "name": "window",
          "type": "number",
          "description": "Words visible at once. Three is the readable maximum on a phone.",
          "required": false,
          "defaultValue": "3",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "kinetic-headline",
      "name": "Kinetic Headline",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "Words rise through a hard clipping mask, staggered, with an accent rule drawing in beneath",
      "tags": [
        "headline",
        "kinetic type",
        "mask reveal",
        "stagger",
        "hero"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/kinetic-headline.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const kineticHeadlineSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"The headline itself. Two to eight words reads best; longer wraps and loses impact.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase label above the headline. Omit for a cleaner card.\"),\n  align: z\n    .enum([\"left\", \"center\"])\n    .default(\"center\")\n    .describe(\"Text alignment. 'left' suits 9:16 and dense copy.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n  rule: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw an accent rule under the headline.\"),\n});\n\ntype Props = z.infer<typeof kineticHeadlineSchema>;\n\nconst KineticHeadline: React.FC<Props> = ({ headline, eyebrow, align, brand, rule }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n  const words = headline.split(/\\s+/).filter(Boolean);\n\n  // Type scale is derived from frame width so one component serves every aspect.\n  const size = width * (words.length > 5 ? 0.062 : 0.082);\n\n  // The rule draws after the last word has begun arriving.\n  const ruleStart = 6 + words.length * 3;\n  const ruleScale = interpolate(stage.frame, [ruleStart, ruleStart + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n        padding: `0 ${width * 0.08}px`,\n      }}\n    >\n      <div style={{ opacity: stage.presence, textAlign: align }}>\n        {eyebrow ? (\n          <Eyebrow label={eyebrow} color={t.accent} font={t.fontMono} size={size * 0.17} />\n        ) : null}\n\n        <div\n          style={{\n            display: \"flex\",\n            flexWrap: \"wrap\",\n            gap: `0 ${size * 0.26}px`,\n            justifyContent: align === \"center\" ? \"center\" : \"flex-start\",\n          }}\n        >\n          {words.map((word, i) => (\n            <Word key={`${word}-${i}`} word={word} index={i} size={size} color={t.ink} font={t.fontDisplay} />\n          ))}\n        </div>\n\n        {rule ? (\n          <div\n            style={{\n              marginTop: size * 0.34,\n              height: Math.max(2, size * 0.035),\n              width: size * 3.2,\n              backgroundColor: t.accent,\n              transform: `scaleX(${ruleScale})`,\n              transformOrigin: align === \"center\" ? \"center\" : \"left\",\n              marginLeft: align === \"center\" ? \"auto\" : 0,\n              marginRight: align === \"center\" ? \"auto\" : 0,\n            }}\n          />\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Eyebrow: React.FC<{ label: string; color: string; font: string; size: number }> = ({\n  label,\n  color,\n  font,\n  size,\n}) => {\n  const stage = useStage(BUDGETS.statement);\n  const y = interpolate(stage.enter, [0, 1], [10, 0]);\n  return (\n    <div\n      style={{\n        fontFamily: font,\n        fontSize: size,\n        fontWeight: 600,\n        letterSpacing: size * 0.16,\n        textTransform: \"uppercase\",\n        color,\n        marginBottom: size * 1.1,\n        transform: `translateY(${y}px)`,\n        opacity: stage.enter,\n      }}\n    >\n      {label}\n    </div>\n  );\n};\n\n/**\n * One word, revealed by a clipping mask it rises through. The mask is a real\n * overflow-hidden box rather than a gradient fade, which keeps glyph edges\n * crisp at 4K — the thing this whole approach exists to protect.\n */\nconst Word: React.FC<{\n  word: string;\n  index: number;\n  size: number;\n  color: string;\n  font: string;\n}> = ({ word, index, size, color, font }) => {\n  const stage = useStaggeredStage(BUDGETS.statement, index, 3);\n  const y = interpolate(stage.enter, [0, 1], [100, 0]);\n  return (\n    <span style={{ display: \"block\", overflow: \"hidden\", paddingBottom: size * 0.06 }}>\n      <span\n        style={{\n          display: \"block\",\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 700,\n          lineHeight: 1.02,\n          letterSpacing: size * -0.022,\n          color,\n          transform: `translateY(${y}%)`,\n        }}\n      >\n        {word}\n      </span>\n    </span>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"kinetic-headline\",\n  name: \"Kinetic Headline\",\n  category: \"typography\",\n  summary: \"Words rise through a hard clipping mask, staggered, with an accent rule drawing in beneath\",\n  tags: [\"headline\", \"kinetic type\", \"mask reveal\", \"stagger\", \"hero\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: kineticHeadlineSchema,\n  defaults: {\n    headline: \"Words, to videos\",\n    eyebrow: \"Motionstash\",\n    align: \"center\",\n    brand: \"slate\",\n    rule: true,\n  },\n  component: KineticHeadline,\n  file: \"src/registry/items/kinetic-headline.tsx\",\n  origin: {\n    prompt:\n      \"A hero headline where each word rises up through a hard-edged mask, staggered by a few frames, with a thin accent rule drawing in underneath after the last word lands.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A hero headline where each word rises up through a hard-edged mask, staggered by a few frames, with a thin accent rule drawing in underneath after the last word lands.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3127,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "The headline itself. Two to eight words reads best; longer wraps and loses impact.",
          "required": true,
          "defaultValue": "Words, to videos",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase label above the headline. Omit for a cleaner card.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "description": "Text alignment. 'left' suits 9:16 and dense copy.",
          "required": false,
          "defaultValue": "center",
          "options": [
            "left",
            "center"
          ]
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        },
        {
          "name": "rule",
          "type": "boolean",
          "description": "Draw an accent rule under the headline.",
          "required": false,
          "defaultValue": "true",
          "options": null
        }
      ]
    },
    {
      "id": "kpi-tile-grid",
      "name": "KPI Tile Grid",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Stat tiles fade up in a three-frame stagger, counting up in four number formats with delta chips and drawn sparklines",
      "tags": [
        "kpi",
        "dashboard",
        "counter",
        "sparkline",
        "stagger",
        "metrics"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/kpi-tile-grid.tsx",
      "source": "/**\n * KPI Tile Grid — a board of stat tiles that arrives as one staggered gesture.\n *\n * Three decisions carry this component:\n *\n *  1. **The formatter is the feature.** A metrics board is only as good as its\n *     figures, so `formatMetric` returns typographic *parts* — a quiet prefix, a\n *     big tabular body, a quiet suffix — rather than one string. Currency marks,\n *     percent signs, compact magnitudes and units are all set at half size in\n *     `inkMuted`, which is what makes four differently-formatted numbers read as\n *     one column of data instead of four unrelated strings. The defaults ship one\n *     tile of each format on purpose.\n *  2. **Rise and fall tones are derived, never literal.** A delta chip wants\n *     \"good\" and \"bad\" colour, and this registry only gives a component two\n *     accents. So the tones are picked by measuring which accent sits nearer\n *     green on the hue wheel: that one carries the rise, the other the fall.\n *     Every brand lands somewhere sensible — Slate gets cyan up / amber down,\n *     Press gets blue up / vermilion down, Field gets olive up / rust down — and\n *     no hex ever enters this file.\n *  3. **Direction and sentiment are separate.** A falling latency is good news.\n *     The triangle follows the *sign* of the delta; the tone follows the\n *     *polarity* the tile declares. That is the difference between a chart that\n *     is decorated and one that is read.\n *\n * Geometry is derived from the tile box rather than the frame, and the tile box\n * is clamped by both available width and available height, so the same component\n * composes a 4-across row at 16:9 and a 2x2 board at 1:1 and 9:16.\n */\nimport { evolvePath } from \"@remotion/paths\";\nimport { makeTriangle } from \"@remotion/shapes\";\nimport React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\n/* ------------------------------------------------------------------- schema */\n\nexport const kpiTileSchema = z.object({\n  label: z\n    .string()\n    .describe(\"What the metric is. Set in mono caps, so two or three words — 'Net revenue', 'Median latency'.\"),\n  value: z\n    .number()\n    .describe(\"The figure the tile lands on. The count always starts at zero, so this is the whole shot.\"),\n  format: z\n    .enum([\"plain\", \"currency\", \"percent\", \"compact\"])\n    .default(\"plain\")\n    .describe(\n      \"How the figure is set. 'plain' groups thousands (1,284); 'currency' adds the currency mark; 'percent' appends %; 'compact' shortens to 12.4k / 3.1M / 1.2B. Mixing formats across tiles is the point of the component.\",\n    ),\n  decimals: z\n    .number()\n    .int()\n    .min(0)\n    .max(2)\n    .default(0)\n    .describe(\"Decimal places. 0 for money and counts, 1 for percentages and ratios — decimals read as precision, so spend them deliberately.\"),\n  unit: z\n    .string()\n    .optional()\n    .describe(\"Small trailing unit for 'plain' and 'currency' tiles — 'ms', 'hrs', 'seats'. Ignored by 'percent' and 'compact', which supply their own mark.\"),\n  delta: z\n    .number()\n    .describe(\"Change against the comparison period, in percent. The sign points the triangle: 12.4 draws an up-triangle, -9.2 a down-triangle.\"),\n  polarity: z\n    .enum([\"up-good\", \"down-good\"])\n    .default(\"up-good\")\n    .describe(\n      \"Which direction is favourable. 'down-good' is for metrics you want falling — latency, churn, cost per unit — and tints a downward delta with the rise tone instead of the fall tone.\",\n    ),\n  series: z\n    .array(z.number())\n    .min(2)\n    .describe(\"Trailing values for the sparkline, oldest first. Eight to sixteen points reads best; the line is scaled to its own min and max, so raw units are fine.\"),\n});\n\nexport const kpiTileGridSchema = z.object({\n  title: z\n    .string()\n    .describe(\"Board headline, top left. Four to six words — this is the sentence the numbers are evidence for.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small accent label above the title. The report's name or cadence: 'Board update', 'Week 34'.\"),\n  period: z\n    .string()\n    .optional()\n    .describe(\"Right-aligned mono line on the title's baseline — the window the figures cover, e.g. '01 Jul — 30 Sep'.\"),\n  tiles: z\n    .array(kpiTileSchema)\n    .min(2)\n    .max(6)\n    .describe(\n      \"The metrics, in reading order. Four is the designed case and fills a 16:9 row; two or three widen, five or six wrap to a three-across grid.\",\n    ),\n  currencySymbol: z\n    .string()\n    .default(\"$\")\n    .describe(\"Mark set before every 'currency' tile, at half size and quiet. '$', '€', '£', 'CHF'.\"),\n  sparklines: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the trailing sparkline in each tile. Turn it off for a denser, figure-only board.\"),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono line at the bottom left, under a hairline — the methodology or comparison basis. One short sentence.\"),\n  source: z\n    .string()\n    .optional()\n    .describe(\"Right-aligned attribution opposite the footnote — the team or system the numbers came from.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof kpiTileGridSchema>;\ntype TileSpec = z.infer<typeof kpiTileSchema>;\n\n/* -------------------------------------------------------------- timing plan */\n\nconst BUDGET = BUDGETS.scene;\n/** Frames each tile lags behind the one before it. Three is one readable beat. */\nconst STAGGER = 3;\n/**\n * The count runs past the enter window and into the hold. That is the one thing\n * the budget explicitly allows: presence is finished in `enter`, but a counter\n * ticking is internal motion, and a figure that lands too early reads as a\n * static slide rather than a measurement.\n */\nconst COUNT_FRAMES = BUDGET.enter + 26;\n/** The sparkline starts a beat after the tile has arrived, and draws under the count. */\nconst SPARK_FROM = 5;\nconst SPARK_FRAMES = 30;\n/** The accent gesture: the chip lands just as the figure settles. */\nconst CHIP_FROM = COUNT_FRAMES - 6;\nconst CHIP_FRAMES = 10;\n\n/* ------------------------------------------------------------ colour derivation */\n\nconst hexToRgb = (hex: string): [number, number, number] => {\n  const raw = hex.trim().replace(\"#\", \"\");\n  const full = raw.length === 3 ? raw.split(\"\").map((c) => c + c).join(\"\") : raw.slice(0, 6);\n  const n = Number.parseInt(full, 16);\n  if (!Number.isFinite(n)) return [128, 128, 128];\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n};\n\n/** A token colour at partial alpha — for chip fills and the area under a sparkline. */\nconst withAlpha = (hex: string, alpha: number) => {\n  const [r, g, b] = hexToRgb(hex);\n  return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, alpha)).toFixed(3)})`;\n};\n\nconst hueOf = (hex: string) => {\n  const [r, g, b] = hexToRgb(hex).map((c) => c / 255);\n  const max = Math.max(r, g, b);\n  const min = Math.min(r, g, b);\n  const chroma = max - min;\n  if (chroma === 0) return 0;\n  const h =\n    max === r ? ((g - b) / chroma) % 6 : max === g ? (b - r) / chroma + 2 : (r - g) / chroma + 4;\n  const deg = h * 60;\n  return deg < 0 ? deg + 360 : deg;\n};\n\n/** Centre of the green sector on the hue wheel. */\nconst GREEN_HUE = 135;\n\nconst hueDistanceToGreen = (hue: number) => {\n  const d = Math.abs(hue - GREEN_HUE);\n  return Math.min(d, 360 - d);\n};\n\n/**\n * Which accent carries a rise, and which a fall. Whichever of the brand's two\n * accents sits nearer green takes the rise; the other takes the fall. Derived\n * rather than declared, so a new brand needs no change here and no component\n * ever spells out a colour.\n */\nconst deltaTones = (t: BrandTokens) => {\n  const accentIsGreener = hueDistanceToGreen(hueOf(t.accent)) <= hueDistanceToGreen(hueOf(t.accentAlt));\n  return accentIsGreener\n    ? { rise: t.accent, fall: t.accentAlt }\n    : { rise: t.accentAlt, fall: t.accent };\n};\n\n/* ----------------------------------------------------------------- formatter */\n\n/**\n * A figure, split for typesetting: the mark before it, the tabular body, the\n * mark after it. `suffixGapEm` is 0 for marks that belong tight to the number\n * (% and the k/M/B magnitudes) and a hair's breadth for real units.\n */\ntype MetricParts = { prefix: string; digits: string; suffix: string; suffixGapEm: number };\n\n/** Advance width of a glyph, in em of the body size. Separators are narrow. */\nconst NARROW = /[., −]/;\nconst DIGIT_EM = 0.62;\nconst NARROW_EM = 0.3;\n/** Prefix and suffix size, as a fraction of the body size. */\nconst AFFIX_EM = 0.52;\n\nconst groupThousands = (integer: string) => integer.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n\nconst fixedParts = (abs: number, decimals: number) => {\n  const fixed = abs.toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const integer = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fraction = dot === -1 ? \"\" : fixed.slice(dot + 1);\n  return groupThousands(integer) + (fraction ? `.${fraction}` : \"\");\n};\n\nconst trimTrailingZero = (s: string) => (s.includes(\".\") ? s.replace(/\\.0+$/, \"\") : s);\n\n/** 12,400 → 12.4k. Two significant-ish digits, because a tile is glanced at. */\nconst compactParts = (abs: number, decimals: number) => {\n  const tiers = [\n    { at: 1e9, div: 1e9, mark: \"B\" },\n    { at: 1e6, div: 1e6, mark: \"M\" },\n    { at: 1e3, div: 1e3, mark: \"k\" },\n  ];\n  for (const tier of tiers) {\n    if (abs >= tier.at) {\n      const scaled = abs / tier.div;\n      const places = scaled >= 100 ? 0 : Math.max(decimals, 1);\n      return { digits: trimTrailingZero(scaled.toFixed(places)), mark: tier.mark };\n    }\n  }\n  return { digits: fixedParts(abs, decimals), mark: \"\" };\n};\n\nconst formatMetric = (\n  value: number,\n  format: TileSpec[\"format\"],\n  decimals: number,\n  currency: string,\n  unit: string | undefined,\n): MetricParts => {\n  // A true minus, not a hyphen: a hyphen next to a 70px figure looks like a typo.\n  const sign = value < 0 ? \"−\" : \"\";\n  const abs = Math.abs(value);\n  const unitSuffix = unit ?? \"\";\n\n  if (format === \"currency\") {\n    return { prefix: currency, digits: sign + fixedParts(abs, decimals), suffix: unitSuffix, suffixGapEm: 0.11 };\n  }\n  if (format === \"percent\") {\n    return { prefix: \"\", digits: sign + fixedParts(abs, decimals), suffix: \"%\", suffixGapEm: 0 };\n  }\n  if (format === \"compact\") {\n    const { digits, mark } = compactParts(abs, decimals);\n    return { prefix: \"\", digits: sign + digits, suffix: mark, suffixGapEm: 0 };\n  }\n  return { prefix: \"\", digits: sign + fixedParts(abs, decimals), suffix: unitSuffix, suffixGapEm: 0.11 };\n};\n\n/** Estimated width of a set figure, in em of its body size. Drives the auto-fit. */\nconst metricEm = (parts: MetricParts) => {\n  const run = (s: string) => [...s].reduce((sum, ch) => sum + (NARROW.test(ch) ? NARROW_EM : DIGIT_EM), 0);\n  return (\n    run(parts.digits) +\n    AFFIX_EM * (run(parts.prefix) + run(parts.suffix)) +\n    (parts.suffix ? parts.suffixGapEm : 0)\n  );\n};\n\n/* ------------------------------------------------------------------ geometry */\n\ntype Geometry = {\n  padX: number;\n  padY: number;\n  gap: number;\n  cols: number;\n  rows: number;\n  hair: number;\n  tileW: number;\n  tileH: number;\n  tilePad: number;\n  innerW: number;\n  labelSize: number;\n  numberBase: number;\n  chipH: number;\n  chipFont: number;\n  sparkH: number;\n  eyebrowSize: number;\n  titleSize: number;\n  footSize: number;\n};\n\n/**\n * Everything is a function of the frame and the tile count. The tile height is\n * clamped by BOTH a proportion of its own width and the vertical space actually\n * left over, which is what stops a 2x2 board at 1:1 from pushing its sparklines\n * off the bottom of the frame.\n */\nconst layout = (width: number, height: number, count: number, hasFooter: boolean): Geometry => {\n  const landscape = width / height >= 1.15;\n  const cols = landscape ? (count <= 4 ? count : 3) : count <= 2 ? count : 2;\n  const rows = Math.ceil(count / cols);\n\n  const padX = width * 0.058;\n  const padY = height * 0.082;\n  const gap = width * 0.017;\n  const hair = Math.max(1, Math.round(width * 0.0009));\n  const tileW = (width - padX * 2 - gap * (cols - 1)) / cols;\n\n  const eyebrowSize = tileW * 0.048;\n  const titleSize = tileW * 0.135;\n  const footSize = tileW * 0.042;\n  // Header = eyebrow line + title line + the hairline under it and its air.\n  const headerH = eyebrowSize * 1.9 + titleSize * 1.85;\n  const footerH = hasFooter ? footSize * 3.2 : 0;\n  // Minimum air between header, grid and footer. Any surplus becomes more air.\n  const blockGap = height * 0.045;\n\n  const availH = height - padY * 2 - headerH - footerH - blockGap * 2 - gap * (rows - 1);\n  const tileH = Math.min(tileW * 1.18, availH / rows);\n\n  const tilePad = Math.min(tileW, tileH * 1.2) * 0.1;\n  /*\n   * The tile's own vertical budget. Every internal size is capped against it as\n   * well as against the tile width, and the four coefficients below are chosen\n   * so the worst case — label, figure, chip and sparkline all at their ceiling —\n   * comes to about 93% of `inner`. That is what keeps a 2x2 board at 1:1, where\n   * tiles are wide and short, from pushing its sparklines through the bottom\n   * edge, without any overflow clipping to hide the mistake.\n   */\n  const inner = Math.max(1, tileH - tilePad * 2);\n  const chipH = Math.min(tileW * 0.088, inner * 0.125);\n\n  return {\n    padX,\n    padY,\n    gap,\n    cols,\n    rows,\n    hair,\n    tileW,\n    tileH,\n    tilePad,\n    innerW: tileW - tilePad * 2,\n    labelSize: Math.min(tileW * 0.048, inner * 0.068),\n    numberBase: Math.min(tileW * 0.205, inner * 0.26),\n    chipH,\n    chipFont: chipH * 0.47,\n    sparkH: Math.min(tileW * 0.3, inner * 0.26),\n    eyebrowSize,\n    titleSize,\n    footSize,\n  };\n};\n\n/* --------------------------------------------------------------- component */\n\nconst KpiTileGrid: React.FC<Props> = ({\n  title,\n  eyebrow,\n  period,\n  tiles,\n  currencySymbol,\n  sparklines,\n  footnote,\n  source,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const hasFooter = Boolean(footnote || source);\n  const geo = layout(width, height, tiles.length, hasFooter);\n  const tones = deltaTones(t);\n\n  /*\n   * One figure size for the whole row, set by the widest figure in it. Letting\n   * each tile auto-fit independently would be easier and would look wrong: four\n   * numbers at four sizes stop reading as one set of measurements. So the row\n   * takes the smallest size any of its tiles needs, and the rest are optically\n   * equal. Sizing is done on the settled values, never the counting ones, so\n   * nothing resizes mid-count.\n   */\n  const numberSize = tiles.reduce((size, tile) => {\n    const settled = formatMetric(tile.value, tile.format, tile.decimals, currencySymbol, tile.unit);\n    return Math.min(size, geo.innerW / metricEm(settled));\n  }, geo.numberBase);\n\n  // The rule under the header draws left to right once the title has arrived —\n  // structure being laid down, not a flourish.\n  const ruleScale = interpolate(stage.frame, [4, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        boxSizing: \"border-box\",\n        padding: `${geo.padY}px ${geo.padX}px`,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"space-between\",\n        fontFamily: t.fontBody,\n      }}\n    >\n      <div\n        style={{\n          opacity: stage.presence,\n          transform: `translateY(${(1 - stage.enter) * geo.titleSize * 0.32}px)`,\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"flex-end\",\n            justifyContent: \"space-between\",\n            gap: geo.gap,\n          }}\n        >\n          <div>\n            {eyebrow ? (\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: geo.eyebrowSize,\n                  fontWeight: 600,\n                  letterSpacing: geo.eyebrowSize * 0.18,\n                  textTransform: \"uppercase\",\n                  color: t.accent,\n                  marginBottom: geo.eyebrowSize * 0.85,\n                }}\n              >\n                {eyebrow}\n              </div>\n            ) : null}\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: geo.titleSize,\n                fontWeight: 700,\n                lineHeight: 1.04,\n                letterSpacing: geo.titleSize * -0.024,\n                color: t.ink,\n              }}\n            >\n              {title}\n            </div>\n          </div>\n\n          {period ? (\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: geo.eyebrowSize * 0.6,\n                flexShrink: 0,\n                paddingBottom: geo.titleSize * 0.12,\n              }}\n            >\n              <div\n                style={{\n                  width: geo.eyebrowSize * 0.4,\n                  height: geo.eyebrowSize * 0.4,\n                  borderRadius: geo.eyebrowSize,\n                  backgroundColor: t.accent,\n                }}\n              />\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: geo.eyebrowSize * 0.95,\n                  letterSpacing: geo.eyebrowSize * 0.1,\n                  textTransform: \"uppercase\",\n                  color: t.inkMuted,\n                  whiteSpace: \"nowrap\",\n                }}\n              >\n                {period}\n              </div>\n            </div>\n          ) : null}\n        </div>\n\n        <div\n          style={{\n            height: geo.hair,\n            marginTop: geo.titleSize * 0.52,\n            backgroundColor: t.rule,\n            transform: `scaleX(${ruleScale})`,\n            transformOrigin: \"left\",\n          }}\n        />\n      </div>\n\n      <div\n        style={{\n          display: \"grid\",\n          gridTemplateColumns: `repeat(${geo.cols}, ${geo.tileW}px)`,\n          gap: geo.gap,\n          justifyContent: \"start\",\n        }}\n      >\n        {tiles.map((tile, i) => (\n          <Tile\n            key={`${tile.label}-${i}`}\n            tile={tile}\n            index={i}\n            t={t}\n            geo={geo}\n            tones={tones}\n            currency={currencySymbol}\n            numberSize={numberSize}\n            sparkline={sparklines}\n          />\n        ))}\n      </div>\n\n      {/*\n        With no footnote the board still needs a third block for `space-between`\n        to distribute against, otherwise the grid lands flush on the bottom\n        padding and the composition goes bottom-heavy.\n      */}\n      {hasFooter ? (\n        <div style={{ opacity: stage.presence }}>\n          <div style={{ height: geo.hair, backgroundColor: t.rule }} />\n          <div\n            style={{\n              display: \"flex\",\n              justifyContent: \"space-between\",\n              gap: geo.gap,\n              marginTop: geo.footSize * 0.95,\n              fontFamily: t.fontMono,\n              fontSize: geo.footSize,\n              color: t.inkMuted,\n            }}\n          >\n            <span style={{ letterSpacing: geo.footSize * 0.02 }}>{footnote ?? \"\"}</span>\n            <span\n              style={{\n                letterSpacing: geo.footSize * 0.12,\n                textTransform: \"uppercase\",\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {source ?? \"\"}\n            </span>\n          </div>\n        </div>\n      ) : (\n        <div />\n      )}\n    </AbsoluteFill>\n  );\n};\n\n/* -------------------------------------------------------------------- tile */\n\n/**\n * One tile. Fades up on its own staggered sub-stage, counts its figure through\n * the end of the enter window, draws its sparkline underneath, and lands its\n * delta chip last — so the eye is pulled label → figure → change.\n */\nconst Tile: React.FC<{\n  tile: TileSpec;\n  index: number;\n  t: BrandTokens;\n  geo: Geometry;\n  tones: { rise: string; fall: string };\n  currency: string;\n  /** Shared across the row — see the note where it is computed. */\n  numberSize: number;\n  sparkline: boolean;\n}> = ({ tile, index, t, geo, tones, currency, numberSize, sparkline }) => {\n  const stage = useStaggeredStage(BUDGET, index, STAGGER);\n\n  const count = interpolate(stage.frame, [0, COUNT_FRAMES], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const draw = interpolate(stage.frame, [SPARK_FROM, SPARK_FROM + SPARK_FRAMES], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const chip = interpolate(stage.frame, [CHIP_FROM, CHIP_FROM + CHIP_FRAMES], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n\n  const parts = formatMetric(tile.value * count, tile.format, tile.decimals, currency, tile.unit);\n\n  const flat = Math.abs(tile.delta) < 0.05;\n  const favourable = tile.polarity === \"up-good\" ? tile.delta > 0 : tile.delta < 0;\n  const tone = flat ? t.inkMuted : favourable ? tones.rise : tones.fall;\n\n  return (\n    <div\n      style={{\n        boxSizing: \"border-box\",\n        height: geo.tileH,\n        padding: geo.tilePad,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"space-between\",\n        backgroundColor: t.surface,\n        border: `${geo.hair}px solid ${t.rule}`,\n        borderRadius: t.radius,\n        opacity: stage.presence,\n        transform: `translateY(${(1 - stage.enter) * geo.tileH * 0.085}px)`,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: geo.labelSize,\n          fontWeight: 600,\n          letterSpacing: geo.labelSize * 0.14,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n        }}\n      >\n        {tile.label}\n      </div>\n\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"baseline\",\n          fontFamily: t.fontDisplay,\n          fontWeight: 700,\n          lineHeight: 1,\n          color: t.ink,\n          fontVariantNumeric: \"tabular-nums\",\n          marginTop: geo.labelSize * 0.9,\n        }}\n      >\n        {parts.prefix ? (\n          <span\n            style={{\n              fontSize: numberSize * AFFIX_EM,\n              color: t.inkMuted,\n              marginRight: numberSize * 0.07,\n            }}\n          >\n            {parts.prefix}\n          </span>\n        ) : null}\n        <span style={{ fontSize: numberSize, letterSpacing: numberSize * -0.026 }}>{parts.digits}</span>\n        {parts.suffix ? (\n          <span\n            style={{\n              fontSize: numberSize * AFFIX_EM,\n              color: t.inkMuted,\n              marginLeft: numberSize * parts.suffixGapEm,\n            }}\n          >\n            {parts.suffix}\n          </span>\n        ) : null}\n      </div>\n\n      <div style={{ marginTop: numberSize * 0.24 }}>\n        <DeltaChip\n          delta={tile.delta}\n          flat={flat}\n          tone={tone}\n          appear={chip}\n          height={geo.chipH}\n          fontSize={geo.chipFont}\n          font={t.fontMono}\n          hair={geo.hair}\n        />\n      </div>\n\n      {sparkline ? (\n        <div style={{ marginTop: geo.chipH * 0.6 }}>\n          <Sparkline\n            series={tile.series}\n            width={geo.innerW}\n            height={geo.sparkH}\n            draw={draw}\n            tone={tone}\n            rule={t.rule}\n            surface={t.surface}\n            hair={geo.hair}\n          />\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\n/* -------------------------------------------------------------------- chip */\n\n/**\n * The delta chip. Triangle from `makeTriangle` rather than a glyph, so it is the\n * same shape at any size and in any typeface. Rising chips arrive from below and\n * falling chips from above: things come from where they are going.\n */\nconst DeltaChip: React.FC<{\n  delta: number;\n  flat: boolean;\n  tone: string;\n  appear: number;\n  height: number;\n  fontSize: number;\n  font: string;\n  hair: number;\n}> = ({ delta, flat, tone, appear, height, fontSize, font, hair }) => {\n  const up = delta >= 0;\n  const marker = makeTriangle({ length: fontSize * 0.78, direction: up ? \"up\" : \"down\" });\n  const travel = (1 - appear) * height * 0.6 * (up ? 1 : -1);\n\n  return (\n    <div\n      style={{\n        display: \"inline-flex\",\n        alignItems: \"center\",\n        gap: height * 0.24,\n        height,\n        padding: `0 ${height * 0.4}px`,\n        borderRadius: height / 2,\n        backgroundColor: withAlpha(tone, 0.14),\n        border: `${hair}px solid ${withAlpha(tone, 0.34)}`,\n        opacity: Math.min(1, appear),\n        transform: `translateY(${travel}px)`,\n        whiteSpace: \"nowrap\",\n      }}\n    >\n      {flat ? (\n        <div style={{ width: fontSize * 0.7, height: Math.max(hair, fontSize * 0.1), backgroundColor: tone }} />\n      ) : (\n        <svg\n          width={marker.width}\n          height={marker.height}\n          viewBox={`0 0 ${marker.width} ${marker.height}`}\n          style={{ display: \"block\", overflow: \"visible\" }}\n        >\n          <path d={marker.path} fill={tone} />\n        </svg>\n      )}\n      <div\n        style={{\n          fontFamily: font,\n          fontSize,\n          fontWeight: 600,\n          letterSpacing: fontSize * 0.02,\n          color: tone,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        {`${Math.abs(delta).toFixed(1)}%`}\n      </div>\n    </div>\n  );\n};\n\n/* --------------------------------------------------------------- sparkline */\n\ntype Point = { x: number; y: number };\n\nconst round = (n: number) => Math.round(n * 100) / 100;\n\n/**\n * Catmull-Rom through the points, converted to cubics. A polyline is honest but\n * reads as a saw at tile scale; a smoothed curve reads as a trend, which is what\n * a sparkline is for. Tension stays below 1 so the curve never overshoots into\n * values the data does not contain.\n */\nconst smoothPath = (pts: Point[], tension: number) => {\n  let d = `M ${round(pts[0].x)} ${round(pts[0].y)}`;\n  for (let i = 0; i < pts.length - 1; i += 1) {\n    const p0 = pts[Math.max(0, i - 1)];\n    const p1 = pts[i];\n    const p2 = pts[i + 1];\n    const p3 = pts[Math.min(pts.length - 1, i + 2)];\n    const c1 = { x: p1.x + ((p2.x - p0.x) / 6) * tension, y: p1.y + ((p2.y - p0.y) / 6) * tension };\n    const c2 = { x: p2.x - ((p3.x - p1.x) / 6) * tension, y: p2.y - ((p3.y - p1.y) / 6) * tension };\n    d += ` C ${round(c1.x)} ${round(c1.y)} ${round(c2.x)} ${round(c2.y)} ${round(p2.x)} ${round(p2.y)}`;\n  }\n  return d;\n};\n\nconst Sparkline: React.FC<{\n  series: number[];\n  width: number;\n  height: number;\n  draw: number;\n  tone: string;\n  rule: string;\n  surface: string;\n  hair: number;\n}> = ({ series, width, height, draw, tone, rule, surface, hair }) => {\n  if (series.length < 2 || width <= 0 || height <= 0) return null;\n\n  const stroke = Math.max(hair * 1.3, height * 0.05);\n  // Inset the top and bottom so the stroke and the end cap stay inside the box.\n  const inset = stroke * 1.6;\n  const lo = Math.min(...series);\n  const hi = Math.max(...series);\n  const span = hi - lo || 1;\n  const plot = Math.max(1, height - inset * 2);\n\n  const pts: Point[] = series.map((v, i) => ({\n    x: (i / (series.length - 1)) * width,\n    y: inset + (1 - (v - lo) / span) * plot,\n  }));\n\n  const line = smoothPath(pts, 0.85);\n  const area = `${line} L ${round(width)} ${round(height)} L 0 ${round(height)} Z`;\n  const last = pts[pts.length - 1];\n  const dot = interpolate(draw, [0.88, 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  return (\n    <svg\n      width={width}\n      height={height}\n      viewBox={`0 0 ${width} ${height}`}\n      style={{ display: \"block\", overflow: \"visible\" }}\n    >\n      <path d={area} fill={withAlpha(tone, 0.12 * draw)} />\n      <line\n        x1={0}\n        y1={height - hair / 2}\n        x2={width}\n        y2={height - hair / 2}\n        stroke={rule}\n        strokeWidth={hair}\n      />\n      <path\n        d={line}\n        fill=\"none\"\n        stroke={tone}\n        strokeWidth={stroke}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        {...evolvePath(draw, line)}\n      />\n      <circle cx={last.x} cy={last.y} r={stroke * 1.9} fill={surface} opacity={dot} />\n      <circle cx={last.x} cy={last.y} r={stroke * 1.05} fill={tone} opacity={dot} />\n    </svg>\n  );\n};\n\n/* ---------------------------------------------------------------- registry */\n\nexport default defineMotionItem({\n  id: \"kpi-tile-grid\",\n  name: \"KPI Tile Grid\",\n  category: \"data\",\n  summary: \"Stat tiles fade up in a three-frame stagger, counting up in four number formats with delta chips and drawn sparklines\",\n  tags: [\"kpi\", \"dashboard\", \"counter\", \"sparkline\", \"stagger\", \"metrics\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: kpiTileGridSchema,\n  defaults: {\n    title: \"Q3 in four numbers\",\n    eyebrow: \"Board update\",\n    period: \"01 Jul — 30 Sep\",\n    tiles: [\n      {\n        label: \"Net revenue\",\n        value: 48200,\n        format: \"currency\",\n        decimals: 0,\n        delta: 12.4,\n        polarity: \"up-good\",\n        series: [31.2, 32.6, 31.9, 34.4, 36.1, 35.2, 37.8, 39.6, 41.2, 43.7, 46.1, 48.2],\n      },\n      {\n        label: \"Gross margin\",\n        value: 68.4,\n        format: \"percent\",\n        decimals: 1,\n        delta: 2.1,\n        polarity: \"up-good\",\n        series: [63.1, 63.8, 64.2, 63.6, 64.9, 65.4, 66.0, 65.7, 66.8, 67.3, 67.9, 68.4],\n      },\n      {\n        label: \"Active teams\",\n        value: 12400,\n        format: \"compact\",\n        decimals: 0,\n        delta: 18.6,\n        polarity: \"up-good\",\n        series: [7100, 7600, 8050, 8400, 8900, 9250, 9800, 10400, 10950, 11500, 11980, 12400],\n      },\n      {\n        label: \"Median latency\",\n        value: 214,\n        format: \"plain\",\n        decimals: 0,\n        unit: \"ms\",\n        delta: -9.2,\n        polarity: \"down-good\",\n        series: [268, 262, 271, 255, 249, 252, 241, 236, 240, 228, 219, 214],\n      },\n    ],\n    currencySymbol: \"$\",\n    sparklines: true,\n    footnote: \"Deltas versus the prior quarter · sparklines show trailing twelve weeks\",\n    source: \"Finance ops\",\n    brand: \"slate\",\n  },\n  component: KpiTileGrid,\n  file: \"src/registry/items/kpi-tile-grid.tsx\",\n  origin: {\n    prompt:\n      \"Four KPI tiles in a row on a dark board, staggered in a few frames apart. Each one has a small mono label in caps, a big number that counts up, a little delta chip with an up or down triangle, and a tiny sparkline that draws itself in. Use a different number format on each tile — currency, percent, compact, plain — and make a falling metric still read as good news.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Four KPI tiles in a row on a dark board, staggered in a few frames apart. Each one has a small mono label in caps, a big number that counts up, a little delta chip with an up or down triangle, and a tiny sparkline that draws itself in. Use a different number format on each tile — currency, percent, compact, plain — and make a falling metric still read as good news.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.33,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Board headline, top left. Four to six words — this is the sentence the numbers are evidence for.",
          "required": true,
          "defaultValue": "Q3 in four numbers",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small accent label above the title. The report's name or cadence: 'Board update', 'Week 34'.",
          "required": false,
          "defaultValue": "Board update",
          "options": null
        },
        {
          "name": "period",
          "type": "string",
          "description": "Right-aligned mono line on the title's baseline — the window the figures cover, e.g. '01 Jul — 30 Sep'.",
          "required": false,
          "defaultValue": "01 Jul — 30 Sep",
          "options": null
        },
        {
          "name": "tiles",
          "type": "array",
          "description": "The metrics, in reading order. Four is the designed case and fills a 16:9 row; two or three widen, five or six wrap to a three-across grid.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "currencySymbol",
          "type": "string",
          "description": "Mark set before every 'currency' tile, at half size and quiet. '$', '€', '£', 'CHF'.",
          "required": false,
          "defaultValue": "$",
          "options": null
        },
        {
          "name": "sparklines",
          "type": "boolean",
          "description": "Draw the trailing sparkline in each tile. Turn it off for a denser, figure-only board.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Quiet mono line at the bottom left, under a hairline — the methodology or comparison basis. One short sentence.",
          "required": false,
          "defaultValue": "Deltas versus the prior quarter · sparklines show trailin…",
          "options": null
        },
        {
          "name": "source",
          "type": "string",
          "description": "Right-aligned attribution opposite the footnote — the team or system the numbers came from.",
          "required": false,
          "defaultValue": "Finance ops",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "layer-stack-explode",
      "name": "Layer Stack Explode",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "An isometric stack separates plate by plate and holds as a labelled architecture diagram, with leader lines waiting for their layer to settle",
      "tags": [
        "diagram",
        "layers",
        "isometric",
        "architecture",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/layer-stack-explode.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const layerStackExplodeSchema = z.object({\n  layers: z\n    .array(z.string())\n    .min(3)\n    .max(6)\n    .describe(\"Top to bottom. Four or five reads best — more and the labels crowd.\"),\n  highlight: z\n    .number()\n    .min(-1)\n    .default(1)\n    .describe(\"Zero-based index rendered in the accent colour. Use -1 for none.\"),\n  spread: z\n    .number()\n    .min(0.4)\n    .max(1.6)\n    .default(1)\n    .describe(\"How far the layers separate. Lower keeps the stack compact.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof layerStackExplodeSchema>;\n\n/**\n * A stack that separates to show its structure, then holds as a readable diagram.\n *\n * The held frame is the deliverable — the explode is just how the viewer gets\n * there. So the separation eases to rest well before the hold window ends, and\n * leader lines only fade in once their layer has stopped moving; a label chasing\n * a moving plate is the thing that makes these look cheap.\n */\nconst LayerStackExplode: React.FC<Props> = ({ layers, highlight, spread, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const halfW = width * 0.19;\n  const halfD = halfW * 0.42;\n  const cx = width * 0.42;\n  const cy = height * 0.5;\n  const step = halfD * 1.5 * spread;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }}>\n        {layers.map((label, i) => {\n          const delay = 10 + i * 5;\n          const sep = interpolate(stage.frame, [delay, delay + 26], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: EASING.enter,\n          });\n          const offset = (i - (layers.length - 1) / 2) * step * sep;\n          const y = cy + offset;\n          const accent = i === highlight;\n          const pts = `${cx},${y - halfD} ${cx + halfW},${y} ${cx},${y + halfD} ${cx - halfW},${y}`;\n\n          // Labels wait for their own plate to settle.\n          const labelIn = interpolate(stage.frame, [delay + 22, delay + 34], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: EASING.enter,\n          });\n          const leadX = cx + halfW + width * 0.02;\n          const labelX = cx + halfW + width * 0.055;\n\n          return (\n            <g key={label}>\n              <polygon\n                points={pts}\n                fill={accent ? t.accent : t.surface}\n                fillOpacity={accent ? 0.22 : 0.9}\n                stroke={accent ? t.accent : t.rule}\n                strokeWidth={Math.max(1.2, width * 0.0013)}\n              />\n              <line\n                x1={cx + halfW}\n                y1={y}\n                x2={leadX + (labelX - leadX) * labelIn}\n                y2={y}\n                stroke={accent ? t.accent : t.rule}\n                strokeWidth={1}\n                opacity={labelIn}\n              />\n              <text\n                x={labelX + width * 0.006}\n                y={y + width * 0.005}\n                fill={accent ? t.accent : t.ink}\n                opacity={labelIn}\n                style={{ fontFamily: t.fontMono, fontSize: width * 0.0125 }}\n              >\n                {label}\n              </text>\n            </g>\n          );\n        })}\n      </svg>\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"layer-stack-explode\",\n  name: \"Layer Stack Explode\",\n  category: \"diagram\",\n  summary:\n    \"An isometric stack separates plate by plate and holds as a labelled architecture diagram, with leader lines waiting for their layer to settle\",\n  tags: [\"diagram\", \"layers\", \"isometric\", \"architecture\", \"explainer\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: layerStackExplodeSchema,\n  defaults: {\n    layers: [\"Interface\", \"Agent\", \"Composition\", \"Renderer\", \"Encoder\"],\n    highlight: 2,\n    spread: 1,\n    brand: \"slate\",\n  },\n  component: LayerStackExplode,\n  file: \"src/registry/items/layer-stack-explode.tsx\",\n  origin: {\n    prompt:\n      \"A stack of isometric layers that starts collapsed into one plane then separates vertically with a stagger, with labels and leader lines fading in beside each layer once it settles.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A stack of isometric layers that starts collapsed into one plane then separates vertically with a stagger, with labels and leader lines fading in beside each layer once it settles.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.1702,
      "props": [
        {
          "name": "layers",
          "type": "array",
          "description": "Top to bottom. Four or five reads best — more and the labels crowd.",
          "required": true,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "highlight",
          "type": "number",
          "description": "Zero-based index rendered in the accent colour. Use -1 for none.",
          "required": false,
          "defaultValue": "2",
          "options": null
        },
        {
          "name": "spread",
          "type": "number",
          "description": "How far the layers separate. Lower keeps the stack compact.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "light-rays-sweep",
      "name": "Light Rays Sweep",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "A fan of soft gradient wedges rotates slowly from an off-frame source, with a bloom at the origin and a vignette holding it back from the edges",
      "tags": [
        "background",
        "light",
        "rays",
        "cinematic",
        "atmosphere"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/light-rays-sweep.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const lightRaysSweepSchema = z.object({\n  rays: z.number().min(3).max(12).default(7).describe(\"Number of light wedges.\"),\n  intensity: z\n    .number()\n    .min(0.1)\n    .max(1)\n    .default(0.45)\n    .describe(\"Brightness of the rays. Keep it under 0.5 if foreground content sits on top.\"),\n  origin: z\n    .enum([\"top-left\", \"top-right\", \"top\"])\n    .default(\"top-left\")\n    .describe(\"Where the off-frame light source sits.\"),\n  sweep: z\n    .number()\n    .min(5)\n    .max(60)\n    .default(22)\n    .describe(\"Degrees the fan rotates across the shot. Small values read as slow and cinematic.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof lightRaysSweepSchema>;\n\nconst ORIGINS = {\n  \"top-left\": { x: -0.12, y: -0.16, base: 28 },\n  \"top-right\": { x: 1.12, y: -0.16, base: 152 },\n  top: { x: 0.5, y: -0.22, base: 90 },\n} as const;\n\nconst LightRaysSweep: React.FC<Props> = ({ rays, intensity, origin, sweep, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  const o = ORIGINS[origin];\n  const ox = width * o.x;\n  const oy = height * o.y;\n  const reach = Math.hypot(width, height) * 1.35;\n\n  // A single slow rotation of the whole fan, started and ended at rest.\n  const rotation = interpolate(stage.progress, [0, 1], [-sweep / 2, sweep / 2], {\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: ox,\n          top: oy,\n          width: 0,\n          height: 0,\n          transform: `rotate(${o.base + rotation}deg)`,\n          opacity: stage.presence * intensity,\n        }}\n      >\n        {Array.from({ length: rays }, (_, i) => {\n          const spread = 46;\n          const angle = -spread / 2 + (spread / Math.max(1, rays - 1)) * i;\n          const wobble = Math.sin(stage.progress * Math.PI * 2 + i) * 1.4;\n          const thickness = reach * (0.018 + random(`ray-w-${i}`) * 0.05);\n          const alpha = 0.35 + random(`ray-a-${i}`) * 0.65;\n          return (\n            <div\n              key={i}\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                width: reach,\n                height: thickness,\n                transformOrigin: \"0% 50%\",\n                transform: `rotate(${angle + wobble}deg) translateY(${-thickness / 2}px)`,\n                backgroundImage: `linear-gradient(90deg, ${t.accent}00 0%, ${t.accent} 8%, ${t.accent}00 88%)`,\n                opacity: alpha,\n                filter: `blur(${thickness * 0.35}px)`,\n              }}\n            />\n          );\n        })}\n      </div>\n\n      {/* Bloom at the source, and a vignette so the rays fall off into the frame. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: ox - width * 0.3,\n          top: oy - width * 0.3,\n          width: width * 0.6,\n          height: width * 0.6,\n          borderRadius: \"50%\",\n          backgroundImage: `radial-gradient(circle, ${t.accent}66 0%, ${t.accent}00 68%)`,\n          opacity: stage.presence * intensity * 1.1,\n          filter: `blur(${width * 0.02}px)`,\n        }}\n      />\n      <AbsoluteFill\n        style={{\n          backgroundImage: `radial-gradient(ellipse at 50% 55%, transparent 42%, ${t.bg} 100%)`,\n          opacity: 0.85,\n        }}\n      />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"light-rays-sweep\",\n  name: \"Light Rays Sweep\",\n  category: \"background\",\n  summary:\n    \"A fan of soft gradient wedges rotates slowly from an off-frame source, with a bloom at the origin and a vignette holding it back from the edges\",\n  tags: [\"background\", \"light\", \"rays\", \"cinematic\", \"atmosphere\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: lightRaysSweepSchema,\n  defaults: { rays: 7, intensity: 0.45, origin: \"top-left\", sweep: 22, brand: \"slate\" },\n  component: LightRaysSweep,\n  file: \"src/registry/items/light-rays-sweep.tsx\",\n  origin: {\n    prompt:\n      \"God rays sweeping slowly across the frame from a light source just off the top-left corner, with a soft bloom at the origin, cinematic and slow enough not to distract from foreground content.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "God rays sweeping slowly across the frame from a light source just off the top-left corner, with a soft bloom at the origin, cinematic and slow enough not to distract from foreground content.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3123,
      "props": [
        {
          "name": "rays",
          "type": "number",
          "description": "Number of light wedges.",
          "required": false,
          "defaultValue": "7",
          "options": null
        },
        {
          "name": "intensity",
          "type": "number",
          "description": "Brightness of the rays. Keep it under 0.5 if foreground content sits on top.",
          "required": false,
          "defaultValue": "0.45",
          "options": null
        },
        {
          "name": "origin",
          "type": "\"top-left\" | \"top-right\" | \"top\"",
          "description": "Where the off-frame light source sits.",
          "required": false,
          "defaultValue": "top-left",
          "options": [
            "top-left",
            "top-right",
            "top"
          ]
        },
        {
          "name": "sweep",
          "type": "number",
          "description": "Degrees the fan rotates across the shot. Small values read as slow and cinematic.",
          "required": false,
          "defaultValue": "22",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "line-chart-draw",
      "name": "Line Chart Draw",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "A smoothed trend line draws on behind a travelling dot, filling a soft area as it goes, then annotates its own peak",
      "tags": [
        "line chart",
        "draw-on",
        "area fill",
        "counter",
        "annotation",
        "svg path"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/line-chart-draw.tsx",
      "source": "import { evolvePath, getLength, getPointAtLength } from \"@remotion/paths\";\nimport React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const lineChartDrawSchema = z.object({\n  title: z\n    .string()\n    .describe(\n      \"The metric the chart is about, set large in the top-left. A noun phrase — 'Weekly active developers', not 'Chart of user growth'.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\n      \"Small uppercase mono label above the title — the dataset, the team, the source. Omit for a barer header.\",\n    ),\n  values: z\n    .array(z.number())\n    .min(2)\n    .describe(\n      \"The series, in order. 8–30 points reads best: fewer looks sparse once smoothed, many more turns the curve into texture. Any magnitude — the y scale is derived from the data.\",\n    ),\n  xLabels: z\n    .array(z.string())\n    .default([])\n    .describe(\n      \"Tick labels along the x axis, spread evenly across the series, so five labels can annotate twenty points. Set in mono — keep each to 3–6 characters. An empty array draws no x ticks.\",\n    ),\n  prefix: z\n    .string()\n    .default(\"\")\n    .describe(\"Set immediately before every figure, on the axis and on the travelling readout — a currency mark such as '$' or '€'.\"),\n  suffix: z\n    .string()\n    .default(\"\")\n    .describe(\"Set immediately after every figure — '%', 'k', 'M', 'ms'. Much cheaper than scaling the numbers themselves.\"),\n  decimals: z\n    .number()\n    .int()\n    .min(0)\n    .max(2)\n    .default(0)\n    .describe(\n      \"Decimal places on the travelling readout. 1 gives the counter something to do across a small range; 0 for large figures.\",\n    ),\n  baseline: z\n    .enum([\"zero\", \"auto\"])\n    .default(\"zero\")\n    .describe(\n      \"'zero' anchors the y axis at zero — honest for volumes, and the right choice whenever the area is filled. 'auto' zooms to the data range, which suits rates and indices but exaggerates every slope.\",\n    ),\n  gridLines: z\n    .number()\n    .int()\n    .min(2)\n    .max(8)\n    .default(5)\n    .describe(\n      \"Roughly how many horizontal gridlines to draw. The scale snaps to whole 1/2/5 steps, so the count you get back can differ by one.\",\n    ),\n  area: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Fill a soft accent gradient under the curve, wiped in behind the leading edge. Turn it off for a purer line, or when an 'auto' baseline makes the fill meaningless.\",\n    ),\n  peakLabel: z\n    .string()\n    .optional()\n    .describe(\n      \"Caption for the callout that lands on the maximum once the line finishes drawing — 'Peak', 'All-time high', 'Launch week'. The value is appended for you. Omit to leave the peak unannotated.\",\n    ),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono line in the top-right corner: the period, the unit, the source. A few words — it is set on one line.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof lineChartDrawSchema>;\n\n/**\n * Advance width of one mono glyph, in em. Declared rather than measured: a\n * render has no layout pass to ask, so every pill, gutter and callout in this\n * component is sized arithmetically from the string length. Geist Mono (and\n * every fallback in the token's stack) sits at 0.6em.\n */\nconst MONO_EM = 0.6;\n\n/** Catmull-Rom tension. 0.9 is smooth without inventing wobble between points. */\nconst TENSION = 0.9;\n\ntype Pt = { x: number; y: number };\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(Math.max(hi, lo), Math.max(lo, v));\nconst r2 = (n: number) => Math.round(n * 100) / 100;\n\n/**\n * Catmull-Rom through every data point, emitted as cubic Béziers.\n *\n * The control points are clamped into each segment's own vertical range, which\n * is the difference between a smooth chart and a lying one: an unclamped spline\n * dips below a local minimum and overshoots above a local maximum, drawing\n * values the data never contained.\n */\nconst smoothPath = (pts: Pt[]): string => {\n  if (pts.length < 2) return \"\";\n  let d = `M ${r2(pts[0].x)} ${r2(pts[0].y)}`;\n  for (let i = 0; i < pts.length - 1; i += 1) {\n    const p0 = i > 0 ? pts[i - 1] : pts[i];\n    const p1 = pts[i];\n    const p2 = pts[i + 1];\n    const p3 = i + 2 < pts.length ? pts[i + 2] : p2;\n    const k = TENSION / 6;\n    const lo = Math.min(p1.y, p2.y);\n    const hi = Math.max(p1.y, p2.y);\n    const c1x = p1.x + (p2.x - p0.x) * k;\n    const c1y = clamp(p1.y + (p2.y - p0.y) * k, lo, hi);\n    const c2x = p2.x - (p3.x - p1.x) * k;\n    const c2y = clamp(p2.y - (p3.y - p1.y) * k, lo, hi);\n    d += ` C ${r2(c1x)} ${r2(c1y)} ${r2(c2x)} ${r2(c2y)} ${r2(p2.x)} ${r2(p2.y)}`;\n  }\n  return d;\n};\n\n/** Round a rough step up to the nearest 1, 2 or 5 decade — the D3 tick ladder. */\nconst niceStep = (rough: number) => {\n  const exp = Math.floor(Math.log10(rough));\n  const base = 10 ** exp;\n  const err = rough / base;\n  const mult = err >= 7.07 ? 10 : err >= 3.16 ? 5 : err >= 1.41 ? 2 : 1;\n  return mult * base;\n};\n\n/** Decimal places a step needs so its ticks read exactly: 2 → 0, 2.5 → 1. */\nconst decimalsOf = (n: number) => {\n  const s = String(Math.round(n * 1000) / 1000);\n  const dot = s.indexOf(\".\");\n  return dot === -1 ? 0 : s.length - dot - 1;\n};\n\ntype Scale = { min: number; max: number; step: number };\n\n/**\n * A y scale on round numbers. Charts are read off their gridlines, so the\n * gridlines get the whole values and the curve takes whatever height is left.\n */\nconst buildScale = (values: number[], ticks: number, zero: boolean): Scale => {\n  const dataMin = Math.min(...values);\n  const dataMax = Math.max(...values);\n  const spread = dataMax - dataMin;\n  const lo = zero ? Math.min(0, dataMin) : dataMin - spread * 0.15;\n  const hi = zero ? dataMax : dataMax + spread * 0.15;\n  const step = niceStep(Math.max(1e-9, (hi - lo) / Math.max(1, ticks - 1)));\n  return {\n    min: Math.floor(lo / step) * step,\n    max: Math.ceil(hi / step) * step,\n    step,\n  };\n};\n\nconst tickValues = ({ min, max, step }: Scale): number[] => {\n  const count = Math.min(24, Math.max(1, Math.round((max - min) / step)));\n  return Array.from({ length: count + 1 }, (_, i) => min + i * step);\n};\n\n/** Group the integer part in threes. Figures in a chart are read, not scanned. */\nconst grouped = (int: string) => int.replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\n\nconst fmt = (v: number, decimals: number, prefix: string, suffix: string) => {\n  const fixed = Math.abs(v).toFixed(decimals);\n  const [int = \"0\", frac] = fixed.split(\".\");\n  const body = frac ? `${grouped(int)}.${frac}` : grouped(int);\n  return `${v < 0 ? \"−\" : \"\"}${prefix}${body}${suffix}`;\n};\n\nconst anchorFor = (i: number, n: number): \"start\" | \"middle\" | \"end\" =>\n  i === 0 ? \"start\" : i === n - 1 ? \"end\" : \"middle\";\n\nconst LineChartDraw: React.FC<Props> = ({\n  title,\n  eyebrow,\n  values,\n  xLabels,\n  prefix,\n  suffix,\n  decimals,\n  baseline,\n  gridLines,\n  area,\n  peakLabel,\n  footnote,\n  brand,\n}) => {\n  /*\n   * The gradient and the sweep clip are referenced by id, so two instances of\n   * this component in one frame would fight over them. useId is the only stable\n   * per-instance name available in a render — sanitised because React 19 wraps\n   * it in characters that are hostile inside url(#...).\n   */\n  const uid = `lcd${React.useId().replace(/[^a-zA-Z0-9]/g, \"\")}`;\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const budget = BUDGETS.scene;\n  const stage = useStage(budget);\n\n  const series = values.length >= 2 ? values : [values[0] ?? 0, values[0] ?? 0];\n\n  /*\n   * Every size derives from the geometric mean of the frame rather than its\n   * width, so a 9:16 cut keeps the same optical type size instead of shrinking\n   * the axis labels to a hairline.\n   */\n  const unit = Math.sqrt(width * height);\n  const padX = width * 0.062;\n  const padTop = height * 0.085;\n  const padBottom = height * 0.072;\n  const hair = Math.max(1, unit * 0.001);\n  const eyebrowSize = unit * 0.0142;\n  const titleSize = unit * 0.039;\n  const axisSize = unit * 0.0139;\n  const readoutSize = unit * 0.0207;\n  const calloutSize = unit * 0.0142;\n\n  /* ------------------------------------------------------------ the scale */\n\n  const scale = buildScale(series, gridLines, baseline === \"zero\");\n  const ticks = tickValues(scale);\n  const tickDecimals = decimalsOf(scale.step);\n  const tickLabels = ticks.map((v) => fmt(v, tickDecimals, prefix, suffix));\n\n  /* ---------------------------------------------------------- the geometry */\n\n  /* The left gutter is sized from the longest axis label, not guessed. */\n  const gutterL =\n    Math.max(...tickLabels.map((l) => l.length)) * axisSize * MONO_EM + axisSize * 1.25;\n  const gutterB = xLabels.length > 0 ? axisSize * 2.6 : axisSize * 0.6;\n  /*\n   * Headroom above the plot, sized so the tallest thing that has to sit above a\n   * point still fits when that point is at the very top of the scale — the\n   * callout if there is one, the travelling readout otherwise. Without it the\n   * annotation flips below the curve and covers the data.\n   */\n  const topInset = peakLabel ? calloutSize * 4.7 : readoutSize * 2.95;\n\n  const headerH = (eyebrow ? eyebrowSize * 1.9 : 0) + titleSize * 1.12;\n  const ruleGap = height * 0.034;\n  const chartGap = height * 0.045;\n  const svgW = width - padX * 2;\n  const svgH = Math.max(\n    unit * 0.2,\n    height - padTop - padBottom - headerH - ruleGap - chartGap - hair,\n  );\n\n  const plotL = gutterL;\n  const plotR = svgW;\n  const plotT = topInset;\n  const plotB = svgH - gutterB;\n  const plotW = Math.max(1, plotR - plotL);\n  const plotH = Math.max(1, plotB - plotT);\n  const span = Math.max(1e-9, scale.max - scale.min);\n\n  const xOf = (i: number) => plotL + (i / (series.length - 1)) * plotW;\n  const yOf = (v: number) => plotB - ((v - scale.min) / span) * plotH;\n  const points: Pt[] = series.map((v, i) => ({ x: xOf(i), y: yOf(v) }));\n\n  const linePath = smoothPath(points);\n  const areaPath = `${linePath} L ${r2(plotR)} ${r2(plotB)} L ${r2(plotL)} ${r2(plotB)} Z`;\n  const lineLength = Math.max(1, getLength(linePath));\n  const lineWeight = Math.max(2, unit * 0.0029);\n\n  /* ----------------------------------------------------------- the timing */\n\n  /*\n   * Grid and axis labels come in on the budget's own enter window (staggered\n   * bottom-up inside GridLine). The curve then draws across two thirds of the\n   * hold, and the peak callout lands with time to spare before the exit — so\n   * the held frame is the finished chart, never a half-drawn one.\n   */\n  const drawIn = budget.enter + 6;\n  const drawOut = budget.enter + Math.round(budget.hold * 0.66);\n  const peakIn = drawOut + 7;\n\n  const draw = interpolate(stage.frame, [drawIn, drawOut], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const axisFade = interpolate(stage.frame, [8, 24], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const footnoteFade = interpolate(stage.frame, [5, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  /* ------------------------------------------------- the leading edge */\n\n  /*\n   * getPointAtLength on the same path evolvePath is dashing means the dot, the\n   * readout and the area's wipe edge are all at the identical arc length — the\n   * dot sits ON the end of the stroke rather than near it.\n   */\n  const head = getPointAtLength(linePath, lineLength * draw) ?? points[0];\n  /* Read the value back off the curve's y, so the counter tells the truth\n     between data points, dips included. */\n  const headValue = scale.min + ((plotB - head.y) / plotH) * span;\n  const drawFade = interpolate(stage.frame, [drawIn, drawIn + 6], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const dotR = unit * 0.0059;\n\n  /* ------------------------------------------------------------- the peak */\n\n  let peakIndex = 0;\n  for (let i = 1; i < series.length; i += 1) {\n    if (series[i] > series[peakIndex]) peakIndex = i;\n  }\n  const peakFade = interpolate(stage.frame, [peakIn, peakIn + 13], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const peakScale = interpolate(stage.frame, [peakIn, peakIn + 13], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n\n  const monoLabel = {\n    fontFamily: t.fontMono,\n    fontWeight: 500,\n    textTransform: \"uppercase\" as const,\n  };\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill\n        style={{\n          padding: `${padTop}px ${padX}px ${padBottom}px`,\n          display: \"flex\",\n          flexDirection: \"column\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* ------------------------------------------------------- header */}\n        <div\n          style={{\n            flex: \"0 0 auto\",\n            height: headerH,\n            display: \"flex\",\n            alignItems: \"flex-end\",\n            justifyContent: \"space-between\",\n            gap: unit * 0.03,\n            transform: `translateY(${(1 - stage.enter) * unit * 0.016}px)`,\n          }}\n        >\n          <div>\n            {eyebrow ? (\n              <div\n                style={{\n                  ...monoLabel,\n                  fontWeight: 600,\n                  fontSize: eyebrowSize,\n                  lineHeight: 1,\n                  letterSpacing: eyebrowSize * 0.18,\n                  color: t.accent,\n                  marginBottom: eyebrowSize * 0.9,\n                }}\n              >\n                {eyebrow}\n              </div>\n            ) : null}\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: titleSize,\n                fontWeight: 700,\n                lineHeight: 1.12,\n                letterSpacing: titleSize * -0.022,\n                color: t.ink,\n              }}\n            >\n              {title}\n            </div>\n          </div>\n\n          {footnote ? (\n            <div\n              style={{\n                ...monoLabel,\n                fontSize: axisSize,\n                lineHeight: 1.3,\n                letterSpacing: axisSize * 0.12,\n                color: t.inkMuted,\n                textAlign: \"right\",\n                maxWidth: svgW * 0.32,\n                opacity: footnoteFade,\n              }}\n            >\n              {footnote}\n            </div>\n          ) : null}\n        </div>\n\n        {/* Hairline tying the header to the plot, drawn left to right. */}\n        <div\n          style={{\n            flex: \"0 0 auto\",\n            marginTop: ruleGap,\n            height: hair,\n            backgroundColor: t.rule,\n            transform: `scaleX(${stage.enter})`,\n            transformOrigin: \"left\",\n          }}\n        />\n\n        {/* -------------------------------------------------------- plot */}\n        <svg\n          width={svgW}\n          height={svgH}\n          viewBox={`0 0 ${r2(svgW)} ${r2(svgH)}`}\n          style={{ flex: \"0 0 auto\", marginTop: chartGap, display: \"block\" }}\n        >\n          <defs>\n            {/* Anchored to the top of the ink rather than the top of the plot,\n                so the fill reads at full strength under the highest point\n                whatever headroom the scale left above it. */}\n            <linearGradient\n              id={`${uid}-area`}\n              x1={0}\n              y1={r2(yOf(Math.max(...series)))}\n              x2={0}\n              y2={plotB}\n              gradientUnits=\"userSpaceOnUse\"\n            >\n              <stop offset=\"0\" stopColor={t.accent} stopOpacity={0.3} />\n              <stop offset=\"0.55\" stopColor={t.accent} stopOpacity={0.1} />\n              <stop offset=\"1\" stopColor={t.accent} stopOpacity={0} />\n            </linearGradient>\n            {/* The area is revealed by a wipe pinned to the leading edge, since\n                a fill cannot be dashed on the way a stroke can. */}\n            <clipPath id={`${uid}-sweep`}>\n              <rect x={0} y={0} width={Math.max(0, head.x)} height={svgH} />\n            </clipPath>\n          </defs>\n\n          {ticks.map((v, i) => (\n            <GridLine\n              key={`tick-${i}`}\n              index={i}\n              y={yOf(v)}\n              left={plotL}\n              right={plotR}\n              label={tickLabels[i] ?? \"\"}\n              size={axisSize}\n              weight={i === 0 ? hair * 1.9 : hair}\n              rule={t.rule}\n              ink={t.inkMuted}\n              font={t.fontMono}\n            />\n          ))}\n\n          {xLabels.length > 0 ? (\n            <g\n              opacity={axisFade}\n              transform={`translate(0 ${r2((1 - axisFade) * axisSize * 0.6)})`}\n            >\n              {xLabels.map((label, i) => (\n                <text\n                  key={`x-${label}-${i}`}\n                  x={r2(\n                    xOf(\n                      xLabels.length === 1\n                        ? 0\n                        : Math.round((i / (xLabels.length - 1)) * (series.length - 1)),\n                    ),\n                  )}\n                  y={r2(plotB + gutterB * 0.72)}\n                  textAnchor={anchorFor(i, xLabels.length)}\n                  fill={t.inkMuted}\n                  style={{\n                    fontFamily: t.fontMono,\n                    fontSize: axisSize,\n                    fontWeight: 500,\n                    letterSpacing: axisSize * 0.1,\n                  }}\n                >\n                  {label}\n                </text>\n              ))}\n            </g>\n          ) : null}\n\n          {area ? (\n            <g clipPath={`url(#${uid}-sweep)`}>\n              <path d={areaPath} fill={`url(#${uid}-area)`} />\n            </g>\n          ) : null}\n\n          <path\n            d={linePath}\n            fill=\"none\"\n            stroke={t.accent}\n            strokeWidth={lineWeight}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            opacity={drawFade}\n            {...evolvePath(draw, linePath)}\n          />\n\n          {/* ------------------------------------------- travelling head */}\n          <g opacity={drawFade}>\n            <line\n              x1={r2(head.x)}\n              y1={r2(head.y + dotR * 2)}\n              x2={r2(head.x)}\n              y2={r2(plotB)}\n              stroke={t.accent}\n              strokeWidth={hair}\n              strokeDasharray={`${r2(hair * 3)} ${r2(hair * 4)}`}\n              opacity={0.4}\n            />\n            <circle\n              cx={r2(head.x)}\n              cy={r2(head.y)}\n              r={dotR}\n              fill={t.accent}\n              stroke={t.bg}\n              strokeWidth={hair * 2}\n            />\n            <Readout\n              x={head.x}\n              y={head.y}\n              gap={dotR + readoutSize * 0.75}\n              bounds={svgW}\n              text={fmt(headValue, decimals, prefix, suffix)}\n              size={readoutSize}\n              surface={t.surface}\n              rule={t.rule}\n              ink={t.ink}\n              font={t.fontMono}\n              radius={t.radius}\n              hair={hair}\n            />\n          </g>\n\n          {/* -------------------------------------------- peak annotation */}\n          {peakLabel ? (\n            <PeakCallout\n              x={xOf(peakIndex)}\n              y={yOf(series[peakIndex])}\n              bounds={svgW}\n              text={`${peakLabel.toUpperCase()} · ${fmt(series[peakIndex], decimals, prefix, suffix)}`}\n              size={calloutSize}\n              fade={peakFade}\n              scale={peakScale}\n              accent={t.accentAlt}\n              onAccent={t.bg}\n              font={t.fontMono}\n              radius={t.radius}\n              hair={hair}\n            />\n          ) : null}\n        </svg>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One gridline and its value. The line grows from the axis outwards and the\n * label fades with it, staggered bottom-up so the scale reads as stacking\n * rather than appearing — the chart's frame builds before any data lands.\n */\nconst GridLine: React.FC<{\n  index: number;\n  y: number;\n  left: number;\n  right: number;\n  label: string;\n  size: number;\n  weight: number;\n  rule: string;\n  ink: string;\n  font: string;\n}> = ({ index, y, left, right, label, size, weight, rule, ink, font }) => {\n  const stage = useStaggeredStage(BUDGETS.scene, index, 2);\n  const grow = stage.enter;\n  return (\n    <g>\n      <line\n        x1={r2(left)}\n        y1={r2(y)}\n        x2={r2(left + (right - left) * grow)}\n        y2={r2(y)}\n        stroke={rule}\n        strokeWidth={weight}\n      />\n      <text\n        x={r2(left - size * 0.95)}\n        y={r2(y)}\n        textAnchor=\"end\"\n        dominantBaseline=\"middle\"\n        fill={ink}\n        opacity={grow}\n        style={{ fontFamily: font, fontSize: size, fontWeight: 500, letterSpacing: size * 0.04 }}\n      >\n        {label}\n      </text>\n    </g>\n  );\n};\n\n/**\n * The value that rides the leading edge. Centred over the dot and clamped into\n * the plot, so it never leaves the frame at either end of the sweep, and\n * flipped below the curve if the curve is running too close to the top.\n */\nconst Readout: React.FC<{\n  x: number;\n  y: number;\n  gap: number;\n  bounds: number;\n  text: string;\n  size: number;\n  surface: string;\n  rule: string;\n  ink: string;\n  font: string;\n  radius: number;\n  hair: number;\n}> = ({ x, y, gap, bounds, text, size, surface, rule, ink, font, radius, hair }) => {\n  const boxW = text.length * size * MONO_EM + size * 1.1;\n  const boxH = size * 1.72;\n  const boxX = clamp(x - boxW / 2, 0, Math.max(0, bounds - boxW));\n  const above = y - gap - boxH;\n  const boxY = above >= 0 ? above : y + gap;\n  return (\n    <g>\n      <rect\n        x={r2(boxX)}\n        y={r2(boxY)}\n        width={r2(boxW)}\n        height={r2(boxH)}\n        rx={Math.min(boxH / 2, radius * 0.7)}\n        fill={surface}\n        stroke={rule}\n        strokeWidth={hair}\n      />\n      <text\n        x={r2(boxX + boxW / 2)}\n        y={r2(boxY + boxH / 2)}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill={ink}\n        style={{ fontFamily: font, fontSize: size, fontWeight: 500, letterSpacing: size * 0.01 }}\n      >\n        {text}\n      </text>\n    </g>\n  );\n};\n\n/**\n * The one accent gesture: once the curve is down, a ring pops out of the\n * maximum and a leader lifts a small pill above it. Scaled about the peak\n * point itself so the whole annotation reads as having come from the data.\n */\nconst PeakCallout: React.FC<{\n  x: number;\n  y: number;\n  bounds: number;\n  text: string;\n  size: number;\n  fade: number;\n  scale: number;\n  accent: string;\n  onAccent: string;\n  font: string;\n  radius: number;\n  hair: number;\n}> = ({ x, y, bounds, text, size, fade, scale, accent, onAccent, font, radius, hair }) => {\n  const boxW = text.length * size * MONO_EM + size * 1.5;\n  const boxH = size * 1.95;\n  const ringR = size * 0.5;\n  const lift = size * 2.4;\n  /* Above the peak by default; below it when the peak is against the ceiling. */\n  const flip = y - lift - boxH < 0;\n  const boxY = flip ? y + lift : y - lift - boxH;\n  const boxX = clamp(x - boxW / 2, 0, Math.max(0, bounds - boxW));\n  const leaderFrom = flip ? y + ringR * 1.8 : y - ringR * 1.8;\n  const leaderTo = flip ? boxY : boxY + boxH;\n  return (\n    <g\n      opacity={fade}\n      transform={`translate(${r2(x)} ${r2(y)}) scale(${r2(scale)}) translate(${r2(-x)} ${r2(-y)})`}\n    >\n      <line\n        x1={r2(x)}\n        y1={r2(leaderFrom)}\n        x2={r2(x)}\n        y2={r2(leaderTo)}\n        stroke={accent}\n        strokeWidth={hair}\n        strokeDasharray={`${r2(hair * 3)} ${r2(hair * 3)}`}\n      />\n      <circle\n        cx={r2(x)}\n        cy={r2(y)}\n        r={r2(ringR)}\n        fill=\"none\"\n        stroke={accent}\n        strokeWidth={hair * 2.4}\n      />\n      <rect\n        x={r2(boxX)}\n        y={r2(boxY)}\n        width={r2(boxW)}\n        height={r2(boxH)}\n        rx={Math.min(boxH / 2, radius * 0.7)}\n        fill={accent}\n      />\n      <text\n        x={r2(boxX + boxW / 2)}\n        y={r2(boxY + boxH / 2)}\n        textAnchor=\"middle\"\n        dominantBaseline=\"middle\"\n        fill={onAccent}\n        style={{\n          fontFamily: font,\n          fontSize: size,\n          fontWeight: 600,\n          letterSpacing: size * 0.1,\n        }}\n      >\n        {text}\n      </text>\n    </g>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"line-chart-draw\",\n  name: \"Line Chart Draw\",\n  category: \"data\",\n  summary:\n    \"A smoothed trend line draws on behind a travelling dot, filling a soft area as it goes, then annotates its own peak\",\n  tags: [\"line chart\", \"draw-on\", \"area fill\", \"counter\", \"annotation\", \"svg path\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: lineChartDrawSchema,\n  defaults: {\n    title: \"Weekly active developers\",\n    eyebrow: \"Platform telemetry\",\n    values: [1.7, 2, 1.9, 2.3, 2.8, 2.6, 3.2, 3.7, 3.5, 4.1, 4.8, 5.4, 5.2, 6.1, 6.9, 7.8, 7.4, 7, 7.2, 7.5],\n    xLabels: [\"JAN\", \"APR\", \"JUL\", \"OCT\", \"JAN\"],\n    prefix: \"\",\n    suffix: \"k\",\n    decimals: 1,\n    baseline: \"zero\",\n    gridLines: 5,\n    area: true,\n    peakLabel: \"Peak\",\n    footnote: \"12-month rolling · thousands\",\n    brand: \"slate\",\n  },\n  component: LineChartDraw,\n  file: \"src/registry/items/line-chart-draw.tsx\",\n  origin: {\n    prompt:\n      \"A line chart that draws itself on: compute a smooth path from a values array, draw the stroke with evolvePath, wipe a soft area gradient in behind the leading edge, and carry a dot with a value label that counts up as it travels. Gridlines and axis labels first at hairline weight, then a small callout on the peak once the line lands.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A line chart that draws itself on: compute a smooth path from a values array, draw the stroke with evolvePath, wipe a soft area gradient in behind the leading edge, and carry a dot with a value label that counts up as it travels. Gridlines and axis labels first at hairline weight, then a small callout on the peak once the line lands.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3839,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "The metric the chart is about, set large in the top-left. A noun phrase — 'Weekly active developers', not 'Chart of user growth'.",
          "required": true,
          "defaultValue": "Weekly active developers",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label above the title — the dataset, the team, the source. Omit for a barer header.",
          "required": false,
          "defaultValue": "Platform telemetry",
          "options": null
        },
        {
          "name": "values",
          "type": "array",
          "description": "The series, in order. 8–30 points reads best: fewer looks sparse once smoothed, many more turns the curve into texture. Any magnitude — the y scale is derived from the data.",
          "required": true,
          "defaultValue": "20 items",
          "options": null
        },
        {
          "name": "xLabels",
          "type": "array",
          "description": "Tick labels along the x axis, spread evenly across the series, so five labels can annotate twenty points. Set in mono — keep each to 3–6 characters. An empty array draws no x ticks.",
          "required": false,
          "defaultValue": "5 items",
          "options": null
        },
        {
          "name": "prefix",
          "type": "string",
          "description": "Set immediately before every figure, on the axis and on the travelling readout — a currency mark such as '$' or '€'.",
          "required": false,
          "defaultValue": "",
          "options": null
        },
        {
          "name": "suffix",
          "type": "string",
          "description": "Set immediately after every figure — '%', 'k', 'M', 'ms'. Much cheaper than scaling the numbers themselves.",
          "required": false,
          "defaultValue": "k",
          "options": null
        },
        {
          "name": "decimals",
          "type": "number",
          "description": "Decimal places on the travelling readout. 1 gives the counter something to do across a small range; 0 for large figures.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "baseline",
          "type": "\"zero\" | \"auto\"",
          "description": "'zero' anchors the y axis at zero — honest for volumes, and the right choice whenever the area is filled. 'auto' zooms to the data range, which suits rates and indices but exaggerates every slope.",
          "required": false,
          "defaultValue": "zero",
          "options": [
            "zero",
            "auto"
          ]
        },
        {
          "name": "gridLines",
          "type": "number",
          "description": "Roughly how many horizontal gridlines to draw. The scale snaps to whole 1/2/5 steps, so the count you get back can differ by one.",
          "required": false,
          "defaultValue": "5",
          "options": null
        },
        {
          "name": "area",
          "type": "boolean",
          "description": "Fill a soft accent gradient under the curve, wiped in behind the leading edge. Turn it off for a purer line, or when an 'auto' baseline makes the fill meaningless.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "peakLabel",
          "type": "string",
          "description": "Caption for the callout that lands on the maximum once the line finishes drawing — 'Peak', 'All-time high', 'Launch week'. The value is appended for you. Omit to leave the peak unannotated.",
          "required": false,
          "defaultValue": "Peak",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Quiet mono line in the top-right corner: the period, the unit, the source. A few words — it is set on one line.",
          "required": false,
          "defaultValue": "12-month rolling · thousands",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "liquid-chrome-wordmark",
      "name": "Liquid Chrome Wordmark",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "A wordmark uncovered by a soft-edged wipe in flowing chrome, finished with one hard light streak and a floor reflection",
      "tags": [
        "logo",
        "wordmark",
        "chrome",
        "specular",
        "reflection",
        "reveal",
        "premium"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/liquid-chrome-wordmark.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const liquidChromeWordmarkSchema = z.object({\n  wordmark: z\n    .string()\n    .describe(\"The mark itself. One or two words — this is set enormous and tracked tight.\"),\n  tagline: z\n    .string()\n    .optional()\n    .describe(\"Line beneath the mark. Arrives after the light streak, in flat ink rather than chrome.\"),\n  flow: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.45)\n    .describe(\"How fast the metal appears to move. 0 is a still polished surface; above 0.7 reads as liquid rather than solid.\"),\n  streak: z\n    .boolean()\n    .default(true)\n    .describe(\"Fire a hard light streak across the mark at the moment it completes.\"),\n  reflection: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the mark's reflection in the floor beneath it.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof liquidChromeWordmarkSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.8\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * The chrome fill.\n *\n * Metal is not a gradient from dark to light — it is a stack of ALTERNATING\n * bands, because a reflective surface samples a whole environment: sky above,\n * horizon in the middle, ground below, with hard transitions between them. Nine\n * stops with abrupt reversals is what reads as chrome; three smooth stops reads\n * as plastic. One band carries a trace of the accent colour, standing in for a\n * coloured light in the room — without it the mark looks like a stock effect.\n */\nconst chromeFill = (t: typeof DEFAULT_BRAND) =>\n  `linear-gradient(176deg,\n    ${alpha(t.ink, 0.42)} 0%,\n    ${alpha(\"#fff\", 0.92)} 11%,\n    ${alpha(t.ink, 0.66)} 21%,\n    ${alpha(t.rule, 0.9)} 31%,\n    ${alpha(\"#fff\", 0.98)} 44%,\n    ${alpha(t.accent, 0.72)} 51%,\n    ${alpha(t.ink, 0.34)} 58%,\n    ${alpha(\"#fff\", 0.86)} 71%,\n    ${alpha(t.rule, 0.95)} 80%,\n    ${alpha(t.ink, 0.58)} 90%,\n    ${alpha(\"#fff\", 0.5)} 100%)`;\n\n/**\n * The mark, as a single text layer with a chrome fill.\n *\n * Reused verbatim for the floor reflection, which is the reason it takes its\n * geometry as props: two copies of one component cannot drift apart, and a\n * reflection that does not match its object is worse than no reflection.\n */\nconst ChromeText: React.FC<{\n  text: string;\n  size: number;\n  flowY: number;\n  t: typeof DEFAULT_BRAND;\n}> = ({ text, size, flowY, t }) => (\n  <div\n    style={{\n      fontFamily: t.fontDisplay,\n      fontSize: size,\n      fontWeight: 700,\n      lineHeight: 1,\n      letterSpacing: size * -0.042,\n      whiteSpace: \"nowrap\",\n      color: \"transparent\",\n      backgroundImage: chromeFill(t),\n      // Tall background, scrolled vertically: the bands slide through the\n      // letterforms instead of the letterforms moving.\n      backgroundSize: \"100% 340%\",\n      backgroundPosition: `0 ${flowY}%`,\n      WebkitBackgroundClip: \"text\",\n      backgroundClip: \"text\",\n    }}\n  >\n    {text}\n  </div>\n);\n\nconst LiquidChromeWordmark: React.FC<Props> = ({\n  wordmark,\n  tagline,\n  flow,\n  streak,\n  reflection,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const size = width * (wordmark.length > 9 ? 0.115 : 0.165);\n\n  /**\n   * The reveal.\n   *\n   * A wipe with a soft leading edge, not a fade: chrome has to be *uncovered* to\n   * read as a physical object, and the soft edge is what stops the wipe looking\n   * like a rectangle passing over text. The wipe is a mask, so the metal\n   * underneath is already flowing before it is visible.\n   */\n  const wipe = interpolate(frame, [2, 34], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const wipeEdge = interpolate(wipe, [0, 1], [-12, 118]);\n\n  // Metal keeps moving through the hold — a still frame of chrome looks painted.\n  const flowY = (frame / fps) * flow * 46;\n\n  /** One hard streak at completion, with a bloom that decays faster than it. */\n  const streakT = interpolate(frame, [32, 56], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const streakLive = streak && streakT > 0 && streakT < 1;\n  const flash = interpolate(frame, [34, 40, 52], [0, 0.5, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const tagIn = interpolate(frame, [46, 68], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.035]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* The room the metal is reflecting: a broad key light above, a cooler\n          bounce below. Without an environment, chrome has nothing to be. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(90% 55% at 50% -6%, ${alpha(t.surface, 0.95)} 0%, ${alpha(\n            t.surface,\n            0.4,\n          )} 34%, ${alpha(t.bg, 0.7)} 64%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(70% 40% at 50% 108%, ${alpha(t.accent, 0.2)} 0%, ${alpha(\n            t.accent,\n            0,\n          )} 62%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          transform: `scale(${push})`,\n          opacity: stage.presence,\n        }}\n      >\n        <div style={{ position: \"relative\" }}>\n          {/* Bloom: a blurred copy behind the mark, screened in. Real specular\n              highlights spill past their own edges. */}\n          <div\n            aria-hidden\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              filter: `blur(${size * 0.09}px)`,\n              opacity: 0.5 * wipe,\n              mixBlendMode: \"screen\",\n              WebkitMaskImage: `linear-gradient(96deg, #000 ${wipeEdge - 10}%, ${alpha(\n                \"#000\",\n                0,\n              )} ${wipeEdge + 4}%)`,\n              maskImage: `linear-gradient(96deg, #000 ${wipeEdge - 10}%, ${alpha(\"#000\", 0)} ${\n                wipeEdge + 4\n              }%)`,\n            }}\n          >\n            <ChromeText text={wordmark} size={size} flowY={flowY} t={t} />\n          </div>\n\n          <div\n            style={{\n              position: \"relative\",\n              WebkitMaskImage: `linear-gradient(96deg, #000 ${wipeEdge - 10}%, ${alpha(\n                \"#000\",\n                0.4,\n              )} ${wipeEdge - 2}%, ${alpha(\"#000\", 0)} ${wipeEdge + 5}%)`,\n              maskImage: `linear-gradient(96deg, #000 ${wipeEdge - 10}%, ${alpha(\"#000\", 0.4)} ${\n                wipeEdge - 2\n              }%, ${alpha(\"#000\", 0)} ${wipeEdge + 5}%)`,\n            }}\n          >\n            <ChromeText text={wordmark} size={size} flowY={flowY} t={t} />\n\n            {/* The streak, clipped to the glyphs. */}\n            {streakLive ? (\n              <div\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  mixBlendMode: \"screen\",\n                  fontFamily: t.fontDisplay,\n                  fontSize: size,\n                  fontWeight: 700,\n                  lineHeight: 1,\n                  letterSpacing: size * -0.042,\n                  whiteSpace: \"nowrap\",\n                  color: \"transparent\",\n                  backgroundImage: `linear-gradient(102deg, ${alpha(\"#fff\", 0)} 40%, ${alpha(\n                    \"#fff\",\n                    0.35,\n                  )} 47%, #fff 50%, ${alpha(\"#fff\", 0.35)} 53%, ${alpha(\"#fff\", 0)} 60%)`,\n                  backgroundSize: \"240% 100%\",\n                  backgroundPosition: `${interpolate(streakT, [0, 1], [-110, 210])}% 0`,\n                  WebkitBackgroundClip: \"text\",\n                  backgroundClip: \"text\",\n                }}\n              >\n                {wordmark}\n              </div>\n            ) : null}\n          </div>\n\n          {/* Floor reflection — flipped, blurred, and dying into the floor. */}\n          {reflection ? (\n            <div\n              aria-hidden\n              style={{\n                position: \"absolute\",\n                top: \"100%\",\n                left: 0,\n                right: 0,\n                height: size * 1.1,\n                marginTop: size * 0.06,\n                overflow: \"hidden\",\n                opacity: 0.2 * wipe,\n                transform: \"scaleY(-1)\",\n                transformOrigin: \"50% 0%\",\n                filter: `blur(${size * 0.018}px)`,\n                WebkitMaskImage: `linear-gradient(to top, ${alpha(\"#000\", 0)} 0%, ${alpha(\n                  \"#000\",\n                  0.3,\n                )} 52%, #000 100%)`,\n                maskImage: `linear-gradient(to top, ${alpha(\"#000\", 0)} 0%, ${alpha(\n                  \"#000\",\n                  0.3,\n                )} 52%, #000 100%)`,\n              }}\n            >\n              <ChromeText text={wordmark} size={size} flowY={flowY} t={t} />\n            </div>\n          ) : null}\n        </div>\n\n        {tagline ? (\n          <div\n            style={{\n              marginTop: size * 0.66,\n              fontFamily: t.fontBody,\n              fontSize: size * 0.115,\n              fontWeight: 400,\n              letterSpacing: size * 0.012,\n              color: t.inkMuted,\n              opacity: tagIn,\n              transform: `translateY(${interpolate(tagIn, [0, 1], [size * 0.08, 0])}px)`,\n            }}\n          >\n            {tagline}\n          </div>\n        ) : null}\n      </div>\n\n      {/* The flash from the streak, thrown across the whole frame. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          opacity: flash,\n          background: `radial-gradient(60% 42% at 50% 48%, ${alpha(\"#fff\", 0.5)} 0%, ${alpha(\n            \"#fff\",\n            0,\n          )} 72%)`,\n          mixBlendMode: \"screen\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(120% 88% at 50% 44%, ${alpha(\"#000\", 0)} 30%, ${alpha(\n            \"#000\",\n            0.18,\n          )} 62%, ${alpha(\"#000\", 0.5)} 92%, ${alpha(\"#000\", 0.66)} 100%)`,\n        }}\n      />\n      <Grain id=\"liquid-chrome-grain\" frame={frame} opacity={0.075} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"liquid-chrome-wordmark\",\n  name: \"Liquid Chrome Wordmark\",\n  category: \"brand\",\n  summary:\n    \"A wordmark uncovered by a soft-edged wipe in flowing chrome, finished with one hard light streak and a floor reflection\",\n  tags: [\"logo\", \"wordmark\", \"chrome\", \"specular\", \"reflection\", \"reveal\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: liquidChromeWordmarkSchema,\n  defaults: {\n    wordmark: \"MOTIONSTASH\",\n    tagline: \"Code-based motion design, for anything that renders\",\n    flow: 0.45,\n    streak: true,\n    reflection: true,\n    brand: \"circuit\",\n  },\n  component: LiquidChromeWordmark,\n  file: \"src/registry/items/liquid-chrome-wordmark.tsx\",\n  origin: {\n    prompt:\n      \"A logo reveal in liquid chrome, the way a car or a phone gets its name on screen. The metal needs alternating hard bands, not a soft gradient, and it should keep flowing slowly the whole time. Uncover the mark with a soft-edged wipe rather than a fade, then fire one hard light streak across it with a bloom flash. Give it a blurred floor reflection and a real vignette.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A logo reveal in liquid chrome, the way a car or a phone gets its name on screen. The metal needs alternating hard bands, not a soft gradient, and it should keep flowing slowly the whole time. Uncover the mark with a soft-edged wipe rather than a fade, then fire one hard light streak across it with a bloom flash. Give it a blurred floor reflection and a real vignette.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7576,
      "props": [
        {
          "name": "wordmark",
          "type": "string",
          "description": "The mark itself. One or two words — this is set enormous and tracked tight.",
          "required": true,
          "defaultValue": "MOTIONSTASH",
          "options": null
        },
        {
          "name": "tagline",
          "type": "string",
          "description": "Line beneath the mark. Arrives after the light streak, in flat ink rather than chrome.",
          "required": false,
          "defaultValue": "Code-based motion design, for anything that renders",
          "options": null
        },
        {
          "name": "flow",
          "type": "number",
          "description": "How fast the metal appears to move. 0 is a still polished surface; above 0.7 reads as liquid rather than solid.",
          "required": false,
          "defaultValue": "0.45",
          "options": null
        },
        {
          "name": "streak",
          "type": "boolean",
          "description": "Fire a hard light streak across the mark at the moment it completes.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "reflection",
          "type": "boolean",
          "description": "Draw the mark's reflection in the floor beneath it.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "logo-marquee-row",
      "name": "Logo Marquee Row",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "A trusted-by wall of drawn marks gliding leftward between two hairlines on a mathematically exact loop, soft-masked at both frame edges",
      "tags": [
        "marquee",
        "logo wall",
        "trusted by",
        "seamless loop",
        "ticker",
        "social proof",
        "infinite scroll"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/logo-marquee-row.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/**\n * The eight marks, drawn rather than imported. Every one is built from the same\n * four primitives — circle, triangle, rounded rect, arc — in a different\n * arrangement, which is exactly how a real logo wall behaves: unrelated\n * companies, one visual weight.\n */\nconst MARKS = [\"ring\", \"prism\", \"bars\", \"arcs\", \"diamond\", \"lozenge\", \"pie\", \"quad\"] as const;\ntype MarkKind = (typeof MARKS)[number];\n\nexport const marqueeLogoSchema = z.object({\n  name: z\n    .string()\n    .describe(\n      \"Company name, set in uppercase mono beside the mark. One or two short words — nine characters or so reads best, and the strip's own width is computed from this string, so a long name simply makes that cell wider.\",\n    ),\n  mark: z\n    .enum(MARKS)\n    .default(\"ring\")\n    .describe(\n      \"Which abstract mark to draw. 'ring' is a circle with a satellite dot, 'prism' nested triangles, 'bars' three rounded columns, 'arcs' a signal fan, 'diamond' a rotated rounded square, 'lozenge' a capsule, 'pie' a three-quarter disc, 'quad' a two-by-two of dots. Vary them across the row — two identical marks side by side read as a mistake.\",\n    ),\n});\n\nexport const logoMarqueeRowSchema = z.object({\n  label: z\n    .string()\n    .default(\"Trusted by\")\n    .describe(\n      \"The quiet mono label above the strip. Two to four words: 'Trusted by', 'Shipping with', 'In production at'.\",\n    ),\n  logos: z\n    .array(marqueeLogoSchema)\n    .min(3)\n    .max(16)\n    .describe(\n      \"The wall, in reading order. Six to ten is the sweet spot. Note the trade: the strip travels its own full length over the clip, so a longer list is a faster glide — trim the list, or drop `showNames`, to slow it down.\",\n    ),\n  meta: z\n    .string()\n    .optional()\n    .describe(\n      \"Small mono line hung off the strip's bottom-right corner, e.g. 'Selected partners'. Omit for a barer frame.\",\n    ),\n  showNames: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Set the name beside each mark. False gives a marks-only wall — anonymous, tighter, and a slower loop because the pattern is shorter.\",\n    ),\n  spacing: z\n    .number()\n    .min(0.6)\n    .max(2)\n    .default(1)\n    .describe(\n      \"Multiplier on the gap between cells. Below 1 the wall reads as a dense ticker; above 1.4 as a sparse, luxury strip. It also scales the loop's length, and therefore its speed.\",\n    ),\n  cycles: z\n    .number()\n    .int()\n    .min(1)\n    .max(4)\n    .default(1)\n    .describe(\n      \"How many times the strip travels its own length across the clip. Whole numbers only — that integer is what makes the last frame land exactly back on the first frame's offset, so the clip can be looped without a jump. 1 is a calm glide, 2 a brisk ticker.\",\n    ),\n  edgeFade: z\n    .number()\n    .min(0)\n    .max(0.3)\n    .default(0.12)\n    .describe(\n      \"Width of the soft gradient mask at each end of the strip, as a fraction of the frame width. 0.12 is enough to swallow a cell; 0 gives a hard crop at the frame edge, which looks broken.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof logoMarqueeRowSchema>;\ntype Logo = z.infer<typeof marqueeLogoSchema>;\n\n/* ------------------------------------------------------------------ metrics */\n\ntype Metrics = {\n  /** The one governing number: the height of a mark's box. */\n  rowH: number;\n  font: number;\n  tracking: number;\n  /** Mark to name. */\n  markGap: number;\n  /** Trailing gap carried by every cell, including the one at the seam. */\n  cellGap: number;\n  bandH: number;\n  hair: number;\n  labelFont: number;\n  labelGap: number;\n  metaFont: number;\n  metaGap: number;\n  tickW: number;\n};\n\n/**\n * Every dimension is a ratio of the mark height, and the mark height is a ratio\n * of the frame's mean edge — `(width + height) / 2`, which is 1500 for both\n * 16:9 and 9:16. A mark is therefore the same physical size in a landscape cut\n * as in a vertical one; the vertical cut just shows fewer of them, which is the\n * correct answer for a marquee rather than a compromise.\n */\nconst metrics = (rowH: number, spacing: number): Metrics => {\n  const font = rowH * 0.32;\n  return {\n    rowH,\n    font,\n    tracking: font * 0.15,\n    markGap: rowH * 0.32,\n    cellGap: rowH * 1.05 * spacing,\n    bandH: rowH * 2.5,\n    hair: Math.max(1, rowH * 0.016),\n    labelFont: rowH * 0.24,\n    labelGap: rowH * 0.9,\n    metaFont: rowH * 0.2,\n    metaGap: rowH * 0.78,\n    tickW: rowH * 0.36,\n  };\n};\n\n/** Mono glyph advance, in ems. The 0.62 leaves slack for the frame before the webfont lands. */\nconst MONO_ADVANCE = 0.62;\n\n/**\n * The rendered width of a name, computed rather than measured.\n *\n * A marquee cannot be laid out by the browser: it needs to know its own pattern\n * width in JavaScript, before the first frame, so the wrap point can be an\n * exact number instead of a measurement that arrives one frame late. Mono\n * advances every glyph identically, so the width is arithmetic — which is the\n * real reason these names are set in mono. CSS also adds tracking *after* the\n * final glyph, so that last step is subtracted back off: what we want here is\n * the width of the ink, not of the box.\n */\nconst nameWidth = (name: string, m: Metrics) =>\n  Math.max(0, name.length * (m.font * MONO_ADVANCE + m.tracking) - m.tracking);\n\n/** Ink width of one cell: the mark, and the name if it is shown. */\nconst cellInk = (logo: Logo, m: Metrics, showNames: boolean) =>\n  m.rowH + (showNames && logo.name.trim().length > 0 ? m.markGap + nameWidth(logo.name, m) : 0);\n\ntype Layout = {\n  /** Left edge of each cell inside one copy of the pattern. */\n  at: number[];\n  /** Width of one full copy — the marquee's wrap distance. */\n  setWidth: number;\n};\n\n/**\n * Lay out one copy of the pattern. Each cell carries its own trailing gap, so\n * the space between the last cell and the first cell of the next copy is the\n * same as every interior gap. That single detail is what hides the seam: get it\n * wrong and the wall visibly breathes once per loop.\n */\nconst layoutSet = (logos: Logo[], m: Metrics, showNames: boolean): Layout => {\n  const at: number[] = [];\n  let x = 0;\n  for (const logo of logos) {\n    at.push(x);\n    x += cellInk(logo, m, showNames) + m.cellGap;\n  }\n  return { at, setWidth: Math.max(1, x) };\n};\n\n/* -------------------------------------------------------------- component */\n\n/**\n * Logo Marquee Row — the \"trusted by\" wall, on an exact loop.\n *\n * The composition is architecture plus one moving band. Two full-bleed\n * hairlines set the band into the frame; the label hangs off the top-left\n * corner behind a short accent dash and the meta line off the bottom-right, so\n * the frame is composed on a diagonal rather than stacked down the middle. At\n * rest it should read as a designed still with one thing in it that moves.\n *\n * **The loop.** The pattern is rendered `copies` times end to end and the whole\n * strip is translated left by `phase * setWidth`, where\n *\n *     phase = ((frame * cycles) / total) % 1\n *\n * `cycles` is an integer, so at `frame === total` — the frame after the last\n * one, which is frame 0 of the repeat — `phase` is exactly `cycles % 1`, i.e.\n * 0, the value it started at. Not approximately: the arithmetic is\n * `(total * cycles) / total`, which is exact in floating point. The wrap is\n * done in the phase rather than by accumulating pixels per frame precisely so\n * that no rounding can drift over the 240 frames and leave a half-pixel jump at\n * the splice.\n *\n * Speed is therefore not free: one whole traversal of the pattern is the\n * slowest exact loop that fits the budget. `cycles` buys multiples of it, and\n * the honest way to slow the wall down is to shorten the pattern.\n *\n * Motion is one gesture at three scales — everything horizontal grows from the\n * left. The accent dash, then the two hairlines a few frames apart, then the\n * cells themselves rising into place in a tight left-to-right stagger. The\n * marquee's own travel is continuous internal motion, so it is legal through\n * the hold and keeps running while presence fades on the exit.\n */\nconst LogoMarqueeRow: React.FC<Props> = ({\n  label,\n  logos,\n  meta,\n  showNames,\n  spacing,\n  cycles,\n  edgeFade,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const m = metrics(((width + height) / 2) * 0.062, spacing);\n  const { at, setWidth } = layoutSet(logos, m, showNames);\n\n  // See the note above: integer `cycles` makes frame `total` fold back onto\n  // frame 0's offset exactly.\n  const phase = (((stage.frame * cycles) / stage.total) % 1 + 1) % 1;\n  const shift = phase * setWidth;\n\n  // The strip is offset by at most one pattern width, so covering the frame\n  // takes one copy for the visible width plus one to feed the seam.\n  const copies = Math.ceil((width + setWidth) / setWidth);\n\n  // Two frames between cells is the readable range; a long wall compresses so\n  // the last cell still lands near the end of the enter window.\n  const perItem = Math.min(2, 10 / logos.length);\n\n  // The soft edges are a real alpha mask, not two scrims in the canvas colour:\n  // a mask fades the marks against whatever is behind the component, which\n  // matters the moment this row is composited over a background item. rgba\n  // black here is a mask channel, not a colour decision.\n  const inner = Math.min(0.49, Math.max(0, edgeFade));\n  const fade = [\n    \"linear-gradient(90deg\",\n    \"rgba(0,0,0,0) 0%\",\n    `rgba(0,0,0,1) ${(inner * 100).toFixed(3)}%`,\n    `rgba(0,0,0,1) ${((1 - inner) * 100).toFixed(3)}%`,\n    \"rgba(0,0,0,0) 100%)\",\n  ].join(\", \");\n\n  const padX = width * 0.075;\n\n  // Optical centring. The header carries more mass than the meta line, so\n  // centring the block's bounding box would sit the *band* below the frame's\n  // centre line. Padding the short side by the difference lifts the band onto\n  // it: the eye centres on the thing that moves, not on the outline of the\n  // whole composition.\n  const headerBlock = m.labelFont * 1.15 + m.labelGap;\n  const metaBlock = meta ? m.metaGap + m.metaFont * 1.15 : 0;\n  const optical = headerBlock - metaBlock;\n\n  const rise = interpolate(stage.enter, [0, 1], [m.rowH * 0.22, 0]);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        paddingTop: optical < 0 ? -optical : 0,\n        paddingBottom: optical > 0 ? optical : 0,\n      }}\n    >\n      <div style={{ opacity: stage.presence, transform: `translateY(${rise}px)` }}>\n        {/* Header. Padded in from the frame edge — the hairlines below are not. */}\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: m.labelFont * 0.85,\n            padding: `0 ${padX}px`,\n            marginBottom: m.labelGap,\n          }}\n        >\n          <Dash width={m.tickW} height={Math.max(2, m.hair * 2.6)} color={t.accent} />\n          <span\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: m.labelFont,\n              fontWeight: 600,\n              lineHeight: 1,\n              letterSpacing: m.labelFont * 0.2,\n              textTransform: \"uppercase\",\n              whiteSpace: \"nowrap\",\n              color: t.ink,\n            }}\n          >\n            {label}\n          </span>\n        </div>\n\n        <Hairline weight={m.hair} color={t.rule} delay={2} />\n\n        {/* The band. Clips the strip, and masks it soft at both ends. */}\n        <div\n          style={{\n            position: \"relative\",\n            height: m.bandH,\n            overflow: \"hidden\",\n            maskImage: fade,\n            WebkitMaskImage: fade,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              height: \"100%\",\n              transform: `translateX(${-shift}px)`,\n            }}\n          >\n            {Array.from({ length: copies }, (_, copy) =>\n              logos.map((logo, i) => (\n                <Cell\n                  // The stagger is keyed to the cell's index in the pattern, not\n                  // to its index in the flattened list, so every copy of a cell\n                  // animates identically. Anything else would make the seam\n                  // visible for the length of the entrance.\n                  key={`${copy}-${logo.name}-${i}`}\n                  logo={logo}\n                  index={i}\n                  perItem={perItem}\n                  x={copy * setWidth + at[i]}\n                  showName={showNames}\n                  m={m}\n                  t={t}\n                />\n              )),\n            )}\n          </div>\n        </div>\n\n        <Hairline weight={m.hair} color={t.rule} delay={6} />\n\n        {meta ? (\n          <div\n            style={{\n              display: \"flex\",\n              justifyContent: \"flex-end\",\n              padding: `0 ${padX}px`,\n              marginTop: m.metaGap,\n            }}\n          >\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: m.metaFont,\n                fontWeight: 500,\n                lineHeight: 1,\n                letterSpacing: m.metaFont * 0.18,\n                textTransform: \"uppercase\",\n                whiteSpace: \"nowrap\",\n                color: t.inkMuted,\n                // Tracking is added after the final glyph too, which pushes\n                // flush-right text off the margin by one step.\n                marginRight: -m.metaFont * 0.18,\n              }}\n            >\n              {meta}\n            </span>\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------- parts */\n\n/**\n * The accent dash in front of the label. The smallest member of the\n * grows-from-the-left family, and the only accent-coloured ink in the frame.\n */\nconst Dash: React.FC<{ width: number; height: number; color: string }> = ({\n  width,\n  height,\n  color,\n}) => {\n  const stage = useStage(BUDGETS.feature);\n  const run = interpolate(stage.frame, [0, 10], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  return (\n    <div\n      style={{\n        flexShrink: 0,\n        width,\n        height,\n        backgroundColor: color,\n        transform: `scaleX(${run})`,\n        transformOrigin: \"left\",\n      }}\n    />\n  );\n};\n\n/**\n * One of the two rules that set the band into the frame. Full bleed — it is\n * deliberately not masked at the edges like the strip is, because the rules are\n * the architecture and the marks are the content: the content passes through\n * the frame, the architecture holds it.\n */\nconst Hairline: React.FC<{ weight: number; color: string; delay: number }> = ({\n  weight,\n  color,\n  delay,\n}) => {\n  const stage = useStage(BUDGETS.feature);\n  const run = interpolate(stage.frame, [delay, delay + 18], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  return (\n    <div\n      style={{\n        width: \"100%\",\n        height: weight,\n        backgroundColor: color,\n        transform: `scaleX(${run})`,\n        transformOrigin: \"left\",\n      }}\n    />\n  );\n};\n\n/**\n * One wall cell: a drawn mark and its name, absolutely positioned at a\n * pre-computed x. Absolute rather than flex because the marquee owns the\n * arithmetic — the same numbers that decide the wrap point have to decide the\n * layout, or the two disagree by a fraction of a pixel and the seam shows.\n */\nconst Cell: React.FC<{\n  logo: Logo;\n  index: number;\n  perItem: number;\n  x: number;\n  showName: boolean;\n  m: Metrics;\n  t: BrandTokens;\n}> = ({ logo, index, perItem, x, showName, m, t }) => {\n  const stage = useStaggeredStage(BUDGETS.feature, index, perItem);\n\n  // Cells rise a little into the band as they arrive. Small: the wall's real\n  // motion is its travel, and a big entrance would compete with it.\n  const y = interpolate(stage.enter, [0, 1], [m.rowH * 0.26, 0]);\n  const opacity = interpolate(stage.enter, [0, 0.7], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: 0,\n        height: \"100%\",\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: m.markGap,\n        transform: `translateY(${y}px)`,\n        opacity,\n      }}\n    >\n      <Mark kind={logo.mark} size={m.rowH} color={t.inkMuted} />\n      {showName && logo.name.trim().length > 0 ? (\n        <span\n          style={{\n            fontFamily: t.fontMono,\n            fontSize: m.font,\n            fontWeight: 500,\n            lineHeight: 1,\n            letterSpacing: m.tracking,\n            textTransform: \"uppercase\",\n            whiteSpace: \"nowrap\",\n            // A degree brighter than the mark, so the pair reads as logotype\n            // plus symbol rather than as two grey things.\n            color: t.ink,\n            opacity: 0.82,\n            // Cap-height centring: uppercase ink sits high in its line box\n            // because the descender space beneath it is empty.\n            transform: `translateY(${m.font * 0.05}px)`,\n          }}\n        >\n          {logo.name}\n        </span>\n      ) : null}\n    </div>\n  );\n};\n\n/* ----------------------------------------------------------------- glyphs */\n\n/** Every mark is drawn in the same 32-unit box and scaled by the svg. */\nconst MARK_VIEW = 32;\n/** Stroke weight in viewBox units. One weight across all eight marks. */\nconst MARK_STROKE = 2.4;\n\n/**\n * The wall.\n *\n * Eight marks that have to look like eight different companies and like one\n * design system at the same time: same box, same stroke weight, same optical\n * mass, entirely different geometry. They are muted by construction — drawn in\n * `inkMuted`, never the accent — because a logo wall where one mark shouts is\n * not a logo wall, it is an advert.\n */\nconst Mark: React.FC<{ kind: MarkKind; size: number; color: string }> = ({\n  kind,\n  size,\n  color,\n}) => {\n  // Shared stroke setup, so a change of weight is a change in one place.\n  const s = {\n    stroke: color,\n    strokeWidth: MARK_STROKE,\n    fill: \"none\",\n    strokeLinecap: \"round\" as const,\n    strokeLinejoin: \"round\" as const,\n  };\n\n  return (\n    <svg\n      width={size}\n      height={size}\n      viewBox={`0 0 ${MARK_VIEW} ${MARK_VIEW}`}\n      style={{ display: \"block\", flexShrink: 0, overflow: \"visible\" }}\n    >\n      {kind === \"ring\" ? (\n        // A circle with a satellite sitting on its path.\n        <>\n          <circle cx={16} cy={16} r={11} {...s} />\n          <circle cx={23.8} cy={8.2} r={3.5} fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"prism\" ? (\n        // Outlined triangle with an inverted solid one nested inside it.\n        <>\n          <path d=\"M16 4.5 L28.6 26.8 L3.4 26.8 Z\" {...s} />\n          <path d=\"M9.8 15.4 L22.2 15.4 L16 26.2 Z\" fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"bars\" ? (\n        // Three rounded columns on a shared baseline.\n        <>\n          <rect x={5.5} y={17} width={5.4} height={10.5} rx={2.7} fill={color} />\n          <rect x={13.3} y={9.5} width={5.4} height={18} rx={2.7} fill={color} />\n          <rect x={21.1} y={4.5} width={5.4} height={23} rx={2.7} fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"arcs\" ? (\n        // A signal fan: three quarter arcs sharing a centre, plus its origin dot.\n        <>\n          <circle cx={7.5} cy={26} r={2.6} fill={color} />\n          <path d=\"M7.5 19.5 A 6.5 6.5 0 0 1 14 26\" {...s} />\n          <path d=\"M7.5 13 A 13 13 0 0 1 20.5 26\" {...s} />\n          <path d=\"M7.5 6.5 A 19.5 19.5 0 0 1 27 26\" {...s} />\n        </>\n      ) : null}\n\n      {kind === \"diamond\" ? (\n        // A rounded square stood on its corner, around a solid core.\n        <>\n          <rect\n            x={6.6}\n            y={6.6}\n            width={18.8}\n            height={18.8}\n            rx={4}\n            transform=\"rotate(45 16 16)\"\n            {...s}\n          />\n          <circle cx={16} cy={16} r={3.4} fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"lozenge\" ? (\n        // A capsule with the weight thrown to one end.\n        <>\n          <rect x={3.2} y={9.8} width={25.6} height={12.4} rx={6.2} {...s} />\n          <circle cx={10} cy={16} r={3.4} fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"pie\" ? (\n        // Three quarters of a disc, with the missing quadrant answered by a dot.\n        <>\n          <path d=\"M16 16 L16 3.5 A 12.5 12.5 0 1 1 3.5 16 Z\" fill={color} />\n          <circle cx={8.6} cy={8.6} r={3} fill={color} />\n        </>\n      ) : null}\n\n      {kind === \"quad\" ? (\n        // Two solid dots and two hollow ones, on opposite diagonals.\n        <>\n          <circle cx={10.2} cy={10.2} r={5.8} fill={color} />\n          <circle cx={21.8} cy={21.8} r={5.8} fill={color} />\n          <circle cx={21.8} cy={10.2} r={4.9} {...s} />\n          <circle cx={10.2} cy={21.8} r={4.9} {...s} />\n        </>\n      ) : null}\n    </svg>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"logo-marquee-row\",\n  name: \"Logo Marquee Row\",\n  category: \"brand\",\n  summary:\n    \"A trusted-by wall of drawn marks gliding leftward between two hairlines on a mathematically exact loop, soft-masked at both frame edges\",\n  tags: [\"marquee\", \"logo wall\", \"trusted by\", \"seamless loop\", \"ticker\", \"social proof\", \"infinite scroll\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: logoMarqueeRowSchema,\n  defaults: {\n    label: \"Trusted by\",\n    logos: [\n      { name: \"Northwind\", mark: \"ring\" },\n      { name: \"Basalt\", mark: \"prism\" },\n      { name: \"Meridian\", mark: \"arcs\" },\n      { name: \"Tessera\", mark: \"quad\" },\n      { name: \"Halyard\", mark: \"diamond\" },\n      { name: \"Fieldwork\", mark: \"bars\" },\n      { name: \"Lumen\", mark: \"pie\" },\n      { name: \"Orbital\", mark: \"lozenge\" },\n    ],\n    meta: \"Selected partners\",\n    showNames: true,\n    spacing: 1,\n    cycles: 1,\n    edgeFade: 0.12,\n    brand: \"slate\",\n  },\n  component: LogoMarqueeRow,\n  file: \"src/registry/items/logo-marquee-row.tsx\",\n  origin: {\n    prompt:\n      \"A 'trusted by' logo marquee that loops seamlessly. Eight abstract company marks drawn from circles, triangles, rounded rects and arcs, each with the company name in mono beside it, sliding leftward at a constant speed between two hairlines. Duplicate the row so the seam never shows and make the loop exact — the last frame has to land back on the first frame's offset. Keep the marks muted, fade the strip out into the frame edges with a soft gradient, and put a quiet mono label above it.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A 'trusted by' logo marquee that loops seamlessly. Eight abstract company marks drawn from circles, triangles, rounded rects and arcs, each with the company name in mono beside it, sliding leftward at a constant speed between two hairlines. Duplicate the row so the seam never shows and make the loop exact — the last frame has to land back on the first frame's offset. Keep the marks muted, fade the strip out into the frame edges with a soft gradient, and put a quiet mono label above it.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4935,
      "props": [
        {
          "name": "label",
          "type": "string",
          "description": "The quiet mono label above the strip. Two to four words: 'Trusted by', 'Shipping with', 'In production at'.",
          "required": false,
          "defaultValue": "Trusted by",
          "options": null
        },
        {
          "name": "logos",
          "type": "array",
          "description": "The wall, in reading order. Six to ten is the sweet spot. Note the trade: the strip travels its own full length over the clip, so a longer list is a faster glide — trim the list, or drop `showNames`, to slow it down.",
          "required": true,
          "defaultValue": "8 items",
          "options": null
        },
        {
          "name": "meta",
          "type": "string",
          "description": "Small mono line hung off the strip's bottom-right corner, e.g. 'Selected partners'. Omit for a barer frame.",
          "required": false,
          "defaultValue": "Selected partners",
          "options": null
        },
        {
          "name": "showNames",
          "type": "boolean",
          "description": "Set the name beside each mark. False gives a marks-only wall — anonymous, tighter, and a slower loop because the pattern is shorter.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "spacing",
          "type": "number",
          "description": "Multiplier on the gap between cells. Below 1 the wall reads as a dense ticker; above 1.4 as a sparse, luxury strip. It also scales the loop's length, and therefore its speed.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "cycles",
          "type": "number",
          "description": "How many times the strip travels its own length across the clip. Whole numbers only — that integer is what makes the last frame land exactly back on the first frame's offset, so the clip can be looped without a jump. 1 is a calm glide, 2 a brisk ticker.",
          "required": false,
          "defaultValue": "1",
          "options": null
        },
        {
          "name": "edgeFade",
          "type": "number",
          "description": "Width of the soft gradient mask at each end of the strip, as a fraction of the frame width. 0.12 is enough to swallow a cell; 0 gives a hard crop at the frame edge, which looks broken.",
          "required": false,
          "defaultValue": "0.12",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "logo-stroke-draw",
      "name": "Logo Stroke Draw",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "Nested rounded squares stroke themselves on in sequence, then hand over to a solid fill as the wordmark masks up",
      "tags": [
        "logo",
        "reveal",
        "sting",
        "evolvepath",
        "stroke"
      ],
      "aspect": "1:1",
      "width": 1080,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/logo-stroke-draw.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { evolvePath } from \"@remotion/paths\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const logoStrokeDrawSchema = z.object({\n  wordmark: z\n    .string()\n    .default(\"MOTIONSTASH\")\n    .describe(\"Wordmark set beneath the mark. Short and uppercase reads best.\"),\n  tagline: z\n    .string()\n    .optional()\n    .describe(\"Optional line under the wordmark. Omit for a barer lockup.\"),\n  rings: z\n    .number()\n    .min(2)\n    .max(4)\n    .default(3)\n    .describe(\"Number of nested rounded squares in the mark. Three is the balanced default.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof logoStrokeDrawSchema>;\n\n/**\n * A rounded square centred on the origin, as a path so it can be stroke-drawn.\n * Built by hand rather than with a shape helper because evolvePath needs a single\n * continuous subpath with a predictable start point — the draw-on has to begin at\n * the top-left corner every time, or the rings appear to unwind from random places.\n */\nconst roundedSquarePath = (half: number, radius: number) => {\n  const h = half;\n  const r = Math.min(radius, half);\n  return [\n    `M ${-h + r} ${-h}`,\n    `L ${h - r} ${-h}`,\n    `A ${r} ${r} 0 0 1 ${h} ${-h + r}`,\n    `L ${h} ${h - r}`,\n    `A ${r} ${r} 0 0 1 ${h - r} ${h}`,\n    `L ${-h + r} ${h}`,\n    `A ${r} ${r} 0 0 1 ${-h} ${h - r}`,\n    `L ${-h} ${-h + r}`,\n    `A ${r} ${r} 0 0 1 ${-h + r} ${-h}`,\n    \"Z\",\n  ].join(\" \");\n};\n\nconst LogoStrokeDraw: React.FC<Props> = ({ wordmark, tagline, rings, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n\n  const side = Math.min(width, height);\n  const markBox = side * 0.42;\n  const strokeW = Math.max(2, side * 0.011);\n\n  const ringDefs = Array.from({ length: rings }, (_, i) => {\n    const scale = 1 - i * 0.24;\n    const half = (markBox / 2) * scale;\n    return {\n      path: roundedSquarePath(half, half * 0.42),\n      rotation: i * 22,\n      // Innermost ring is the accent; the outer rings carry the ink.\n      color: i === rings - 1 ? t.accent : t.ink,\n      index: i,\n    };\n  });\n\n  // The stroke hands over to a solid fill once every ring has finished drawing.\n  const fillStart = 6 + rings * 4 + 8;\n  const fill = interpolate(stage.frame, [fillStart, fillStart + 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        gap: side * 0.045,\n      }}\n    >\n      <div style={{ opacity: stage.presence }}>\n        <svg\n          width={markBox * 1.2}\n          height={markBox * 1.2}\n          viewBox={`${-markBox * 0.6} ${-markBox * 0.6} ${markBox * 1.2} ${markBox * 1.2}`}\n        >\n          {ringDefs.map((ring) => (\n            <Ring key={ring.index} {...ring} strokeW={strokeW} fill={fill} />\n          ))}\n        </svg>\n      </div>\n\n      <Wordmark\n        text={wordmark}\n        tagline={tagline}\n        size={side * 0.062}\n        tokens={t}\n        delay={fillStart}\n      />\n    </AbsoluteFill>\n  );\n};\n\nconst Ring: React.FC<{\n  path: string;\n  rotation: number;\n  color: string;\n  index: number;\n  strokeW: number;\n  fill: number;\n}> = ({ path, rotation, color, index, strokeW, fill }) => {\n  const stage = useStaggeredStage(BUDGETS.statement, index, 4);\n  const draw = interpolate(stage.frame, [4, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const evolution = evolvePath(draw, path);\n\n  return (\n    <g transform={`rotate(${rotation})`}>\n      <path\n        d={path}\n        fill=\"none\"\n        stroke={color}\n        strokeWidth={strokeW}\n        strokeLinecap=\"round\"\n        strokeDasharray={evolution.strokeDasharray}\n        strokeDashoffset={evolution.strokeDashoffset}\n        opacity={1 - fill * 0.85}\n      />\n      <path d={path} fill={color} opacity={fill * (index === 0 ? 0.14 : 0.9)} />\n    </g>\n  );\n};\n\nconst Wordmark: React.FC<{\n  text: string;\n  tagline?: string;\n  size: number;\n  tokens: typeof DEFAULT_BRAND;\n  delay: number;\n}> = ({ text, tagline, size, tokens, delay }) => {\n  const stage = useStage(BUDGETS.statement);\n  const reveal = interpolate(stage.frame, [delay, delay + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const y = interpolate(reveal, [0, 1], [100, 0]);\n\n  return (\n    <div style={{ textAlign: \"center\", opacity: stage.presence }}>\n      <span style={{ display: \"block\", overflow: \"hidden\", paddingBottom: size * 0.1 }}>\n        <span\n          style={{\n            display: \"block\",\n            fontFamily: tokens.fontDisplay,\n            fontSize: size,\n            fontWeight: 700,\n            letterSpacing: size * 0.14,\n            color: tokens.ink,\n            transform: `translateY(${y}%)`,\n          }}\n        >\n          {text}\n        </span>\n      </span>\n      {tagline ? (\n        <div\n          style={{\n            marginTop: size * 0.34,\n            fontFamily: tokens.fontMono,\n            fontSize: size * 0.32,\n            letterSpacing: size * 0.06,\n            textTransform: \"uppercase\",\n            color: tokens.inkMuted,\n            opacity: reveal,\n          }}\n        >\n          {tagline}\n        </div>\n      ) : null}\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"logo-stroke-draw\",\n  name: \"Logo Stroke Draw\",\n  category: \"brand\",\n  summary:\n    \"Nested rounded squares stroke themselves on in sequence, then hand over to a solid fill as the wordmark masks up\",\n  tags: [\"logo\", \"reveal\", \"sting\", \"evolvepath\", \"stroke\"],\n  budget: BUDGETS.statement,\n  aspect: \"1:1\",\n  schema: logoStrokeDrawSchema,\n  defaults: {\n    wordmark: \"MOTIONSTASH\",\n    tagline: \"Code-based motion design\",\n    rings: 3,\n    brand: \"slate\",\n  },\n  component: LogoStrokeDraw,\n  file: \"src/registry/items/logo-stroke-draw.tsx\",\n  origin: {\n    prompt:\n      \"A logo sting where an abstract mark of nested rotated rounded squares draws itself on stroke by stroke, then the strokes cross-fade into solid fills and a wordmark masks up underneath.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A logo sting where an abstract mark of nested rotated rounded squares draws itself on stroke by stroke, then the strokes cross-fade into solid fills and a wordmark masks up underneath.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3802,
      "props": [
        {
          "name": "wordmark",
          "type": "string",
          "description": "Wordmark set beneath the mark. Short and uppercase reads best.",
          "required": false,
          "defaultValue": "MOTIONSTASH",
          "options": null
        },
        {
          "name": "tagline",
          "type": "string",
          "description": "Optional line under the wordmark. Omit for a barer lockup.",
          "required": false,
          "defaultValue": "Code-based motion design",
          "options": null
        },
        {
          "name": "rings",
          "type": "number",
          "description": "Number of nested rounded squares in the mark. Three is the balanced default.",
          "required": false,
          "defaultValue": "3",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "lower-third-bar",
      "name": "Lower Third Bar",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "An accent stripe leads a surface panel out of the left safe area, then the name and role type in behind it",
      "tags": [
        "lower third",
        "broadcast",
        "name plate",
        "wipe",
        "safe area"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 60,
      "durationSeconds": 2,
      "budget": {
        "enter": 12,
        "hold": 36,
        "exit": 12
      },
      "file": "src/registry/items/lower-third-bar.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\n/**\n * The item's budget, hoisted so the component, its sub-components and the\n * registration below all read the same numbers. A lower third is a glance, not\n * a read: `beat` (12 / 36 / 12) is the right length and everything internal is\n * timed to land inside the 12-frame enter window.\n */\nconst BUDGET = BUDGETS.beat;\n\n/** Frame at which the exit window opens. The furl and the retract hang off this. */\nconst EXIT_AT = BUDGET.enter + BUDGET.hold;\n\n/** Frames between the name line and the role line. */\nconst LINE_STAGGER = 2;\n/** Frames before a line's first token starts to rise. */\nconst TEXT_IN = 3;\n/** Frames for one token to clear its mask. */\nconst TEXT_DUR = 6;\n/** Frames spread across all of a line's tokens, first to last. */\nconst TEXT_SPREAD = 2;\n\nexport const lowerThirdBarSchema = z.object({\n  name: z\n    .string()\n    .describe(\"The person or place being named. One to four words; it sets the panel's width, so keep it short.\"),\n  role: z\n    .string()\n    .optional()\n    .describe(\"Second line — title, affiliation or location. Set in mono and muted. Omit for a name-only plate.\"),\n  safeArea: z\n    .number()\n    .min(0)\n    .max(20)\n    .default(6)\n    .describe(\"Safe-area inset as a percentage of each axis. 6 is the broadcast title-safe convention; raise it for social crops with UI chrome.\"),\n  divider: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw a hairline rule between the name and the role. Turn it off for a tighter, more editorial plate.\"),\n  uppercaseRole: z\n    .boolean()\n    .default(true)\n    .describe(\"Set the role line in uppercase with wide tracking — the broadcast default. False keeps the copy as written.\"),\n  scrim: z\n    .boolean()\n    .default(true)\n    .describe(\"Fade the bottom of the frame under the bar. Keep it on when the title sits over footage; off over a flat plate.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof lowerThirdBarSchema>;\n\nconst LowerThirdBar: React.FC<Props> = ({\n  name,\n  role,\n  safeArea,\n  divider,\n  uppercaseRole,\n  scrim,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  // Broadcast sizes type off the picture's short edge, and the safe area off\n  // each axis independently. Both are proportions, so this one file reads\n  // correctly at 16:9, 9:16, 1:1 and 4:5 without a single hard-coded pixel.\n  const unit = Math.min(width, height);\n  const insetX = width * (safeArea / 100);\n  const insetY = height * (safeArea / 100);\n\n  const nameSize = unit * 0.05;\n  const roleSize = unit * 0.0205;\n  const padX = nameSize * 0.58;\n  const padY = nameSize * 0.36;\n  const gap = nameSize * 0.2;\n  const stripeW = Math.max(3, unit * 0.01);\n  const hair = Math.max(1, unit * 0.0015);\n  // The bar reads as one object: token radius on both outer ends, but capped on\n  // the stripe so a 10px-wide sliver never turns into a lozenge.\n  const stripeR = Math.min(t.radius, stripeW * 0.5);\n\n  // Presence: the whole bar arrives from off-frame left and leaves the same\n  // way. Distance is enough to clear the stripe past the frame edge.\n  const travel = insetX + stripeW + unit * 0.06;\n  const barX =\n    interpolate(stage.enter, [0, 1], [-travel, 0]) +\n    interpolate(stage.exit, [0, 1], [0, -travel]);\n\n  // The panel unfurls out from behind the stripe as an animated percentage\n  // width rather than a scaleX: a scaled box stretches its corner radius and\n  // smears its shadow, and both of those are visible at 4K.\n  const unfurl = interpolate(stage.frame, [1, BUDGET.enter - 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const furl = interpolate(stage.frame, [EXIT_AT, EXIT_AT + 10], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.exit,\n  });\n  const panel = unfurl * furl;\n\n  // The hairline draws in once the panel is most of the way out.\n  const ruleWidth =\n    interpolate(stage.frame, [3, BUDGET.enter], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.travel,\n    }) * furl;\n\n  // The plate empties before the panel moves: EASING.exit holds the furl at\n  // ~99% for its first five frames, so the copy is fully gone while the panel\n  // is still at full width and no glyph is ever caught outside the surface.\n  const textOpacity = interpolate(stage.frame, [EXIT_AT, EXIT_AT + 5], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.exit,\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      {scrim ? (\n        <AbsoluteFill\n          style={{\n            background:\n              \"linear-gradient(to top, rgba(0,0,0,0.30) 0%, rgba(0,0,0,0.10) 24%, rgba(0,0,0,0) 44%)\",\n            opacity: stage.presence,\n          }}\n        />\n      ) : null}\n\n      <AbsoluteFill\n        style={{\n          justifyContent: \"flex-end\",\n          alignItems: \"flex-start\",\n          paddingLeft: insetX,\n          paddingBottom: insetY,\n        }}\n      >\n        <div\n          style={{\n            display: \"flex\",\n            alignItems: \"stretch\",\n            transform: `translateX(${barX}px)`,\n            opacity: stage.presence,\n          }}\n        >\n          {/* The accent stripe leads. It sits above the panel in paint order so\n              the panel's shadow never bleeds across the accent colour. */}\n          <div\n            style={{\n              width: stripeW,\n              flexShrink: 0,\n              backgroundColor: t.accent,\n              borderRadius: `${stripeR}px 0 0 ${stripeR}px`,\n              zIndex: 1,\n            }}\n          />\n\n          <div style={{ position: \"relative\", padding: `${padY}px ${padX}px` }}>\n            {/* Panel. The only element carrying a shadow — text stays flat. */}\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                bottom: 0,\n                width: `${panel * 100}%`,\n                backgroundColor: t.surface,\n                borderRadius: `0 ${t.radius}px ${t.radius}px 0`,\n                boxShadow: `0 ${unit * 0.001}px ${unit * 0.002}px rgba(0,0,0,0.22), 0 ${unit * 0.006}px ${unit * 0.024}px rgba(0,0,0,0.3)`,\n              }}\n            />\n\n            <div\n              style={{\n                position: \"relative\",\n                display: \"flex\",\n                flexDirection: \"column\",\n                alignItems: \"flex-start\",\n                opacity: textOpacity,\n              }}\n            >\n              <RevealLine\n                text={name}\n                split=\"word\"\n                index={0}\n                size={nameSize}\n                weight={700}\n                color={t.ink}\n                font={t.fontDisplay}\n                tracking={nameSize * -0.02}\n                uppercase={false}\n                offset={0}\n              />\n\n              {role && divider ? (\n                <div\n                  style={{\n                    marginTop: gap,\n                    height: hair,\n                    width: `${ruleWidth * 100}%`,\n                    backgroundColor: t.rule,\n                  }}\n                />\n              ) : null}\n\n              {role ? (\n                <RevealLine\n                  text={role}\n                  split=\"char\"\n                  index={1}\n                  size={roleSize}\n                  weight={500}\n                  color={t.inkMuted}\n                  font={t.fontMono}\n                  tracking={roleSize * (uppercaseRole ? 0.15 : 0.05)}\n                  uppercase={uppercaseRole}\n                  offset={divider ? gap * 0.9 : gap * 0.7}\n                />\n              ) : null}\n            </div>\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One line of the plate, typed in left to right through hard clipping masks.\n *\n * The display line splits on words and the mono line on characters: splitting\n * a display face per glyph throws away pair kerning, which shows at this size,\n * while a mono face has none to lose and reads as a machine typing. The stagger\n * runs in the same direction as the panel's wipe, so each token lands behind\n * the advancing edge rather than in front of it.\n */\nconst RevealLine: React.FC<{\n  text: string;\n  split: \"word\" | \"char\";\n  index: number;\n  size: number;\n  weight: number;\n  color: string;\n  font: string;\n  tracking: number;\n  uppercase: boolean;\n  offset: number;\n}> = ({ text, split, index, size, weight, color, font, tracking, uppercase, offset }) => {\n  const stage = useStaggeredStage(BUDGET, index, LINE_STAGGER);\n  const tokens = split === \"word\" ? text.split(/\\s+/).filter(Boolean) : Array.from(text);\n  const step = tokens.length > 1 ? TEXT_SPREAD / (tokens.length - 1) : 0;\n\n  // The mask bleeds below the baseline so descenders are not cropped, then the\n  // bleed is removed from layout again to keep the stack's spacing exact.\n  const bleed = size * 0.16;\n\n  return (\n    <span\n      style={{\n        display: \"flex\",\n        flexWrap: \"nowrap\",\n        columnGap: split === \"word\" ? size * 0.26 : 0,\n        marginTop: offset,\n      }}\n    >\n      {tokens.map((token, i) => {\n        const start = TEXT_IN + i * step;\n        const rise = interpolate(stage.frame, [start, start + TEXT_DUR], [1, 0], {\n          extrapolateLeft: \"clamp\",\n          extrapolateRight: \"clamp\",\n          easing: EASING.enter,\n        });\n        return (\n          <span\n            key={`${token}-${i}`}\n            style={{\n              display: \"block\",\n              overflow: \"hidden\",\n              paddingBottom: bleed,\n              marginBottom: -bleed,\n            }}\n          >\n            <span\n              style={{\n                display: \"block\",\n                fontFamily: font,\n                fontSize: size,\n                fontWeight: weight,\n                lineHeight: 1.05,\n                letterSpacing: tracking,\n                textTransform: uppercase ? \"uppercase\" : \"none\",\n                color,\n                transform: `translateY(${rise * 120}%)`,\n              }}\n            >\n              {token === \" \" ? \"\\u00A0\" : token}\n            </span>\n          </span>\n        );\n      })}\n    </span>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"lower-third-bar\",\n  name: \"Lower Third Bar\",\n  category: \"titles\",\n  summary:\n    \"An accent stripe leads a surface panel out of the left safe area, then the name and role type in behind it\",\n  tags: [\"lower third\", \"broadcast\", \"name plate\", \"wipe\", \"safe area\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: lowerThirdBarSchema,\n  defaults: {\n    name: \"Amara Okonkwo\",\n    role: \"Lead Motion Designer\",\n    safeArea: 6,\n    divider: true,\n    uppercaseRole: true,\n    scrim: true,\n    brand: \"slate\",\n  },\n  component: LowerThirdBar,\n  file: \"src/registry/items/lower-third-bar.tsx\",\n  origin: {\n    prompt:\n      \"The broadcast lower third, done properly. A thick accent stripe slides in from the left safe area, a surface panel unfurls out from behind it, then the name and the role type in with a small stagger — name in the display face, role in mono, smaller and muted. Subtle drop shadow on the panel only, and on the way out it retracts the way it came.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "The broadcast lower third, done properly. A thick accent stripe slides in from the left safe area, a surface panel unfurls out from behind it, then the name and the role type in with a small stagger — name in the display face, role in mono, smaller and muted. Subtle drop shadow on the panel only, and on the way out it retracts the way it came.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.1454,
      "props": [
        {
          "name": "name",
          "type": "string",
          "description": "The person or place being named. One to four words; it sets the panel's width, so keep it short.",
          "required": true,
          "defaultValue": "Amara Okonkwo",
          "options": null
        },
        {
          "name": "role",
          "type": "string",
          "description": "Second line — title, affiliation or location. Set in mono and muted. Omit for a name-only plate.",
          "required": false,
          "defaultValue": "Lead Motion Designer",
          "options": null
        },
        {
          "name": "safeArea",
          "type": "number",
          "description": "Safe-area inset as a percentage of each axis. 6 is the broadcast title-safe convention; raise it for social crops with UI chrome.",
          "required": false,
          "defaultValue": "6",
          "options": null
        },
        {
          "name": "divider",
          "type": "boolean",
          "description": "Draw a hairline rule between the name and the role. Turn it off for a tighter, more editorial plate.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "uppercaseRole",
          "type": "boolean",
          "description": "Set the role line in uppercase with wide tracking — the broadcast default. False keeps the copy as written.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "scrim",
          "type": "boolean",
          "description": "Fade the bottom of the frame under the bar. Keep it on when the title sits over footage; off over a flat plate.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "mask-wipe-shape",
      "name": "Mask Wipe Shape",
      "category": "transition",
      "categoryLabel": "Transitions",
      "summary": "The next scene is revealed through a circle, star or rounded square growing past the frame, with an accent outline that fades as it clears",
      "tags": [
        "transition",
        "mask",
        "clip-path",
        "reveal",
        "shapes"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/mask-wipe-shape.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { makeCircle, makeRect, makeStar } from \"@remotion/shapes\";\nimport { translatePath } from \"@remotion/paths\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const maskWipeShapeSchema = z.object({\n  first: z.string().default(\"The old way\").describe(\"Headline of the outgoing scene.\"),\n  second: z.string().default(\"The new one\").describe(\"Headline of the incoming scene.\"),\n  shape: z\n    .enum([\"circle\", \"star\", \"square\"])\n    .default(\"circle\")\n    .describe(\"The shape the next scene is revealed through.\"),\n  outline: z.boolean().default(true).describe(\"Draw an accent outline on the growing shape.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof maskWipeShapeSchema>;\n\n/**\n * The incoming scene lives inside a shape that grows past the frame.\n *\n * The clip path is built in frame coordinates and centred with `translatePath`,\n * so it can be applied straight to a full-frame element via `clip-path: path()`.\n * Building it that way — rather than clipping a small box and scaling it — keeps\n * the revealed scene at its true size throughout, which is what makes the reveal\n * read as a window opening rather than as content zooming in.\n */\nconst buildPath = (kind: Props[\"shape\"], radius: number, width: number, height: number) => {\n  const shape =\n    kind === \"circle\"\n      ? makeCircle({ radius })\n      : kind === \"star\"\n        ? makeStar({ points: 6, innerRadius: radius * 0.55, outerRadius: radius })\n        : makeRect({ width: radius * 1.9, height: radius * 1.9, cornerRadius: radius * 0.18 });\n\n  return translatePath(shape.path, (width - shape.width) / 2, (height - shape.height) / 2);\n};\n\nconst MaskWipeShape: React.FC<Props> = ({ first, second, shape, outline, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const start = 60;\n  const span = 34;\n  // Grows past the frame diagonal so the shape fully clears the corners.\n  const maxRadius = Math.hypot(width, height) * 0.62;\n\n  const grow = interpolate(stage.frame, [start, start + span], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const radius = Math.max(1, grow * maxRadius);\n  const path = buildPath(shape, radius, width, height);\n\n  // The outline fades as the shape exceeds the frame — past that it is just a line at the edges.\n  const outlineOpacity = interpolate(grow, [0, 0.15, 0.75, 1], [0, 1, 1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <Panel headline={first} tokens={t} width={width} tone=\"base\" />\n\n      {grow > 0 ? (\n        <AbsoluteFill style={{ clipPath: `path(\"${path}\")` }}>\n          <Panel headline={second} tokens={t} width={width} tone=\"accent\" />\n        </AbsoluteFill>\n      ) : null}\n\n      {outline && grow > 0 ? (\n        <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }}>\n          <path\n            d={path}\n            fill=\"none\"\n            stroke={t.accent}\n            strokeWidth={Math.max(2, width * 0.0022)}\n            opacity={outlineOpacity}\n          />\n        </svg>\n      ) : null}\n    </AbsoluteFill>\n  );\n};\n\nconst Panel: React.FC<{\n  headline: string;\n  tokens: BrandTokens;\n  width: number;\n  tone: \"base\" | \"accent\";\n}> = ({ headline, tokens, width, tone }) => (\n  <AbsoluteFill\n    style={{\n      backgroundColor: tone === \"accent\" ? tokens.surface : tokens.bg,\n      justifyContent: \"center\",\n      alignItems: \"center\",\n    }}\n  >\n    <div\n      style={{\n        fontFamily: tokens.fontDisplay,\n        fontSize: width * 0.072,\n        fontWeight: 700,\n        letterSpacing: width * -0.0018,\n        color: tone === \"accent\" ? tokens.accent : tokens.ink,\n        textAlign: \"center\",\n      }}\n    >\n      {headline}\n    </div>\n  </AbsoluteFill>\n);\n\nexport default defineMotionItem({\n  id: \"mask-wipe-shape\",\n  name: \"Mask Wipe Shape\",\n  category: \"transition\",\n  summary:\n    \"The next scene is revealed through a circle, star or rounded square growing past the frame, with an accent outline that fades as it clears\",\n  tags: [\"transition\", \"mask\", \"clip-path\", \"reveal\", \"shapes\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: maskWipeShapeSchema,\n  defaults: {\n    first: \"The old way\",\n    second: \"The new one\",\n    shape: \"circle\",\n    outline: true,\n    brand: \"slate\",\n  },\n  component: MaskWipeShape,\n  file: \"src/registry/items/mask-wipe-shape.tsx\",\n  origin: {\n    prompt:\n      \"A transition where a shape grows from the centre until it fills the frame, with the next scene visible inside it and a thin accent outline on the shape while it grows.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A transition where a shape grows from the centre until it fills the frame, with the next scene visible inside it and a thin accent outline on the shape while it grows.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2149,
      "props": [
        {
          "name": "first",
          "type": "string",
          "description": "Headline of the outgoing scene.",
          "required": false,
          "defaultValue": "The old way",
          "options": null
        },
        {
          "name": "second",
          "type": "string",
          "description": "Headline of the incoming scene.",
          "required": false,
          "defaultValue": "The new one",
          "options": null
        },
        {
          "name": "shape",
          "type": "\"circle\" | \"star\" | \"square\"",
          "description": "The shape the next scene is revealed through.",
          "required": false,
          "defaultValue": "circle",
          "options": [
            "circle",
            "star",
            "square"
          ]
        },
        {
          "name": "outline",
          "type": "boolean",
          "description": "Draw an accent outline on the growing shape.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "message-thread",
      "name": "Message Thread",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A conversation assembling itself, typing bubbles swapping for messages as the thread scrolls up",
      "tags": [
        "messaging",
        "chat",
        "bubbles",
        "typing indicator",
        "ui",
        "vertical"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/message-thread.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/* ------------------------------------------------------------------ schema */\n\nconst messageSchema = z.object({\n  from: z\n    .enum([\"them\", \"me\"])\n    .describe(\n      \"Who sent it. 'them' is an incoming bubble, left-aligned on the surface colour; 'me' is outgoing, right-aligned on the accent.\",\n    ),\n  text: z\n    .string()\n    .describe(\n      \"The message body. A dozen words is the ceiling — past three wrapped lines a bubble stops reading as chat and starts reading as a paragraph.\",\n    ),\n  typing: z\n    .boolean()\n    .optional()\n    .describe(\n      \"Force the typing indicator on or off for this one bubble. Omit to follow the thread-level `typingIndicator`. Ignored on outgoing messages — you never watch yourself type.\",\n    ),\n});\n\nexport const messageThreadSchema = z.object({\n  messages: z\n    .array(messageSchema)\n    .min(1)\n    .max(9)\n    .describe(\n      \"The conversation, in order. Alternate senders, and end on an outgoing message so the read receipt lands at the foot of the thread. Six short messages is the sweet spot for this budget; more are compressed to fit and the oldest scroll off the top.\",\n    ),\n  contact: z\n    .string()\n    .default(\"Priya Raman\")\n    .describe(\n      \"Name in the thread header. Its initials draw the avatar too, so a first and last name reads best.\",\n    ),\n  status: z\n    .string()\n    .optional()\n    .describe(\"Small line under the contact name, e.g. 'Online' or 'Active now'. Omit for a barer header.\"),\n  typingIndicator: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Show a three-dot typing bubble before every incoming message, which then collapses as the message takes its place.\",\n    ),\n  dayLabel: z\n    .string()\n    .optional()\n    .describe(\n      \"Rule-and-label divider above the first message, e.g. 'Today'. Omit to open straight on the first bubble.\",\n    ),\n  receipt: z\n    .string()\n    .optional()\n    .describe(\n      \"Read receipt under the last outgoing bubble, e.g. 'Read 9:41'. Omit to leave the thread hanging.\",\n    ),\n  composer: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Show the input bar pinned to the bottom of the frame. Its send button presses in just before each outgoing message.\",\n    ),\n  composerLabel: z\n    .string()\n    .default(\"Message\")\n    .describe(\"Placeholder text in the input bar. Only rendered when `composer` is true.\"),\n  pace: z\n    .number()\n    .min(0.5)\n    .max(2)\n    .default(1.15)\n    .describe(\n      \"Scales the whole conversation's timing. 1 is the natural read, below 1 hurries it, above 1 lets it breathe. Capped automatically so the last bubble always lands before the exit window.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof messageThreadSchema>;\ntype Message = z.infer<typeof messageSchema>;\n\n/* ---------------------------------------------------------------- schedule */\n\nconst BUDGET = BUDGETS.feature;\n\n/**\n * Frames left for the conversation itself: the whole enter+hold span, minus a\n * tail so the receipt and the settle of the last bubble are read at rest\n * rather than under the exit fade.\n */\nconst AVAILABLE = BUDGET.enter + BUDGET.hold - 30;\n\n/** Frames between the last outgoing bubble landing and the receipt appearing. */\nconst RECEIPT_DELAY = 12;\n\n/** Typing and dwell windows, in frames, before `pace` is applied. */\nconst TYPING = { min: 16, max: 46, perChar: 0.5 } as const;\nconst DWELL = { min: 15, max: 40, perChar: 0.45 } as const;\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\ntype Beat = {\n  /** Frame the typing bubble appears, or null when this message has none. */\n  typingFrom: number | null;\n  /** Frame the message bubble lands — and the typing bubble starts collapsing. */\n  appear: number;\n  /** Last bubble of a run from the same sender: the one that carries the tail. */\n  runEnd: boolean;\n};\n\n/**\n * The whole thread's timing, resolved once from the message list.\n *\n * Length drives it: a long message takes longer to \"type\" and is dwelt on\n * longer once it lands, so the rhythm comes out uneven the way a real\n * conversation is. The raw schedule is then scaled by a single factor —\n * `pace`, capped so the thread cannot run past the hold window — which keeps\n * the relative rhythm intact at any speed and makes the component safe to\n * hand an arbitrary number of messages.\n */\nconst buildBeats = (messages: Message[], typingIndicator: boolean, pace: number): Beat[] => {\n  const raw: { typingFrom: number | null; appear: number }[] = [];\n  let cursor = 3;\n\n  for (const message of messages) {\n    const chars = Math.max(1, message.text.trim().length);\n    const wantsTyping = message.from === \"them\" && (message.typing ?? typingIndicator);\n\n    let typingFrom: number | null = null;\n    if (wantsTyping) {\n      typingFrom = cursor;\n      cursor += clamp(chars * TYPING.perChar, TYPING.min, TYPING.max);\n    }\n\n    raw.push({ typingFrom, appear: cursor });\n    cursor += clamp(chars * DWELL.perChar, DWELL.min, DWELL.max);\n  }\n\n  const factor = Math.min(pace, AVAILABLE / Math.max(1, cursor));\n\n  return raw.map((beat, i) => ({\n    typingFrom: beat.typingFrom === null ? null : Math.round(beat.typingFrom * factor),\n    appear: Math.round(beat.appear * factor),\n    runEnd: i === messages.length - 1 || messages[i + 1].from !== messages[i].from,\n  }));\n};\n\nconst initialsOf = (name: string) => {\n  const letters = name\n    .split(/\\s+/)\n    .filter(Boolean)\n    .slice(0, 2)\n    .map((word) => word.charAt(0).toUpperCase())\n    .join(\"\");\n  return letters === \"\" ? \"?\" : letters;\n};\n\n/* --------------------------------------------------------------- component */\n\n/**\n * Message Thread — a conversation assembling itself.\n *\n * The frame is a product surface, not a slide: a header hairline at the top, an\n * input bar at the bottom, and between them a thread that is anchored to its\n * bottom edge. Every bubble opens its own slot with the same spring that scales\n * it in, so the messages above are pushed up by exactly the amount the new\n * bubble grows — the scroll is a consequence of the arrival rather than a second\n * animation. Overflow off the top is clipped under a fade, which is what a real\n * thread does once it is longer than the screen.\n *\n * Incoming messages are preceded by a typing bubble that collapses as the\n * message expands, the two heights crossing so the swap never cuts. Bubbles\n * scale from 0.9 about their tail corner, which is the only fixed point in the\n * layout, so they read as unfolding out of the corner they are anchored to.\n */\nconst MessageThread: React.FC<Props> = ({\n  messages,\n  contact,\n  status,\n  typingIndicator,\n  dayLabel,\n  receipt,\n  composer,\n  composerLabel,\n  pace,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  // The thread lives in a column proportioned like a phone. In 9:16 that is\n  // nearly full-bleed; in a wide frame it becomes a centred column instead of\n  // a stretched one, so the same component survives every aspect.\n  const col = Math.min(width * 0.86, height * 0.5);\n  const gutter = (width - col) / 2;\n\n  // One type unit for the whole surface. Everything else is a ratio of it.\n  const font = col * 0.052;\n  const hair = Math.max(1, col * 0.0013);\n  // Corner radius stays brand-faithful: the token is scaled by the type size,\n  // so Press keeps its near-square bubbles and Signal keeps its soft ones.\n  const radius = Math.max(2, t.radius * (font / 15));\n\n  const beats = buildBeats(messages, typingIndicator, pace);\n  const lastOutgoing = messages.reduce((acc, m, i) => (m.from === \"me\" ? i : acc), -1);\n  const sends = messages.map((m, i) => (m.from === \"me\" ? beats[i].appear : -1)).filter((f) => f >= 0);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          paddingTop: height * 0.05,\n          paddingBottom: height * 0.045,\n          paddingLeft: gutter,\n          paddingRight: gutter,\n          opacity: stage.presence,\n          transform: `translateY(${interpolate(stage.enter, [0, 1], [height * 0.016, 0])}px)`,\n        }}\n      >\n        <Header name={contact} status={status} t={t} font={font} hair={hair} />\n\n        {/* Thread viewport. Bottom-anchored: the stack grows upward and the\n            oldest messages are clipped out of the top under the fade. */}\n        <div style={{ position: \"relative\", flex: 1, minHeight: 0 }}>\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              display: \"flex\",\n              flexDirection: \"column\",\n              justifyContent: \"flex-end\",\n              overflow: \"hidden\",\n            }}\n          >\n            {dayLabel === undefined ? null : (\n              <DayDivider label={dayLabel} t={t} font={font} hair={hair} />\n            )}\n\n            {messages.map((message, i) => (\n              <Row\n                key={`${message.from}-${i}`}\n                message={message}\n                beat={beats[i]}\n                t={t}\n                font={font}\n                radius={radius}\n                hair={hair}\n                maxWidth={col * 0.72}\n                // Tight rhythm inside a run from one sender, a wider beat when\n                // the thread changes hands. The first row needs neither.\n                gap={\n                  i === 0\n                    ? font * 0.12\n                    : messages[i - 1].from === message.from\n                      ? font * 0.22\n                      : font * 0.5\n                }\n                frame={stage.frame}\n                fps={stage.fps}\n                receipt={i === lastOutgoing ? receipt : undefined}\n              />\n            ))}\n          </div>\n\n          <div\n            style={{\n              position: \"absolute\",\n              top: 0,\n              left: 0,\n              right: 0,\n              height: font * 2.4,\n              backgroundImage: `linear-gradient(to bottom, ${t.bg}, ${t.bg}00)`,\n            }}\n          />\n        </div>\n\n        {composer ? (\n          <Composer\n            label={composerLabel}\n            t={t}\n            font={font}\n            hair={hair}\n            radius={radius}\n            frame={stage.frame}\n            sends={sends}\n          />\n        ) : null}\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------ header */\n\nconst Header: React.FC<{\n  name: string;\n  status: string | undefined;\n  t: BrandTokens;\n  font: number;\n  hair: number;\n}> = ({ name, status, t, font, hair }) => {\n  const avatar = font * 2;\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: font * 0.6,\n        paddingBottom: font * 0.75,\n        borderBottom: `${hair}px solid ${t.rule}`,\n      }}\n    >\n      <svg\n        width={font * 0.54}\n        height={font * 0.9}\n        viewBox=\"0 0 12 20\"\n        style={{ display: \"block\", flexShrink: 0, overflow: \"visible\" }}\n      >\n        <path\n          d=\"M10 2 L2 10 L10 18\"\n          fill=\"none\"\n          stroke={t.inkMuted}\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n\n      <div\n        style={{\n          width: avatar,\n          height: avatar,\n          borderRadius: avatar / 2,\n          backgroundColor: t.surface,\n          border: `${hair}px solid ${t.rule}`,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          flexShrink: 0,\n        }}\n      >\n        <span\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: font * 0.78,\n            fontWeight: 600,\n            color: t.ink,\n            letterSpacing: font * 0.01,\n          }}\n        >\n          {initialsOf(name)}\n        </span>\n      </div>\n\n      <div style={{ display: \"flex\", flexDirection: \"column\", gap: font * 0.15 }}>\n        <span\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: font * 0.94,\n            fontWeight: 600,\n            lineHeight: 1.1,\n            letterSpacing: font * -0.012,\n            color: t.ink,\n          }}\n        >\n          {name}\n        </span>\n        {status === undefined ? null : (\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: font * 0.26 }}>\n            <div\n              style={{\n                width: font * 0.19,\n                height: font * 0.19,\n                borderRadius: font * 0.1,\n                backgroundColor: t.accent,\n              }}\n            />\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: font * 0.38,\n                fontWeight: 500,\n                letterSpacing: font * 0.05,\n                textTransform: \"uppercase\",\n                color: t.inkMuted,\n              }}\n            >\n              {status}\n            </span>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst DayDivider: React.FC<{ label: string; t: BrandTokens; font: number; hair: number }> = ({\n  label,\n  t,\n  font,\n  hair,\n}) => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      gap: font * 0.55,\n      paddingTop: font * 0.9,\n      paddingBottom: font * 0.55,\n      flexShrink: 0,\n    }}\n  >\n    <div style={{ flex: 1, height: hair, backgroundColor: t.rule }} />\n    <span\n      style={{\n        fontFamily: t.fontMono,\n        fontSize: font * 0.36,\n        fontWeight: 500,\n        letterSpacing: font * 0.08,\n        textTransform: \"uppercase\",\n        color: t.inkMuted,\n      }}\n    >\n      {label}\n    </span>\n    <div style={{ flex: 1, height: hair, backgroundColor: t.rule }} />\n  </div>\n);\n\n/* -------------------------------------------------------------------- rows */\n\n/**\n * One message's slot in the thread.\n *\n * The height is animated by a single-track grid interpolated from `0fr` to\n * `1fr` — the one way to animate to a content-derived height without measuring\n * the DOM, which a frame-addressable render cannot do. The slot's content is\n * `align-self: end`, so while the track is short the bubble is clipped from the\n * top and appears to rise out of the composer rather than being sliced off at\n * the bottom.\n *\n * Two details are load-bearing. The grid item carries no padding of its own:\n * padding would floor the track's base size, the slot would never collapse to\n * nothing, and every message still to come would leak a sliver of itself into\n * the thread — so the gap above the bubble lives one level further in, as part\n * of the content the track scales.\n */\nconst Slot: React.FC<{\n  factor: number;\n  mine: boolean;\n  inset: number;\n  gap: number;\n  children: React.ReactNode;\n}> = ({ factor, mine, inset, gap, children }) => (\n  <div\n    style={{\n      display: \"grid\",\n      gridTemplateRows: `${factor}fr`,\n      overflow: \"hidden\",\n      // The tail hangs outside the bubble; this padding is inside the clip box,\n      // which is what keeps it from being shaved off.\n      paddingLeft: mine ? 0 : inset,\n      paddingRight: mine ? inset : 0,\n    }}\n  >\n    <div\n      style={{\n        alignSelf: \"end\",\n        minHeight: 0,\n        display: \"flex\",\n        justifyContent: mine ? \"flex-end\" : \"flex-start\",\n      }}\n    >\n      <div style={{ paddingTop: gap }}>{children}</div>\n    </div>\n  </div>\n);\n\nconst Row: React.FC<{\n  message: Message;\n  beat: Beat;\n  t: BrandTokens;\n  font: number;\n  radius: number;\n  hair: number;\n  maxWidth: number;\n  gap: number;\n  frame: number;\n  fps: number;\n  receipt: string | undefined;\n}> = ({ message, beat, t, font, radius, hair, maxWidth, gap, frame, fps, receipt }) => {\n  const mine = message.from === \"me\";\n  const tailSize = font * 0.46;\n  const inset = tailSize * 1.25;\n  const origin = mine ? \"100% 100%\" : \"0% 100%\";\n\n  // One spring for the bubble: it scales it in and opens its slot at the same\n  // rate, so the arrival and the push of the stack above are one gesture.\n  const grow = spring({ frame, fps, config: SPRINGS.snap, durationInFrames: 18, delay: beat.appear });\n  const slot = clamp(grow, 0, 1);\n  const fade = interpolate(frame, [beat.appear, beat.appear + 5], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  // The typing bubble grows in, then collapses on the frame the message lands.\n  const typingIn = spring({\n    frame,\n    fps,\n    config: SPRINGS.snap,\n    durationInFrames: 14,\n    delay: beat.typingFrom ?? 0,\n  });\n  const typingOut = interpolate(frame, [beat.appear, beat.appear + 7], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.exit,\n  });\n  const typing = beat.typingFrom === null ? 0 : clamp(typingIn, 0, 1) * (1 - typingOut);\n\n  return (\n    // Rows are the thread's flex items and must never shrink: a conversation\n    // taller than the frame has to let its oldest rows overflow off the top and\n    // be clipped, not squash every bubble to make itself fit.\n    <div style={{ flexShrink: 0 }}>\n      {beat.typingFrom === null ? null : (\n        <Slot factor={typing} mine={mine} inset={inset} gap={gap}>\n          <div\n            style={{\n              transform: `scale(${0.9 + 0.1 * clamp(typingIn, 0, 1)})`,\n              transformOrigin: origin,\n              opacity: clamp(typing * 1.8, 0, 1),\n            }}\n          >\n            <TypingBubble\n              t={t}\n              font={font}\n              radius={radius}\n              hair={hair}\n              tailSize={tailSize}\n              frame={frame}\n              start={beat.typingFrom}\n            />\n          </div>\n        </Slot>\n      )}\n\n      <Slot factor={slot} mine={mine} inset={inset} gap={gap}>\n        <div\n          style={{\n            maxWidth,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: mine ? \"flex-end\" : \"flex-start\",\n          }}\n        >\n          <div\n            style={{\n              position: \"relative\",\n              opacity: fade,\n              transform: `scale(${0.9 + 0.1 * grow})`,\n              transformOrigin: origin,\n            }}\n          >\n            <div\n              style={{\n                fontFamily: t.fontBody,\n                fontSize: font,\n                fontWeight: 400,\n                lineHeight: 1.32,\n                letterSpacing: font * -0.006,\n                color: mine ? t.accentInk : t.ink,\n                backgroundColor: mine ? t.accent : t.surface,\n                border: `${hair}px solid ${mine ? t.accent : t.rule}`,\n                borderRadius: radius,\n                // The tail corner squares off so the tail meets the bubble flush.\n                borderBottomRightRadius: mine && beat.runEnd ? 0 : radius,\n                borderBottomLeftRadius: !mine && beat.runEnd ? 0 : radius,\n                padding: `${font * 0.5}px ${font * 0.72}px`,\n              }}\n            >\n              {message.text}\n            </div>\n            {beat.runEnd ? (\n              <Tail\n                mine={mine}\n                size={tailSize}\n                fill={mine ? t.accent : t.surface}\n                stroke={mine ? t.accent : t.rule}\n                hair={hair}\n              />\n            ) : null}\n          </div>\n\n          {receipt === undefined ? null : (\n            <Receipt\n              label={receipt}\n              t={t}\n              font={font}\n              frame={frame}\n              start={beat.appear + RECEIPT_DELAY}\n            />\n          )}\n        </div>\n      </Slot>\n    </div>\n  );\n};\n\n/**\n * Bubble tail. Two paths: a fill that tucks a couple of hairlines under the\n * bubble's corner so no seam shows at the junction, and an open stroke that\n * carries the incoming bubble's border around the tip and back along the\n * baseline. The upper edge is concave, which is what makes it read as a tail\n * hanging off the corner rather than a triangle glued to the side.\n */\nconst Tail: React.FC<{\n  mine: boolean;\n  size: number;\n  fill: string;\n  stroke: string;\n  hair: number;\n}> = ({ mine, size, fill, stroke, hair }) => {\n  const h = size * 0.9;\n  const overlap = hair * 2;\n  const box = size + overlap;\n\n  return (\n    <svg\n      width={box}\n      height={h}\n      viewBox={`0 0 ${box} ${h}`}\n      style={{\n        position: \"absolute\",\n        bottom: 0,\n        left: mine ? undefined : -size,\n        right: mine ? -size : undefined,\n        transform: mine ? \"scaleX(-1)\" : undefined,\n        overflow: \"visible\",\n      }}\n    >\n      <path\n        d={`M ${box} 0 L ${box} ${h} L 0 ${h} Q ${size * 0.62} ${h * 0.78} ${size} 0 Z`}\n        fill={fill}\n      />\n      <path\n        d={`M ${size} 0 Q ${size * 0.62} ${h * 0.78} 0 ${h} L ${box} ${h}`}\n        fill=\"none\"\n        stroke={stroke}\n        strokeWidth={hair}\n      />\n    </svg>\n  );\n};\n\n/* ---------------------------------------------------------------- indicator */\n\n/** Frames for one full pulse of a typing dot. */\nconst DOT_CYCLE = 21;\n\nconst TypingBubble: React.FC<{\n  t: BrandTokens;\n  font: number;\n  radius: number;\n  hair: number;\n  tailSize: number;\n  frame: number;\n  start: number;\n}> = ({ t, font, radius, hair, tailSize, frame, start }) => {\n  const dot = font * 0.25;\n  return (\n    <div style={{ position: \"relative\" }}>\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: dot * 0.8,\n          backgroundColor: t.surface,\n          border: `${hair}px solid ${t.rule}`,\n          borderRadius: radius,\n          borderBottomLeftRadius: 0,\n          padding: `${font * 0.5}px ${font * 0.6}px`,\n        }}\n      >\n        {[0, 1, 2].map((i) => (\n          <Dot key={i} index={i} frame={frame} start={start} size={dot} color={t.inkMuted} />\n        ))}\n      </div>\n      <Tail mine={false} size={tailSize} fill={t.surface} stroke={t.rule} hair={hair} />\n    </div>\n  );\n};\n\n/**\n * One dot, on a fixed 21-frame cycle offset five frames per dot: a loop with a\n * dead beat at the end, which is what stops three dots from reading as a wave\n * and makes them read as a hesitation.\n */\nconst Dot: React.FC<{\n  index: number;\n  frame: number;\n  start: number;\n  size: number;\n  color: string;\n}> = ({ index, frame, start, size, color }) => {\n  const phase = (((frame - start - index * 5) % DOT_CYCLE) + DOT_CYCLE) % DOT_CYCLE;\n  const pulse = interpolate(\n    phase,\n    [0, DOT_CYCLE * 0.24, DOT_CYCLE * 0.52, DOT_CYCLE],\n    [0, 1, 0, 0],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.travel },\n  );\n  return (\n    <div\n      style={{\n        width: size,\n        height: size,\n        borderRadius: size / 2,\n        backgroundColor: color,\n        opacity: 0.34 + 0.66 * pulse,\n        transform: `translateY(${-size * 0.42 * pulse}px) scale(${0.86 + 0.14 * pulse})`,\n      }}\n    />\n  );\n};\n\n/* ------------------------------------------------------- receipt & composer */\n\nconst Receipt: React.FC<{\n  label: string;\n  t: BrandTokens;\n  font: number;\n  frame: number;\n  start: number;\n}> = ({ label, t, font, frame, start }) => {\n  const p = interpolate(frame, [start, start + 10], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const size = font * 0.34;\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.5,\n        paddingTop: font * 0.28,\n        paddingRight: font * 0.14,\n        opacity: p,\n        transform: `translateY(${(1 - p) * font * 0.14}px)`,\n      }}\n    >\n      <svg\n        width={size * 1.5}\n        height={size}\n        viewBox=\"0 0 18 12\"\n        style={{ display: \"block\", overflow: \"visible\" }}\n      >\n        <path\n          d=\"M1 7 L4.5 10.5 L11 1.5\"\n          fill=\"none\"\n          stroke={t.accent}\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n        <path\n          d=\"M7 7 L10.5 10.5 L17 1.5\"\n          fill=\"none\"\n          stroke={t.accent}\n          strokeWidth={2}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n      <span\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: size,\n          fontWeight: 500,\n          letterSpacing: size * 0.06,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nconst Composer: React.FC<{\n  label: string;\n  t: BrandTokens;\n  font: number;\n  hair: number;\n  radius: number;\n  frame: number;\n  sends: number[];\n}> = ({ label, t, font, hair, radius, frame, sends }) => {\n  // The send button presses in on the four frames before each outgoing bubble\n  // and releases as it lands. Cause before effect: the tap, then the message.\n  const press = sends.reduce((acc, at) => {\n    const d = frame - (at - 4);\n    if (d < 0 || d > 12) return acc;\n    return Math.max(\n      acc,\n      interpolate(d, [0, 3, 12], [0, 1, 0], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n        easing: EASING.travel,\n      }),\n    );\n  }, 0);\n\n  const bar = font * 1.9;\n  const corner = Math.min(bar / 2, radius * 1.4);\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: font * 0.42,\n        marginTop: font * 0.5,\n        paddingTop: font * 0.85,\n        borderTop: `${hair}px solid ${t.rule}`,\n      }}\n    >\n      <div\n        style={{\n          flex: 1,\n          display: \"flex\",\n          alignItems: \"center\",\n          height: bar,\n          paddingLeft: font * 0.7,\n          paddingRight: font * 0.7,\n          borderRadius: corner,\n          backgroundColor: t.surface,\n          border: `${hair}px solid ${t.rule}`,\n        }}\n      >\n        <span style={{ fontFamily: t.fontBody, fontSize: font * 0.84, color: t.inkMuted }}>\n          {label}\n        </span>\n      </div>\n\n      <div\n        style={{\n          width: bar,\n          height: bar,\n          flexShrink: 0,\n          borderRadius: corner,\n          backgroundColor: t.accent,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          transform: `scale(${1 - 0.09 * press})`,\n        }}\n      >\n        <svg\n          width={bar * 0.44}\n          height={bar * 0.44}\n          viewBox=\"0 0 24 24\"\n          style={{ display: \"block\", overflow: \"visible\" }}\n        >\n          <path\n            d=\"M12 20 L12 5 M5 11.5 L12 4.5 L19 11.5\"\n            fill=\"none\"\n            stroke={t.accentInk}\n            strokeWidth={2.6}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n      </div>\n    </div>\n  );\n};\n\n/* ---------------------------------------------------------------- registry */\n\nexport default defineMotionItem({\n  id: \"message-thread\",\n  name: \"Message Thread\",\n  category: \"product\",\n  summary:\n    \"A conversation assembling itself, typing bubbles swapping for messages as the thread scrolls up\",\n  tags: [\"messaging\", \"chat\", \"bubbles\", \"typing indicator\", \"ui\", \"vertical\"],\n  budget: BUDGETS.feature,\n  aspect: \"9:16\",\n  schema: messageThreadSchema,\n  defaults: {\n    messages: [\n      { from: \"them\", text: \"Any chance the launch cut lands by Friday?\" },\n      { from: \"me\", text: \"It rendered while we were talking.\" },\n      { from: \"them\", text: \"The whole thing?\" },\n      { from: \"me\", text: \"Whole thing. New copy, same components.\" },\n      { from: \"them\", text: \"Send me the 9:16 and I'll post it at nine.\" },\n      { from: \"me\", text: \"Already in your folder.\" },\n    ],\n    contact: \"Priya Raman\",\n    status: \"Online\",\n    typingIndicator: true,\n    dayLabel: \"Today\",\n    receipt: \"Read 9:41\",\n    composer: true,\n    composerLabel: \"Message\",\n    pace: 1.15,\n    brand: \"slate\",\n  },\n  component: MessageThread,\n  file: \"src/registry/items/message-thread.tsx\",\n  origin: {\n    prompt:\n      \"A vertical messaging thread that builds itself: incoming bubbles on the left in the surface colour, outgoing on the right in the accent, each one springing in from its bottom corner. Show a three-dot typing bubble before every incoming message and swap it for the message when it arrives, keep the stack anchored to the bottom so it scrolls up as it fills, and put a read receipt under the last outgoing bubble.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical messaging thread that builds itself: incoming bubbles on the left in the surface colour, outgoing on the right in the accent, each one springing in from its bottom corner. Show a three-dot typing bubble before every incoming message and swap it for the message when it arrives, keep the stack anchored to the bottom so it scrolls up as it fills, and put a read receipt under the last outgoing bubble.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3545,
      "props": [
        {
          "name": "messages",
          "type": "array",
          "description": "The conversation, in order. Alternate senders, and end on an outgoing message so the read receipt lands at the foot of the thread. Six short messages is the sweet spot for this budget; more are compressed to fit and the oldest scroll off the top.",
          "required": true,
          "defaultValue": "6 items",
          "options": null
        },
        {
          "name": "contact",
          "type": "string",
          "description": "Name in the thread header. Its initials draw the avatar too, so a first and last name reads best.",
          "required": false,
          "defaultValue": "Priya Raman",
          "options": null
        },
        {
          "name": "status",
          "type": "string",
          "description": "Small line under the contact name, e.g. 'Online' or 'Active now'. Omit for a barer header.",
          "required": false,
          "defaultValue": "Online",
          "options": null
        },
        {
          "name": "typingIndicator",
          "type": "boolean",
          "description": "Show a three-dot typing bubble before every incoming message, which then collapses as the message takes its place.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "dayLabel",
          "type": "string",
          "description": "Rule-and-label divider above the first message, e.g. 'Today'. Omit to open straight on the first bubble.",
          "required": false,
          "defaultValue": "Today",
          "options": null
        },
        {
          "name": "receipt",
          "type": "string",
          "description": "Read receipt under the last outgoing bubble, e.g. 'Read 9:41'. Omit to leave the thread hanging.",
          "required": false,
          "defaultValue": "Read 9:41",
          "options": null
        },
        {
          "name": "composer",
          "type": "boolean",
          "description": "Show the input bar pinned to the bottom of the frame. Its send button presses in just before each outgoing message.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "composerLabel",
          "type": "string",
          "description": "Placeholder text in the input bar. Only rendered when `composer` is true.",
          "required": false,
          "defaultValue": "Message",
          "options": null
        },
        {
          "name": "pace",
          "type": "number",
          "description": "Scales the whole conversation's timing. 1 is the natural read, below 1 hurries it, above 1 lets it breathe. Capped automatically so the last bubble always lands before the exit window.",
          "required": false,
          "defaultValue": "1.15",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "metric-chip-stack",
      "name": "Metric Chip Stack",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Stat blocks wipe open one after another, each with its caption on a small pill straddling the bottom edge",
      "tags": [
        "stat",
        "chip",
        "kpi",
        "wipe",
        "stack",
        "finance",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/metric-chip-stack.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * The two-part stat chip: a value on a solid accent block, with its caption on a\n * small dark pill straddling the bottom edge.\n *\n * This shape does more work than anything else in the finance-explainer genre —\n * it appears a dozen times in a nine-minute video — and the reason is the split.\n * The number is the claim and gets the loud treatment; the caption is what the\n * number *means* and would weaken the claim if it were set at the same weight.\n * Putting it on its own pill, half-off the block, keeps the two readable as one\n * object without either competing.\n *\n * The block wipes open from the left rather than fading, because a solid field of\n * saturated colour looks broken at 40% opacity. Wipe, don't fade, whenever the\n * thing arriving is a flat colour.\n */\n\nexport const metricChipStackSchema = z.object({\n  chips: z\n    .array(\n      z.object({\n        value: z.string().describe(\"The figure. Short — '7k employees', '$10 Million', '91%'.\"),\n        caption: z.string().describe(\"What the figure is. Two or three words on the pill below.\"),\n      }),\n    )\n    .min(1)\n    .max(5)\n    .describe(\n      \"One to five chips, stacked and revealed in order. Three is the sweet spot; past five the stack is taller than the safe area of a vertical cut.\",\n    ),\n  align: z\n    .enum([\"center\", \"left\"])\n    .default(\"center\")\n    .describe(\"Centred sits the stack in frame. Left is for putting it beside a talking head.\"),\n  dots: z\n    .boolean()\n    .default(true)\n    .describe(\"Fine dot texture over the accent fill. Stops a large flat block reading as a solid colour swatch, and is barely visible until it is missing.\"),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof metricChipStackSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nexport const MetricChipStack: React.FC<Props> = ({ chips, align, dots, brand }) => {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  /*\n   * A slow push across the whole shot.\n   *\n   * Every graphic in this batch builds and then stops, and the craft audit\n   * scored them near the bottom of the library for exactly that — a locked-off\n   * frame is the one thing the house rules forbid. The reference footage is\n   * never still either: these sit inside a cut with a moving camera. The only\n   * one of the six that scored above the cut line was the one that already had\n   * this.\n   */\n  const push = 1 + 0.045 * stage.progress;\n\n  const u = Math.min(width, height) / 1080;\n  const blockW = Math.min(width * 0.36, 660 * u);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n        justifyContent: \"center\",\n        paddingLeft: align === \"left\" ? width * 0.09 : 0,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          gap: 54 * u,\n          transform: `scale(${push})`,\n        }}\n      >\n        {chips.map((c, i) => (\n          <Chip\n            key={c.value + i}\n            index={i}\n            value={c.value}\n            caption={c.caption}\n            width={blockW}\n            u={u}\n            dots={dots}\n            t={t}\n            frame={frame}\n            fps={fps}\n          />\n        ))}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Chip: React.FC<{\n  index: number;\n  value: string;\n  caption: string;\n  width: number;\n  u: number;\n  dots: boolean;\n  t: (typeof BRANDS)[string];\n  frame: number;\n  fps: number;\n}> = ({ index, value, caption, width, u, dots, t, frame, fps }) => {\n  const stage = useStaggeredStage(BUDGET, index, 9);\n\n  // The block wipes; the pill springs in after its block has finished opening.\n  const wipe = stage.enter;\n  const pill = spring({\n    frame: frame - BUDGET.enter - index * 9 - 4,\n    fps,\n    config: { damping: 13, mass: 0.5 },\n  });\n\n  const h = 142 * u;\n\n  return (\n    <div style={{ position: \"relative\", width, height: h + 26 * u, opacity: stage.presence }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: `0 0 auto 0`,\n          height: h,\n          backgroundColor: t.accent,\n          // A left-to-right wipe. `inset()` keeps the type from stretching, which\n          // is what a scaleX would do to it.\n          clipPath: `inset(0 ${(1 - wipe) * 100}% 0 0)`,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          overflow: \"hidden\",\n        }}\n      >\n        {dots ? (\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              backgroundImage: `radial-gradient(${t.accentInk} 1px, transparent 1px)`,\n              backgroundSize: `${11 * u}px ${11 * u}px`,\n              opacity: 0.12,\n            }}\n          />\n        ) : null}\n        <span\n          style={{\n            position: \"relative\",\n            fontFamily: t.fontDisplay,\n            fontWeight: 500,\n            fontSize: 58 * u,\n            letterSpacing: \"-0.02em\",\n            color: t.accentInk,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {value}\n        </span>\n      </div>\n\n      {/* the caption pill, straddling the block's bottom edge */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: h - 20 * u,\n          transform: `translateX(-50%) scale(${Math.max(0, pill)})`,\n          backgroundColor: t.bg,\n          border: `${Math.max(1, 1.5 * u)}px solid ${t.accent}`,\n          borderRadius: 8 * u,\n          padding: `${7 * u}px ${18 * u}px`,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        <span\n          style={{\n            fontFamily: t.fontBody,\n            fontWeight: 500,\n            fontSize: 23 * u,\n            color: t.ink,\n          }}\n        >\n          {caption}\n        </span>\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"metric-chip-stack\",\n  name: \"Metric Chip Stack\",\n  category: \"data\",\n  summary:\n    \"Stat blocks wipe open one after another, each with its caption on a small pill straddling the bottom edge\",\n  tags: [\"stat\", \"chip\", \"kpi\", \"wipe\", \"stack\", \"finance\", \"explainer\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: metricChipStackSchema,\n  defaults: {\n    chips: [\n      { value: \"7k employees\", caption: \"Company Size\" },\n      { value: \"100 countries\", caption: \"Global Presence\" },\n      { value: \"13 in 6 years\", caption: \"Acquisitions\" },\n    ],\n    align: \"center\",\n    dots: true,\n    brand: \"ledger\",\n  },\n  component: MetricChipStack,\n  file: \"src/registry/items/metric-chip-stack.tsx\",\n  origin: {\n    prompt:\n      \"Stacked stat chips in the finance-explainer style: each is a solid accent block with the figure set large in white, and a small dark pill outlined in the accent sits half-off the block's bottom edge carrying the caption. The blocks wipe open left to right one after another — never fade, because a flat saturated field looks broken at partial opacity — and each caption pill springs in just after its own block finishes. A fine dot texture over the fill keeps the block from reading as a plain colour swatch.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Stacked stat chips in the finance-explainer style: each is a solid accent block with the figure set large in white, and a small dark pill outlined in the accent sits half-off the block's bottom edge carrying the caption. The blocks wipe open left to right one after another — never fade, because a flat saturated field looks broken at partial opacity — and each caption pill springs in just after its own block finishes. A fine dot texture over the fill keeps the block from reading as a plain colour swatch.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6102,
      "props": [
        {
          "name": "chips",
          "type": "array",
          "description": "One to five chips, stacked and revealed in order. Three is the sweet spot; past five the stack is taller than the safe area of a vertical cut.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "align",
          "type": "\"center\" | \"left\"",
          "description": "Centred sits the stack in frame. Left is for putting it beside a talking head.",
          "required": false,
          "defaultValue": "center",
          "options": [
            "center",
            "left"
          ]
        },
        {
          "name": "dots",
          "type": "boolean",
          "description": "Fine dot texture over the accent fill. Stops a large flat block reading as a solid colour swatch, and is barely visible until it is missing.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "metric-odometer",
      "name": "Metric Odometer",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Mechanical digit wheels spin and settle left to right with true directional motion blur, inside a dial that draws around them",
      "tags": [
        "counter",
        "odometer",
        "metric",
        "motion blur",
        "vertical",
        "dial",
        "premium"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/metric-odometer.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\n/** Thousands separators without touching a locale — renders must be reproducible. */\nconst group = (n: number) => {\n  const s = Math.round(Math.abs(n)).toString();\n  let out = \"\";\n  for (let i = 0; i < s.length; i += 1) {\n    if (i > 0 && (s.length - i) % 3 === 0) out += \",\";\n    out += s[i];\n  }\n  return `${n < 0 ? \"-\" : \"\"}${out}`;\n};\n\nexport const metricOdometerSchema = z.object({\n  value: z\n    .number()\n    .describe(\"The figure the odometer lands on. Integers only — decimals belong in the unit.\"),\n  label: z\n    .string()\n    .describe(\"What the figure is. Set small and tracked, above the dial.\"),\n  unit: z\n    .string()\n    .optional()\n    .describe(\"Unit under the figure, e.g. 'components' or 'ms'. Omit for a bare number.\"),\n  delta: z\n    .string()\n    .optional()\n    .describe(\"Change chip beside the unit, e.g. '+12.4%'. Omit when there is no comparison.\"),\n  fraction: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.78)\n    .describe(\"How far the dial ring draws around, 0 to 1. Use it to show the figure as a proportion of a target.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof metricOdometerSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.87\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * One digit wheel.\n *\n * A real odometer does not cross-fade between numerals, and neither does this:\n * the strip holds every digit it will pass through and is translated past a\n * window, so the intermediate numerals are genuinely on screen. Left digits get\n * fewer rotations than right ones, which is why the number settles left to\n * right — the detail that makes a counter feel mechanical rather than\n * interpolated.\n */\nconst Wheel: React.FC<{\n  target: number;\n  index: number;\n  size: number;\n  lineH: number;\n  colour: string;\n  font: string;\n}> = ({ target, index, size, lineH, colour, font }) => {\n  const frame = useCurrentFrame();\n  const cycles = 1 + index;\n  const delay = 4;\n  const dur = 26 + index * 7;\n\n  const e = interpolate(frame, [delay, delay + dur], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const cells = cycles * 10 + 10;\n  const offset = -(cycles * 10 + target) * lineH * e;\n\n  /**\n   * Motion blur on the wheel.\n   *\n   * Sampled from the actual travel between this frame and the last, which is\n   * what makes it correct rather than decorative: the smear is strongest in the\n   * middle of the spin and exactly zero once the wheel has landed. Directional,\n   * via an SVG filter, because CSS blur is isotropic and would fog the glyph\n   * sideways too.\n   */\n  const ePrev = interpolate(frame - 1, [delay, delay + dur], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const speed = Math.abs(e - ePrev) * (cycles * 10 + target) * lineH;\n  const smear = Math.min(size * 0.28, speed * 0.34);\n  const filterId = `wheel-blur-${index}`;\n\n  return (\n    <span\n      style={{\n        display: \"inline-block\",\n        height: lineH,\n        overflow: \"hidden\",\n        verticalAlign: \"top\",\n      }}\n    >\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <filter id={filterId} x=\"-20%\" y=\"-50%\" width=\"140%\" height=\"200%\">\n          <feGaussianBlur stdDeviation={`0 ${smear}`} />\n        </filter>\n      </svg>\n      <span\n        style={{\n          display: \"block\",\n          transform: `translateY(${offset}px)`,\n          filter: smear > 0.4 ? `url(#${filterId})` : \"none\",\n        }}\n      >\n        {Array.from({ length: cells }).map((_, k) => (\n          <span\n            key={k}\n            style={{\n              display: \"block\",\n              height: lineH,\n              lineHeight: `${lineH}px`,\n              fontFamily: font,\n              fontSize: size,\n              fontWeight: 700,\n              letterSpacing: size * -0.03,\n              color: colour,\n              textAlign: \"center\",\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n          >\n            {k % 10}\n          </span>\n        ))}\n      </span>\n    </span>\n  );\n};\n\nconst MetricOdometer: React.FC<Props> = ({ value, label, unit, delta, fraction, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const chars = [...group(value)];\n  const digitCount = chars.filter((c) => /[0-9]/.test(c)).length;\n  const size = width * (digitCount > 5 ? 0.155 : 0.2);\n  const lineH = size * 1.06;\n\n  // The dial. Drawn on the same long ease as the wheels, so ring and number\n  // arrive as one event.\n  const r = width * 0.315;\n  const circ = 2 * Math.PI * r;\n  const ring = interpolate(frame, [6, 52], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const swept = fraction * ring;\n  const headAngle = -90 + swept * 360;\n  const headX = width / 2 + Math.cos((headAngle * Math.PI) / 180) * r;\n  const headY = height * 0.47 + Math.sin((headAngle * Math.PI) / 180) * r;\n\n  const labelIn = interpolate(frame, [0, 16], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const unitIn = interpolate(frame, [30 + digitCount * 5, 52 + digitCount * 5], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  /** One specular pass across the numerals, once they have all landed. */\n  const settleAt = 30 + digitCount * 7 + 6;\n  const sweepT = interpolate(frame, [settleAt, settleAt + 24], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const sweepLive = sweepT > 0 && sweepT < 1;\n\n  const push = interpolate(stage.progress, [0, 1], [1, 1.03]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(80% 50% at 50% 30%, ${alpha(t.surface, 0.9)} 0%, ${alpha(\n            t.surface,\n            0.34,\n          )} 34%, ${alpha(t.bg, 0.82)} 66%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(48% 30% at 50% 47%, ${alpha(t.accent, 0.2 * ring)} 0%, ${alpha(\n            t.accent,\n            0,\n          )} 70%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 47%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* the dial */}\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <circle\n            cx={width / 2}\n            cy={height * 0.47}\n            r={r}\n            fill=\"none\"\n            stroke={alpha(t.ink, 0.09)}\n            strokeWidth={Math.max(2, width * 0.004)}\n          />\n          <circle\n            cx={width / 2}\n            cy={height * 0.47}\n            r={r}\n            fill=\"none\"\n            stroke={t.accent}\n            strokeWidth={Math.max(3, width * 0.011)}\n            strokeLinecap=\"round\"\n            strokeDasharray={circ}\n            strokeDashoffset={circ * (1 - swept)}\n            transform={`rotate(-90 ${width / 2} ${height * 0.47})`}\n            opacity={0.95}\n          />\n          {swept > 0.004 ? (\n            <circle\n              cx={headX}\n              cy={headY}\n              r={Math.max(4, width * 0.0125)}\n              fill={t.accent}\n            />\n          ) : null}\n        </svg>\n\n        {/* the arc head's glow, as a real blurred layer rather than a filter on\n            the whole svg */}\n        {swept > 0.004 ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: headX,\n              top: headY,\n              width: width * 0.16,\n              height: width * 0.16,\n              marginLeft: -width * 0.08,\n              marginTop: -width * 0.08,\n              borderRadius: \"50%\",\n              background: `radial-gradient(circle, ${alpha(t.accent, 0.55)} 0%, ${alpha(\n                t.accent,\n                0.18,\n              )} 34%, ${alpha(t.accent, 0)} 70%)`,\n              mixBlendMode: \"screen\",\n            }}\n          />\n        ) : null}\n\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            paddingBottom: height * 0.06,\n          }}\n        >\n          <div\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: width * 0.03,\n              fontWeight: 500,\n              letterSpacing: width * 0.008,\n              textTransform: \"uppercase\",\n              color: t.accent,\n              opacity: labelIn,\n              transform: `translateY(${interpolate(labelIn, [0, 1], [-width * 0.015, 0])}px)`,\n              marginBottom: size * 0.2,\n            }}\n          >\n            {label}\n          </div>\n\n          <div style={{ position: \"relative\" }}>\n            <div style={{ display: \"flex\", alignItems: \"flex-start\" }}>\n              {(() => {\n                let d = -1;\n                return chars.map((char, i) => {\n                  if (!/[0-9]/.test(char)) {\n                    return (\n                      <span\n                        key={`sep-${i}`}\n                        style={{\n                          display: \"inline-block\",\n                          height: lineH,\n                          lineHeight: `${lineH}px`,\n                          fontFamily: t.fontDisplay,\n                          fontSize: size,\n                          fontWeight: 700,\n                          letterSpacing: size * -0.03,\n                          color: alpha(t.ink, 0.4),\n                          opacity: interpolate(frame, [14, 30], [0, 1], {\n                            extrapolateLeft: \"clamp\",\n                            extrapolateRight: \"clamp\",\n                          }),\n                        }}\n                      >\n                        {char}\n                      </span>\n                    );\n                  }\n                  d += 1;\n                  return (\n                    <Wheel\n                      key={`d-${i}`}\n                      target={Number(char)}\n                      index={d}\n                      size={size}\n                      lineH={lineH}\n                      colour={t.ink}\n                      font={t.fontDisplay}\n                    />\n                  );\n                });\n              })()}\n            </div>\n\n            {sweepLive ? (\n              <div\n                aria-hidden\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  pointerEvents: \"none\",\n                  mixBlendMode: \"overlay\",\n                  background: `linear-gradient(100deg, ${alpha(\"#fff\", 0)} 38%, ${alpha(\n                    \"#fff\",\n                    0.4,\n                  )} 49%, ${alpha(\"#fff\", 0.62)} 50%, ${alpha(\"#fff\", 0.4)} 51%, ${alpha(\n                    \"#fff\",\n                    0,\n                  )} 62%)`,\n                  backgroundSize: \"230% 100%\",\n                  backgroundPosition: `${interpolate(sweepT, [0, 1], [-110, 210])}% 0`,\n                }}\n              />\n            ) : null}\n          </div>\n\n          {unit || delta ? (\n            <div\n              style={{\n                marginTop: size * 0.14,\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: width * 0.025,\n                opacity: unitIn,\n                transform: `translateY(${interpolate(unitIn, [0, 1], [width * 0.02, 0])}px)`,\n              }}\n            >\n              {unit ? (\n                <span\n                  style={{\n                    fontFamily: t.fontBody,\n                    fontSize: width * 0.045,\n                    fontWeight: 400,\n                    color: t.inkMuted,\n                  }}\n                >\n                  {unit}\n                </span>\n              ) : null}\n              {delta ? (\n                <span\n                  style={{\n                    fontFamily: t.fontMono,\n                    fontSize: width * 0.03,\n                    fontWeight: 500,\n                    color: t.accentAlt,\n                    border: `1px solid ${alpha(t.accentAlt, 0.35)}`,\n                    backgroundColor: alpha(t.accentAlt, 0.1),\n                    borderRadius: 999,\n                    padding: `${width * 0.008}px ${width * 0.022}px`,\n                  }}\n                >\n                  {delta}\n                </span>\n              ) : null}\n            </div>\n          ) : null}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(112% 78% at 50% 44%, ${alpha(\"#000\", 0)} 32%, ${alpha(\n            \"#000\",\n            0.16,\n          )} 62%, ${alpha(\"#000\", 0.48)} 90%, ${alpha(\"#000\", 0.66)} 100%)`,\n        }}\n      />\n      <Grain id=\"metric-odometer-grain\" frame={frame} opacity={0.075} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"metric-odometer\",\n  name: \"Metric Odometer\",\n  category: \"data\",\n  summary:\n    \"Mechanical digit wheels spin and settle left to right with true directional motion blur, inside a dial that draws around them\",\n  tags: [\"counter\", \"odometer\", \"metric\", \"motion blur\", \"vertical\", \"dial\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: metricOdometerSchema,\n  defaults: {\n    value: 41820,\n    label: \"Frames rendered\",\n    unit: \"frames\",\n    delta: \"+12.4%\",\n    fraction: 0.78,\n    brand: \"slate\",\n  },\n  component: MetricOdometer,\n  file: \"src/registry/items/metric-odometer.tsx\",\n  origin: {\n    prompt:\n      \"A vertical hero number that lands like a mechanical odometer rather than a tweened counter. Each digit should be a real wheel of numerals translated past a window, with the left digits making fewer rotations so the figure settles left to right. Add directional motion blur sampled from the wheel's actual speed, so it smears during the spin and is perfectly sharp once landed. Draw a dial ring around it with a glowing head, and run one specular pass over the numerals at the end.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical hero number that lands like a mechanical odometer rather than a tweened counter. Each digit should be a real wheel of numerals translated past a window, with the left digits making fewer rotations so the figure settles left to right. Add directional motion blur sampled from the wheel's actual speed, so it smears during the spin and is perfectly sharp once landed. Draw a dial ring around it with a glowing head, and run one specular pass over the numerals at the end.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.8332,
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The figure the odometer lands on. Integers only — decimals belong in the unit.",
          "required": true,
          "defaultValue": "41820",
          "options": null
        },
        {
          "name": "label",
          "type": "string",
          "description": "What the figure is. Set small and tracked, above the dial.",
          "required": true,
          "defaultValue": "Frames rendered",
          "options": null
        },
        {
          "name": "unit",
          "type": "string",
          "description": "Unit under the figure, e.g. 'components' or 'ms'. Omit for a bare number.",
          "required": false,
          "defaultValue": "frames",
          "options": null
        },
        {
          "name": "delta",
          "type": "string",
          "description": "Change chip beside the unit, e.g. '+12.4%'. Omit when there is no comparison.",
          "required": false,
          "defaultValue": "+12.4%",
          "options": null
        },
        {
          "name": "fraction",
          "type": "number",
          "description": "How far the dial ring draws around, 0 to 1. Use it to show the figure as a proportion of a target.",
          "required": false,
          "defaultValue": "0.78",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "minimal-video-frame",
      "name": "Minimal Video Frame",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A near-white player that strokes itself on, scrubs, and takes a cursor to its like control",
      "tags": [
        "player",
        "light",
        "minimal",
        "ui",
        "cursor",
        "scrub",
        "reactions"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/minimal-video-frame.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const minimalVideoFrameSchema = z.object({\n  title: z\n    .string()\n    .optional()\n    .describe(\"Line under the player. Left off, the frame is pure player chrome and works as a background plate.\"),\n  duration: z\n    .string()\n    .default(\"0:30\")\n    .describe(\"Duration label at the right of the scrub bar.\"),\n  liked: z\n    .boolean()\n    .default(true)\n    .describe(\"Move a cursor to the like control and press it partway through the shot.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\"Brand token id — one of the keys of BRANDS. Written for the light brands.\"),\n});\n\ntype Props = z.infer<typeof minimalVideoFrameSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"multiply\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.94\" numOctaves={3} seed={Math.floor(frame / 3) % 48} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst MinimalVideoFrame: React.FC<Props> = ({ title, duration, liked, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const frameW = width * 0.58;\n  const frameH = frameW * 0.5625;\n  const left = (width - frameW) / 2;\n  // Leaves room below the picture for the scrub bar, the title and the reaction\n  // row — all three used to run off the bottom of the frame.\n  const top = height * 0.12;\n  const fs = width * 0.017;\n\n  /**\n   * The player draws itself: the border strokes on before anything sits inside\n   * it. On a near-white ground there is no light to model with, so the entrance\n   * has to come from line and timing instead — which is exactly what makes this\n   * register as \"minimal\" rather than as \"unfinished\".\n   */\n  const draw = interpolate(frame, [2, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const playIn = interpolate(frame, [20, 38], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n  const progress = interpolate(frame, [34, BUDGET.enter + BUDGET.hold], [0, 0.68], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const titleIn = interpolate(frame, [44, 62], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const LIKE_AT = 78;\n  const cursorT = interpolate(frame, [52, LIKE_AT], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const likePress = liked\n    ? interpolate(frame, [LIKE_AT, LIKE_AT + 5, LIKE_AT + 14], [0, 1, 0.55], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      })\n    : 0;\n  const isLiked = liked && frame > LIKE_AT;\n\n  const barY = top + frameH + fs * 1.9;\n  const likeX = left + frameW * 0.74;\n\n  const push = interpolate(stage.progress, [0, 1], [1.015, 1]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(95% 65% at 50% 16%, ${alpha(\"#fff\", 0.95)} 0%, ${alpha(\n            \"#fff\",\n            0.4,\n          )} 42%, ${alpha(t.ink, 0.02)} 74%, ${alpha(t.ink, 0.06)} 100%)`,\n        }}\n      />\n      {/* The faint mesh that keeps a near-white ground from reading as empty. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          opacity: 0.5,\n          backgroundImage: `radial-gradient(${alpha(t.ink, 0.1)} 1px, transparent 1px)`,\n          backgroundSize: `${width * 0.011}px ${width * 0.011}px`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 42%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* the picture area */}\n        <div\n          style={{\n            position: \"absolute\",\n            left,\n            top,\n            width: frameW,\n            height: frameH,\n            backgroundColor: alpha(t.ink, 0.035),\n            border: `1px solid ${alpha(t.ink, 0.16)}`,\n            clipPath: `inset(0 ${(1 - draw) * 100}% 0 0)`,\n          }}\n        />\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: left + frameW / 2 - fs * 2,\n            top: top + frameH / 2 - fs * 2,\n            width: fs * 4,\n            height: fs * 4,\n            borderRadius: \"50%\",\n            border: `${Math.max(1.5, fs * 0.09)}px solid ${alpha(t.ink, 0.42)}`,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            opacity: playIn,\n            transform: `scale(${interpolate(playIn, [0, 1], [0.7, 1])})`,\n          }}\n        >\n          <div\n            style={{\n              marginLeft: fs * 0.3,\n              width: 0,\n              height: 0,\n              borderTop: `${fs * 0.72}px solid transparent`,\n              borderBottom: `${fs * 0.72}px solid transparent`,\n              borderLeft: `${fs * 1.15}px solid ${alpha(t.ink, 0.55)}`,\n            }}\n          />\n        </div>\n\n        {/* scrub bar */}\n        <div style={{ position: \"absolute\", left, top: barY, width: frameW, display: \"flex\", alignItems: \"center\", gap: fs }}>\n          <div style={{ position: \"relative\", flex: 1, height: Math.max(2, fs * 0.14), borderRadius: 999, backgroundColor: alpha(t.ink, 0.13) }}>\n            <div style={{ width: `${progress * 100}%`, height: \"100%\", borderRadius: 999, backgroundColor: t.accent }} />\n            <div\n              style={{\n                position: \"absolute\",\n                left: `${progress * 100}%`,\n                top: \"50%\",\n                width: fs * 0.85,\n                height: fs * 0.85,\n                marginLeft: -fs * 0.42,\n                marginTop: -fs * 0.42,\n                borderRadius: \"50%\",\n                backgroundColor: t.accent,\n                boxShadow: `0 ${fs * 0.1}px ${fs * 0.3}px ${alpha(t.ink, 0.2)}`,\n              }}\n            />\n          </div>\n          <span style={{ fontFamily: t.fontMono, fontSize: fs * 0.85, color: alpha(t.ink, 0.45) }}>{duration}</span>\n        </div>\n\n        {/* reaction row */}\n        <div style={{ position: \"absolute\", left: left + frameW * 0.66, top: barY + fs * 1.9, display: \"flex\", gap: fs * 1.5, alignItems: \"center\" }}>\n          <ThumbIcon fs={fs} colour={isLiked ? t.accent : alpha(t.ink, 0.42)} filled={isLiked} scale={1 + likePress * 0.22} />\n          <ThumbIcon fs={fs} colour={alpha(t.ink, 0.42)} filled={false} scale={1} flip />\n          <svg width={fs * 1.5} height={fs * 1.5} viewBox=\"0 0 24 24\" aria-hidden>\n            <circle cx=\"18\" cy=\"5\" r=\"3\" fill=\"none\" stroke={alpha(t.ink, 0.42)} strokeWidth=\"1.8\" />\n            <circle cx=\"6\" cy=\"12\" r=\"3\" fill=\"none\" stroke={alpha(t.ink, 0.42)} strokeWidth=\"1.8\" />\n            <circle cx=\"18\" cy=\"19\" r=\"3\" fill=\"none\" stroke={alpha(t.ink, 0.42)} strokeWidth=\"1.8\" />\n            <path d=\"M8.6 10.6l6.8-4M8.6 13.4l6.8 4\" stroke={alpha(t.ink, 0.42)} strokeWidth=\"1.8\" />\n          </svg>\n        </div>\n\n        {title ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left,\n              top: barY + fs * 1.9,\n              width: frameW * 0.6,\n              fontFamily: t.fontDisplay,\n              fontSize: fs * 1.5,\n              fontWeight: 600,\n              letterSpacing: fs * -0.02,\n              color: t.ink,\n              opacity: titleIn,\n              transform: `translateY(${interpolate(titleIn, [0, 1], [fs * 0.7, 0])}px)`,\n            }}\n          >\n            {title}\n          </div>\n        ) : null}\n\n        {liked ? (\n          <svg\n            width={fs * 1.6}\n            height={fs * 1.6}\n            viewBox=\"0 0 24 24\"\n            style={{\n              position: \"absolute\",\n              left: interpolate(cursorT, [0, 1], [left + frameW * 0.42, likeX + fs * 0.5]),\n              top: interpolate(cursorT, [0, 1], [top + frameH * 0.8, barY + fs * 2.7]),\n              filter: `drop-shadow(0 1px 2px ${alpha(t.ink, 0.3)})`,\n            }}\n            aria-hidden\n          >\n            <path d=\"M5 2l14 10-6.2 1.1L16 20l-2.6 1.2-3.2-6.6L5 18z\" fill={t.surface} stroke={t.ink} strokeWidth=\"1.4\" strokeLinejoin=\"round\" />\n          </svg>\n        ) : null}\n      </div>\n\n      <Grain id=\"minimal-video-grain\" frame={frame} opacity={0.08} />\n    </AbsoluteFill>\n  );\n};\n\nconst ThumbIcon: React.FC<{ fs: number; colour: string; filled: boolean; scale: number; flip?: boolean }> = ({\n  fs,\n  colour,\n  filled,\n  scale,\n  flip,\n}) => (\n  <svg\n    width={fs * 1.5}\n    height={fs * 1.5}\n    viewBox=\"0 0 24 24\"\n    style={{ transform: `scale(${scale})${flip ? \" rotate(180deg)\" : \"\"}` }}\n    aria-hidden\n  >\n    <path\n      d=\"M7 10v9H4a1 1 0 01-1-1v-7a1 1 0 011-1zM7 10l4.2-7.2A1.6 1.6 0 0114 3.7v4.6h4.6a2 2 0 011.95 2.5l-1.6 6.4A2 2 0 0117 18.7H7z\"\n      fill={filled ? colour : \"none\"}\n      stroke={colour}\n      strokeWidth=\"1.7\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nexport default defineMotionItem({\n  id: \"minimal-video-frame\",\n  name: \"Minimal Video Frame\",\n  category: \"product\",\n  summary:\n    \"A near-white player that strokes itself on, scrubs, and takes a cursor to its like control\",\n  tags: [\"player\", \"light\", \"minimal\", \"ui\", \"cursor\", \"scrub\", \"reactions\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: minimalVideoFrameSchema,\n  defaults: {\n    title: \"Launch film — final cut\",\n    duration: \"0:30\",\n    liked: true,\n    brand: \"paper\",\n  },\n  component: MinimalVideoFrame,\n  file: \"src/registry/items/minimal-video-frame.tsx\",\n  origin: {\n    prompt:\n      \"A very quiet light-mode video player on an almost-white ground with a faint dot mesh. The picture frame should stroke itself on from the left before anything appears inside it, then the play control scales in, the scrub bar fills, and a cursor travels over to the like button and presses it. No shadows to speak of — on a white ground the entrance has to come from line and timing.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A very quiet light-mode video player on an almost-white ground with a faint dot mesh. The picture frame should stroke itself on from the left before anything appears inside it, then the play control scales in, the scrub bar fills, and a cursor travels over to the like button and presses it. No shadows to speak of — on a white ground the entrance has to come from line and timing.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.4096,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Line under the player. Left off, the frame is pure player chrome and works as a background plate.",
          "required": false,
          "defaultValue": "Launch film — final cut",
          "options": null
        },
        {
          "name": "duration",
          "type": "string",
          "description": "Duration label at the right of the scrub bar.",
          "required": false,
          "defaultValue": "0:30",
          "options": null
        },
        {
          "name": "liked",
          "type": "boolean",
          "description": "Move a cursor to the like control and press it partway through the shot.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. Written for the light brands.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "number-ticker",
      "name": "Number Ticker",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "A metric spins up on per-digit odometer wheels, each lagging the one to its left, and an accent rule draws once it lands",
      "tags": [
        "counter",
        "odometer",
        "metric",
        "kpi",
        "tabular figures",
        "roll"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/number-ticker.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const numberTickerSchema = z.object({\n  value: z\n    .number()\n    .describe(\"The figure the ticker lands on. This is the shot — pick the number the scene is about.\"),\n  from: z\n    .number()\n    .default(0)\n    .describe(\"Figure the wheels start on. 0 reads as 'counting up'; a nearby value reads as 'moving'.\"),\n  prefix: z\n    .string()\n    .optional()\n    .describe(\"Set before the number, half size and quiet — a currency mark such as '$' or '€'.\"),\n  suffix: z\n    .string()\n    .optional()\n    .describe(\"Set after the number, half size and quiet — '%', 'x', 'M', 'ms'. Omit for a bare figure.\"),\n  decimals: z\n    .number()\n    .int()\n    .min(0)\n    .max(3)\n    .default(0)\n    .describe(\"Fixed decimal places. Decimal wheels spin fastest, so 1–2 places buys a lot of motion.\"),\n  separator: z\n    .enum([\"comma\", \"period\", \"thin-space\", \"none\"])\n    .default(\"comma\")\n    .describe(\n      \"Thousands separator. 'thin-space' is the typographic choice for data; 'period' also flips the decimal mark to a comma for European locales.\",\n    ),\n  label: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono line above the number — what the metric is. Omit for a bare figure.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\"Quiet mono line under the number: the qualifier, period or source. One short sentence.\"),\n  underline: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw an accent rule the width of the number once the count settles.\"),\n  align: z\n    .enum([\"left\", \"center\"])\n    .default(\"left\")\n    .describe(\"Block alignment. 'left' composes the frame; 'center' suits 1:1 and 9:16.\"),\n  spin: z\n    .number()\n    .int()\n    .min(0)\n    .max(4)\n    .default(2)\n    .describe(\n      \"Extra revolutions the fastest (right-hand) wheel makes. 0 rolls each wheel straight to its digit; 1+ gives every wheel at least one full turn, the right-hand ones more.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof numberTickerSchema>;\n\n/**\n * Fixed advance widths, in em of the digit size. Declaring them rather than\n * letting the glyphs measure themselves is what makes the figures tabular by\n * construction: every wheel is exactly as wide as every other, whether it\n * happens to hold a \"1\" or an \"8\", so nothing shifts sideways while it rolls.\n */\nconst DIGIT_EM = 0.63;\nconst MARK_EM = 0.32;\n/** Prefix/suffix size, as a fraction of the digit size. */\nconst AFFIX_EM = 0.5;\n/** Wheel window height in em — the clipped aperture the digits pass through. */\nconst CELL_EM = 1.12;\n/** Approximate (ascent − descent) / 2 for the display face, used to sit the\n *  half-size affixes on the digits' baseline rather than their centre. */\nconst BASELINE_EM = 0.28;\n\nconst BUDGET = BUDGETS.statement;\n/** Frames each wheel lags behind the wheel to its left. */\nconst LAG = 2;\n/**\n * How long one wheel takes to roll. Deliberately longer than the enter window:\n * presence is done in `enter`, but a counter is the canonical piece of internal\n * motion the budget allows to continue into the hold.\n */\nconst ROLL_FRAMES = BUDGET.enter + 22;\nconst EXIT_START = BUDGET.enter + BUDGET.hold;\n\nconst SEPARATOR_CHARS = {\n  comma: \",\",\n  period: \".\",\n  \"thin-space\": \"\\u2009\",\n  none: \"\",\n} as const;\n\n/** A cell in the number row: either a rolling wheel or a static mark. */\ntype Glyph =\n  | { kind: \"digit\"; place: number; slot: number }\n  | { kind: \"mark\"; char: string; muted: boolean };\n\n/**\n * Lay the target number out as cells, tagging each digit with its place value\n * (2 = hundreds, -1 = tenths). Place value is what makes this an odometer and\n * not a slot machine: a wheel's travel is a function of the column it sits in.\n */\nconst buildGlyphs = (\n  value: number,\n  decimals: number,\n  thousands: string,\n  decimalMark: string,\n): { glyphs: Glyph[]; digitCount: number } => {\n  const fixed = Math.abs(value).toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const intPart = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fracPart = dot === -1 ? \"\" : fixed.slice(dot + 1);\n\n  const glyphs: Glyph[] = [];\n  if (value < 0) glyphs.push({ kind: \"mark\", char: \"\\u2212\", muted: false });\n\n  let slot = 0;\n  for (let i = 0; i < intPart.length; i += 1) {\n    const place = intPart.length - 1 - i;\n    if (i > 0 && place % 3 === 2 && thousands) {\n      glyphs.push({ kind: \"mark\", char: thousands, muted: true });\n    }\n    glyphs.push({ kind: \"digit\", place, slot });\n    slot += 1;\n  }\n\n  if (fracPart.length > 0) {\n    glyphs.push({ kind: \"mark\", char: decimalMark, muted: false });\n    for (let i = 0; i < fracPart.length; i += 1) {\n      glyphs.push({ kind: \"digit\", place: -(i + 1), slot });\n      slot += 1;\n    }\n  }\n\n  return { glyphs, digitCount: slot };\n};\n\n/** The digit standing in `place` of `n`, read off the formatted string so the\n *  arithmetic never introduces a floating-point 4.999999. */\nconst digitAtPlace = (n: number, place: number, decimals: number): number => {\n  const fixed = Math.abs(n).toFixed(decimals);\n  const dot = fixed.indexOf(\".\");\n  const intPart = dot === -1 ? fixed : fixed.slice(0, dot);\n  const fracPart = dot === -1 ? \"\" : fixed.slice(dot + 1);\n  const index = place >= 0 ? intPart.length - 1 - place : -place - 1;\n  const source = place >= 0 ? intPart : fracPart;\n  if (index < 0 || index >= source.length) return 0;\n  const digit = Number(source.charAt(index));\n  return Number.isFinite(digit) ? digit : 0;\n};\n\nconst NumberTicker: React.FC<Props> = ({\n  value,\n  from,\n  prefix,\n  suffix,\n  decimals,\n  separator,\n  label,\n  caption,\n  underline,\n  align,\n  spin,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  const thousands = SEPARATOR_CHARS[separator];\n  // A period as the group separator implies a comma as the decimal mark.\n  const decimalMark = separator === \"period\" ? \",\" : \".\";\n  const { glyphs, digitCount } = buildGlyphs(value, decimals, thousands, decimalMark);\n\n  // The number is set as large as the frame will take it: capped against both\n  // axes, then against the measured width of this particular figure, so a\n  // seven-digit metric and a two-digit one are both composed rather than cropped.\n  const emUnits =\n    glyphs.reduce((acc, g) => acc + (g.kind === \"digit\" ? DIGIT_EM : MARK_EM), 0) +\n    (prefix ? prefix.length * AFFIX_EM * DIGIT_EM + 0.08 : 0) +\n    (suffix ? suffix.length * AFFIX_EM * DIGIT_EM + 0.1 : 0);\n  const size = Math.min(width * 0.155, height * 0.25, (width * 0.8) / emUnits);\n  const cellH = size * CELL_EM;\n  const affixSize = size * AFFIX_EM;\n\n  // Beats: the label leads, the wheels roll and cascade, the caption arrives as\n  // the count lands, and the rule draws last. Everything is at rest well before\n  // the exit window opens.\n  const labelIn = interpolate(stage.frame, [0, 12], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const captionIn = interpolate(stage.frame, [ROLL_FRAMES - 8, ROLL_FRAMES + 4], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const rollEnd = (digitCount - 1) * LAG + ROLL_FRAMES;\n  const ruleStart = Math.min(rollEnd + 3, EXIT_START - 18);\n  const ruleDraw = interpolate(stage.frame, [ruleStart, ruleStart + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const barH = Math.max(2, size * 0.016);\n  const blockY = interpolate(stage.enter, [0, 1], [height * 0.022, 0]);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n        paddingLeft: width * 0.09,\n        paddingRight: width * 0.09,\n        // Sits the block a touch above the optical centre, which leaves the\n        // caption room to breathe instead of hanging off the bottom of the stack.\n        paddingBottom: height * 0.03,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n          opacity: stage.presence,\n          transform: `translateY(${blockY}px)`,\n        }}\n      >\n        {label ? (\n          <div\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: size * 0.095,\n              fontWeight: 600,\n              letterSpacing: size * 0.016,\n              textTransform: \"uppercase\",\n              color: t.accent,\n              marginBottom: size * 0.16,\n              opacity: labelIn,\n              transform: `translateY(${interpolate(labelIn, [0, 1], [size * 0.12, 0])}px)`,\n            }}\n          >\n            {label}\n          </div>\n        ) : null}\n\n        {/* Stretch column: its width is set by the number row, so the rule\n            underneath inherits the exact width of the figure. */}\n        <div style={{ display: \"flex\", flexDirection: \"column\", alignItems: \"stretch\" }}>\n          <div style={{ display: \"flex\", flexDirection: \"row\", alignItems: \"flex-start\" }}>\n            {prefix ? (\n              <Affix\n                text={prefix}\n                size={affixSize}\n                nudge={(size - affixSize) * BASELINE_EM}\n                cellH={cellH}\n                color={t.inkMuted}\n                font={t.fontDisplay}\n                style={{ marginRight: size * 0.06 }}\n              />\n            ) : null}\n\n            {glyphs.map((glyph, i) =>\n              glyph.kind === \"digit\" ? (\n                <DigitWheel\n                  key={`d-${i}`}\n                  frame={stage.frame}\n                  startDigit={digitAtPlace(from, glyph.place, decimals)}\n                  targetDigit={digitAtPlace(value, glyph.place, decimals)}\n                  // Revolutions ramp from 1 on the left to spin + 1 on the\n                  // right, the way a real odometer's low-order wheels turn more\n                  // times than its high-order ones. Every wheel makes at least\n                  // one full turn, so nothing in the row sits out the gesture.\n                  turns={\n                    spin === 0\n                      ? 0\n                      : 1 + Math.round((spin * glyph.slot) / Math.max(1, digitCount - 1))\n                  }\n                  delay={glyph.slot * LAG}\n                  size={size}\n                  cellH={cellH}\n                  color={t.ink}\n                  font={t.fontDisplay}\n                />\n              ) : (\n                <div\n                  key={`m-${i}`}\n                  style={{\n                    width: size * MARK_EM,\n                    height: cellH,\n                    lineHeight: `${cellH}px`,\n                    textAlign: \"center\",\n                    fontFamily: t.fontDisplay,\n                    fontSize: size,\n                    fontWeight: 700,\n                    color: glyph.muted ? t.inkMuted : t.ink,\n                  }}\n                >\n                  {glyph.char}\n                </div>\n              ),\n            )}\n\n            {suffix ? (\n              <Affix\n                text={suffix}\n                size={affixSize}\n                nudge={(size - affixSize) * BASELINE_EM}\n                cellH={cellH}\n                color={t.inkMuted}\n                font={t.fontDisplay}\n                style={{ marginLeft: size * 0.07 }}\n              />\n            ) : null}\n          </div>\n\n          {underline ? (\n            <div style={{ position: \"relative\", height: barH, marginTop: size * 0.14 }}>\n              {/* The track is always there; the accent draws along it, which is\n                  the one emphasis gesture in the shot. */}\n              <div style={{ position: \"absolute\", inset: 0, backgroundColor: t.rule }} />\n              <div\n                style={{\n                  position: \"absolute\",\n                  inset: 0,\n                  backgroundColor: t.accent,\n                  transform: `scaleX(${ruleDraw})`,\n                  transformOrigin: align === \"center\" ? \"center\" : \"left\",\n                }}\n              />\n            </div>\n          ) : null}\n        </div>\n\n        {caption ? (\n          <div\n            style={{\n              marginTop: size * (underline ? 0.17 : 0.24),\n              maxWidth: width * 0.62,\n              fontFamily: t.fontMono,\n              fontSize: size * 0.085,\n              fontWeight: 500,\n              lineHeight: 1.5,\n              letterSpacing: size * 0.004,\n              color: t.inkMuted,\n              textAlign: align === \"center\" ? \"center\" : \"left\",\n              opacity: captionIn,\n              transform: `translateY(${interpolate(captionIn, [0, 1], [size * 0.07, 0])}px)`,\n            }}\n          >\n            {caption}\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One digit wheel: a strip of cells sliding behind a hard clipping aperture.\n *\n * The strip is built to length — cell 0 holds the digit the wheel starts on,\n * the last cell holds the digit it lands on, and every cell between counts up\n * by one — so the wheel can be positioned by a single scalar and always\n * arrives dead on its target. Overflow is a real clipped box rather than a\n * gradient mask, which keeps the glyph edges crisp at 4K.\n */\nconst DigitWheel: React.FC<{\n  frame: number;\n  startDigit: number;\n  targetDigit: number;\n  turns: number;\n  delay: number;\n  size: number;\n  cellH: number;\n  color: string;\n  font: string;\n}> = ({ frame, startDigit, targetDigit, turns, delay, size, cellH, color, font }) => {\n  const travel = turns * 10 + ((targetDigit - startDigit + 10) % 10);\n\n  // EASING.travel is the grammar's curve for counters: starts and ends at rest.\n  const positionAt = (f: number) =>\n    interpolate(f, [delay, delay + ROLL_FRAMES], [0, travel], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.travel,\n    });\n\n  const position = positionAt(frame);\n  // Analytic frame-to-frame speed, so the blur is a pure function of the frame\n  // and resolves to zero the instant the wheel settles.\n  const speed = Math.abs(positionAt(frame + 0.5) - positionAt(frame - 0.5)) * cellH;\n  const blur = Math.min(size * 0.018, speed * 0.16);\n\n  const cells: number[] = [];\n  for (let k = 0; k <= travel; k += 1) cells.push((startDigit + k) % 10);\n\n  return (\n    <div style={{ width: size * DIGIT_EM, height: cellH, overflow: \"hidden\" }}>\n      <div\n        style={{\n          transform: `translateY(${-position * cellH}px)`,\n          filter: blur > 0.5 ? `blur(${blur}px)` : undefined,\n          willChange: \"transform\",\n        }}\n      >\n        {cells.map((digit, k) => (\n          <div\n            key={k}\n            style={{\n              height: cellH,\n              lineHeight: `${cellH}px`,\n              textAlign: \"center\",\n              fontFamily: font,\n              fontSize: size,\n              fontWeight: 700,\n              color,\n              fontVariantNumeric: \"tabular-nums\",\n              fontFeatureSettings: '\"tnum\" 1',\n            }}\n          >\n            {digit}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\n/** Prefix or suffix, half size, dropped onto the digits' baseline. */\nconst Affix: React.FC<{\n  text: string;\n  size: number;\n  nudge: number;\n  cellH: number;\n  color: string;\n  font: string;\n  style?: React.CSSProperties;\n}> = ({ text, size, nudge, cellH, color, font, style }) => (\n  <div\n    style={{\n      height: cellH,\n      lineHeight: `${cellH}px`,\n      fontFamily: font,\n      fontSize: size,\n      fontWeight: 600,\n      color,\n      transform: `translateY(${nudge}px)`,\n      ...style,\n    }}\n  >\n    {text}\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"number-ticker\",\n  name: \"Number Ticker\",\n  category: \"typography\",\n  summary:\n    \"A metric spins up on per-digit odometer wheels, each lagging the one to its left, and an accent rule draws once it lands\",\n  tags: [\"counter\", \"odometer\", \"metric\", \"kpi\", \"tabular figures\", \"roll\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: numberTickerSchema,\n  defaults: {\n    value: 1248930,\n    from: 0,\n    prefix: \"$\",\n    suffix: undefined,\n    decimals: 0,\n    separator: \"comma\",\n    label: \"Annual recurring revenue\",\n    caption: \"Up 41% year over year · rolling twelve months\",\n    underline: true,\n    align: \"left\",\n    spin: 2,\n    brand: \"slate\",\n  },\n  component: NumberTicker,\n  file: \"src/registry/items/number-ticker.tsx\",\n  origin: {\n    prompt:\n      \"A big metric counting up like a mechanical odometer: every digit on its own vertical wheel inside a clipped column, the right-hand wheels spinning further than the left-hand ones and each wheel lagging a couple of frames behind the one before it. Currency prefix, thousands separators, tabular figures, a small quiet mono caption underneath, and a thin accent rule that draws itself once the count settles.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A big metric counting up like a mechanical odometer: every digit on its own vertical wheel inside a clipped column, the right-hand wheels spinning further than the left-hand ones and each wheel lagging a couple of frames behind the one before it. Currency prefix, thousands separators, tabular figures, a small quiet mono caption underneath, and a thin accent rule that draws itself once the count settles.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.8238,
      "props": [
        {
          "name": "value",
          "type": "number",
          "description": "The figure the ticker lands on. This is the shot — pick the number the scene is about.",
          "required": true,
          "defaultValue": "1248930",
          "options": null
        },
        {
          "name": "from",
          "type": "number",
          "description": "Figure the wheels start on. 0 reads as 'counting up'; a nearby value reads as 'moving'.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "prefix",
          "type": "string",
          "description": "Set before the number, half size and quiet — a currency mark such as '$' or '€'.",
          "required": false,
          "defaultValue": "$",
          "options": null
        },
        {
          "name": "suffix",
          "type": "string",
          "description": "Set after the number, half size and quiet — '%', 'x', 'M', 'ms'. Omit for a bare figure.",
          "required": false,
          "defaultValue": null,
          "options": null
        },
        {
          "name": "decimals",
          "type": "number",
          "description": "Fixed decimal places. Decimal wheels spin fastest, so 1–2 places buys a lot of motion.",
          "required": false,
          "defaultValue": "0",
          "options": null
        },
        {
          "name": "separator",
          "type": "\"comma\" | \"period\" | \"thin-space\" | \"none\"",
          "description": "Thousands separator. 'thin-space' is the typographic choice for data; 'period' also flips the decimal mark to a comma for European locales.",
          "required": false,
          "defaultValue": "comma",
          "options": [
            "comma",
            "period",
            "thin-space",
            "none"
          ]
        },
        {
          "name": "label",
          "type": "string",
          "description": "Small uppercase mono line above the number — what the metric is. Omit for a bare figure.",
          "required": false,
          "defaultValue": "Annual recurring revenue",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Quiet mono line under the number: the qualifier, period or source. One short sentence.",
          "required": false,
          "defaultValue": "Up 41% year over year · rolling twelve months",
          "options": null
        },
        {
          "name": "underline",
          "type": "boolean",
          "description": "Draw an accent rule the width of the number once the count settles.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "description": "Block alignment. 'left' composes the frame; 'center' suits 1:1 and 9:16.",
          "required": false,
          "defaultValue": "left",
          "options": [
            "left",
            "center"
          ]
        },
        {
          "name": "spin",
          "type": "number",
          "description": "Extra revolutions the fastest (right-hand) wheel makes. 0 rolls each wheel straight to its digit; 1+ gives every wheel at least one full turn, the right-hand ones more.",
          "required": false,
          "defaultValue": "2",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "packet-dot-flow",
      "name": "Packet Dot Flow",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "Requests stream along the top in accent and return underneath in muted, with per-hop latency labels and a loop that closes exactly",
      "tags": [
        "diagram",
        "flow",
        "latency",
        "loop",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/packet-dot-flow.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const packetDotFlowSchema = z.object({\n  nodes: z\n    .array(z.string())\n    .min(2)\n    .max(5)\n    .describe(\"Service names, left to right. The request travels along the top and returns underneath.\"),\n  latencies: z\n    .array(z.string())\n    .describe(\"One label per hop, e.g. '2ms'. Rendered on the outbound leg.\"),\n  packets: z\n    .number()\n    .min(2)\n    .max(10)\n    .default(5)\n    .describe(\"Dots in flight at once. More reads as throughput, fewer as a single trace.\"),\n  laps: z\n    .number()\n    .min(1)\n    .max(4)\n    .default(2)\n    .describe(\"Whole round trips across the shot. Integers only, or the loop will not close.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof packetDotFlowSchema>;\n\n/**\n * The request/response explainer shot.\n *\n * The outbound leg runs along the top in the accent colour and the return leg\n * runs underneath, muted — that asymmetry is what stops it reading as decorative\n * particles and starts it reading as a round trip. Dot phases are evenly spaced\n * and driven by an integer number of laps, so the stream loops seamlessly and can\n * sit under a voiceover of any length.\n */\nconst PacketDotFlow: React.FC<Props> = ({ nodes, latencies, packets, laps, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const boxW = width * 0.15;\n  const boxH = height * 0.12;\n  const gap = (width * 0.84 - boxW * nodes.length) / Math.max(1, nodes.length - 1);\n  const left = width * 0.08;\n  const midY = height * 0.5;\n  const outY = midY - boxH * 0.9;\n  const backY = midY + boxH * 0.9;\n\n  const xOf = (i: number) => left + i * (boxW + gap);\n  const centreOf = (i: number) => xOf(i) + boxW / 2;\n  const spanStart = centreOf(0);\n  const spanEnd = centreOf(nodes.length - 1);\n  const cycle = (stage.progress * Math.round(laps)) % 1;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }}>\n        <line x1={spanStart} y1={outY} x2={spanEnd} y2={outY} stroke={t.rule} strokeWidth={Math.max(1, width * 0.0012)} />\n        <line x1={spanStart} y1={backY} x2={spanEnd} y2={backY} stroke={t.rule} strokeWidth={Math.max(1, width * 0.0012)} />\n\n        {nodes.slice(0, -1).map((_, i) => (\n          <g key={i}>\n            <line x1={centreOf(i)} y1={outY} x2={centreOf(i)} y2={midY - boxH / 2} stroke={t.rule} strokeWidth={1} />\n            <line x1={centreOf(i)} y1={midY + boxH / 2} x2={centreOf(i)} y2={backY} stroke={t.rule} strokeWidth={1} />\n            {latencies[i] ? (\n              <text\n                x={(centreOf(i) + centreOf(i + 1)) / 2}\n                y={outY - height * 0.022}\n                textAnchor=\"middle\"\n                fill={t.inkMuted}\n                style={{ fontFamily: t.fontMono, fontSize: width * 0.0092 }}\n              >\n                {latencies[i]}\n              </text>\n            ) : null}\n          </g>\n        ))}\n        <line x1={centreOf(nodes.length - 1)} y1={outY} x2={centreOf(nodes.length - 1)} y2={midY - boxH / 2} stroke={t.rule} strokeWidth={1} />\n        <line x1={centreOf(nodes.length - 1)} y1={midY + boxH / 2} x2={centreOf(nodes.length - 1)} y2={backY} stroke={t.rule} strokeWidth={1} />\n\n        {Array.from({ length: packets }, (_, i) => {\n          const phase = (cycle + i / packets) % 1;\n          const outbound = phase < 0.5;\n          const p = outbound ? phase / 0.5 : (phase - 0.5) / 0.5;\n          const x = outbound ? spanStart + (spanEnd - spanStart) * p : spanEnd - (spanEnd - spanStart) * p;\n          const y = outbound ? outY : backY;\n          const r = width * (outbound ? 0.0045 : 0.0035);\n          return (\n            <circle\n              key={i}\n              cx={x}\n              cy={y}\n              r={r}\n              fill={outbound ? t.accent : t.inkMuted}\n              opacity={outbound ? 1 : 0.65}\n            />\n          );\n        })}\n      </svg>\n\n      {nodes.map((label, i) => (\n        <NodeBox\n          key={label}\n          label={label}\n          index={i}\n          x={xOf(i)}\n          y={midY - boxH / 2}\n          w={boxW}\n          h={boxH}\n          tokens={t}\n          accent={i === nodes.length - 1}\n        />\n      ))}\n    </AbsoluteFill>\n  );\n};\n\nconst NodeBox: React.FC<{\n  label: string;\n  index: number;\n  x: number;\n  y: number;\n  w: number;\n  h: number;\n  tokens: BrandTokens;\n  accent: boolean;\n}> = ({ label, index, x, y, w, h, tokens, accent }) => {\n  const stage = useStaggeredStage(BUDGETS.feature, index, 4);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: y,\n        width: w,\n        height: h,\n        borderRadius: tokens.radius,\n        backgroundColor: tokens.surface,\n        border: `1px solid ${accent ? tokens.accent : tokens.rule}`,\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        opacity: stage.enter,\n        transform: `scale(${0.9 + stage.enter * 0.1})`,\n      }}\n    >\n      <span\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: w * 0.1,\n          letterSpacing: w * 0.002,\n          color: accent ? tokens.accent : tokens.ink,\n        }}\n      >\n        {label}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"packet-dot-flow\",\n  name: \"Packet Dot Flow\",\n  category: \"diagram\",\n  summary:\n    \"Requests stream along the top in accent and return underneath in muted, with per-hop latency labels and a loop that closes exactly\",\n  tags: [\"diagram\", \"flow\", \"latency\", \"loop\", \"explainer\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: packetDotFlowSchema,\n  defaults: {\n    nodes: [\"Client\", \"Edge\", \"API\", \"Postgres\"],\n    latencies: [\"12ms\", \"4ms\", \"1.8ms\"],\n    packets: 5,\n    laps: 2,\n    brand: \"slate\",\n  },\n  component: PacketDotFlow,\n  file: \"src/registry/items/packet-dot-flow.tsx\",\n  origin: {\n    prompt:\n      \"Data flowing between services: dots travelling right along a top rail and returning left along a bottom rail in a muted colour, with latency labels on each hop, looping seamlessly.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Data flowing between services: dots travelling right along a top rail and returning left along a bottom rail in a muted colour, with latency labels on each hop, looping seamlessly.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3355,
      "props": [
        {
          "name": "nodes",
          "type": "array",
          "description": "Service names, left to right. The request travels along the top and returns underneath.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "latencies",
          "type": "array",
          "description": "One label per hop, e.g. '2ms'. Rendered on the outbound leg.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "packets",
          "type": "number",
          "description": "Dots in flight at once. More reads as throughput, fewer as a single trace.",
          "required": false,
          "defaultValue": "5",
          "options": null
        },
        {
          "name": "laps",
          "type": "number",
          "description": "Whole round trips across the shot. Integers only, or the loop will not close.",
          "required": false,
          "defaultValue": "2",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "particle-converge",
      "name": "Particle Converge",
      "category": "brand",
      "categoryLabel": "Logo & brand",
      "summary": "Points fly in from off frame and resolve into a mark, each trail sampled from its own velocity so it stretches in flight and collapses on arrival",
      "tags": [
        "logo",
        "particles",
        "converge",
        "motion trails",
        "vertical",
        "bloom",
        "premium"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/particle-converge.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const particleConvergeSchema = z.object({\n  wordmark: z\n    .string()\n    .describe(\"The name that resolves under the mark, once the points have landed.\"),\n  tagline: z\n    .string()\n    .optional()\n    .describe(\"One quiet line under the wordmark. Arrives last.\"),\n  mark: z\n    .enum([\"chevron\", \"ring\", \"frame\"])\n    .default(\"chevron\")\n    .describe(\"The shape the points converge into. 'chevron' is a rising mark, 'ring' a closed circle, 'frame' a rounded square outline.\"),\n  halo: z\n    .boolean()\n    .default(true)\n    .describe(\"Add a second, sparser ring of points orbiting the mark. Reads as depth around the shape.\"),\n  count: z\n    .number()\n    .int()\n    .min(80)\n    .max(420)\n    .default(240)\n    .describe(\"How many points. Below 120 the shape reads as dotted; above 320 the trails start to merge into a solid.\"),\n  brand: z\n    .string()\n    .default(\"signal\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof particleConvergeSchema>;\n\nconst BUDGET = BUDGETS.scene;\nconst SEED = \"particle-converge\";\n\ntype Pt = { x: number; y: number };\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.86\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * Even sampling along a polyline.\n *\n * Written out rather than pulled from @remotion/paths because the marks here are\n * straight-edged and circular, and because EVEN spacing is the whole point: a\n * naive one-point-per-vertex distribution clumps at the corners, and clumping is\n * instantly legible as a shortcut. Walking by arc length puts the same number of\n * points on a long edge as on a short one, in proportion.\n */\nconst samplePolyline = (pts: Pt[], n: number, closed: boolean): Pt[] => {\n  const path = closed ? [...pts, pts[0]] : pts;\n  const segs: { a: Pt; b: Pt; len: number }[] = [];\n  let total = 0;\n  for (let i = 0; i < path.length - 1; i += 1) {\n    const a = path[i];\n    const b = path[i + 1];\n    const len = Math.hypot(b.x - a.x, b.y - a.y);\n    segs.push({ a, b, len });\n    total += len;\n  }\n  if (total === 0 || segs.length === 0) return Array.from({ length: n }, () => ({ x: 0, y: 0 }));\n\n  const out: Pt[] = [];\n  let si = 0;\n  let acc = 0;\n  for (let k = 0; k < n; k += 1) {\n    const target = (k / n) * total;\n    while (si < segs.length - 1 && acc + segs[si].len < target) {\n      acc += segs[si].len;\n      si += 1;\n    }\n    const s = segs[si];\n    const u = s.len === 0 ? 0 : Math.min(1, (target - acc) / s.len);\n    out.push({ x: s.a.x + (s.b.x - s.a.x) * u, y: s.a.y + (s.b.y - s.a.y) * u });\n  }\n  return out;\n};\n\nconst circlePts = (r: number, n = 128): Pt[] =>\n  Array.from({ length: n }, (_, i) => {\n    const a = (i / n) * Math.PI * 2;\n    return { x: Math.cos(a) * r, y: Math.sin(a) * r };\n  });\n\nconst roundedRectPts = (s: number, radius: number, per = 14): Pt[] => {\n  const h = s / 2;\n  const r = Math.min(radius, h);\n  const pts: Pt[] = [];\n  const corners: { cx: number; cy: number; from: number }[] = [\n    { cx: h - r, cy: h - r, from: 0 },\n    { cx: -h + r, cy: h - r, from: 90 },\n    { cx: -h + r, cy: -h + r, from: 180 },\n    { cx: h - r, cy: -h + r, from: 270 },\n  ];\n  for (const c of corners) {\n    for (let i = 0; i <= per; i += 1) {\n      const a = ((c.from + (i / per) * 90) * Math.PI) / 180;\n      pts.push({ x: c.cx + Math.cos(a) * r, y: c.cy + Math.sin(a) * r });\n    }\n  }\n  return pts;\n};\n\nconst markPoints = (kind: Props[\"mark\"], s: number, n: number): Pt[] => {\n  if (kind === \"ring\") return samplePolyline(circlePts(s * 0.42), n, true);\n  if (kind === \"frame\") return samplePolyline(roundedRectPts(s * 0.78, s * 0.2), n, true);\n  // The chevron: a rising mark, open at both ends so the shape has direction.\n  return samplePolyline(\n    [\n      { x: -s * 0.38, y: s * 0.3 },\n      { x: -s * 0.38, y: -s * 0.24 },\n      { x: 0, y: s * 0.12 },\n      { x: s * 0.38, y: -s * 0.24 },\n      { x: s * 0.38, y: s * 0.3 },\n    ],\n    n,\n    false,\n  );\n};\n\nconst ParticleConverge: React.FC<Props> = ({ wordmark, tagline, mark, halo, count, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const cx = width / 2;\n  const cy = height * 0.4;\n  const markSize = width * 0.5;\n\n  const shapeCount = halo ? Math.round(count * 0.74) : count;\n  const haloCount = count - shapeCount;\n  const shape = markPoints(mark, markSize, shapeCount);\n  const orbit = halo ? samplePolyline(circlePts(markSize * 0.62), haloCount, true) : [];\n  const targets = [...shape, ...orbit];\n\n  /** The convergence window. Everything else keys off it. */\n  const CONVERGE_END = 46;\n  const settled = interpolate(frame, [30, CONVERGE_END], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  // Flash at the moment the last points land, decaying faster than it rose.\n  const flash = interpolate(frame, [CONVERGE_END - 6, CONVERGE_END, CONVERGE_END + 16], [0, 0.45, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const wordIn = interpolate(frame, [CONVERGE_END - 2, CONVERGE_END + 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const tagIn = interpolate(frame, [CONVERGE_END + 12, CONVERGE_END + 36], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // The settled mark keeps turning. A converged particle field that freezes\n  // completely stops reading as a swarm and starts reading as a PNG — and it\n  // measures as a dead frame, which is the same defect stated numerically.\n  const spin = interpolate(stage.progress, [0, 1], [0, 6.5]);\n  const push = interpolate(stage.progress, [0, 1], [1.05, 1]);\n\n  /** Position and velocity for one point, at this frame and the last. */\n  const solve = (i: number, f: number) => {\n    const target = targets[i];\n    const ang = random(`${SEED}-a-${i}`) * Math.PI * 2;\n    const rad = width * (0.5 + random(`${SEED}-r-${i}`) * 0.55);\n    const delay = random(`${SEED}-d-${i}`) * 15;\n    const dur = 28 + random(`${SEED}-u-${i}`) * 15;\n\n    const e = interpolate(f, [delay, delay + dur], [0, 1], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.enter,\n    });\n\n    /**\n     * Formation jitter.\n     *\n     * Applied at the TARGET and scaled by arrival, so it is absent in flight\n     * and only present once a point has taken its place. A swarm holding a\n     * shape does not go rigid — each point keeps working against its neighbours\n     * — and this is the difference between a converged field and a still image\n     * of one. Each point gets its own phase and its own axis from the seed, so\n     * the whole formation never pulses in unison.\n     */\n    const bp = random(`${SEED}-p-${i}`) * Math.PI * 2;\n    const bs = 0.9 + random(`${SEED}-s-${i}`) * 1.1;\n    const swim = Math.sin((f / fps) * bs + bp) * width * 0.0034;\n    const tx = target.x + Math.cos(bp) * swim * e;\n    const ty = target.y + Math.sin(bp) * swim * e;\n\n    const sx = cx + Math.cos(ang) * rad;\n    const sy = cy + Math.sin(ang) * rad * 0.8;\n    return {\n      x: sx + (cx + tx - sx) * e,\n      y: sy + (cy + ty - sy) * e,\n      e,\n    };\n  };\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(72% 46% at 50% 34%, ${alpha(t.surface, 0.8)} 0%, ${alpha(\n            t.surface,\n            0.26,\n          )} 36%, ${alpha(t.bg, 0.88)} 68%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: `50% ${(cy / height) * 100}%`,\n          opacity: stage.presence,\n        }}\n      >\n        {/* the swarm */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            mixBlendMode: \"screen\",\n            transform: `rotate(${spin}deg)`,\n            transformOrigin: `${cx}px ${cy}px`,\n          }}\n        >\n          {targets.map((_, i) => {\n            const now = solve(i, frame);\n            const prev = solve(i, frame - 1);\n            const vx = now.x - prev.x;\n            const vy = now.y - prev.y;\n            const speed = Math.hypot(vx, vy);\n\n            const z = random(`${SEED}-z-${i}`);\n\n            /**\n             * A light wave through the settled formation.\n             *\n             * Once the points have landed there is nothing left to animate\n             * except the points themselves, and a logo made of particles that\n             * simply stops is indistinguishable from a still. So a soft front\n             * travels across the mark and each point brightens and swells as it\n             * passes — the formation is holding, but light is still moving\n             * through it.\n             */\n            const waveX = ((frame / 30) * 0.42) % 2.4 - 0.7;\n            const rel = (now.x / width - waveX) * 2.6;\n            const lit = Math.exp(-rel * rel) * settled;\n\n            const dot = width * (0.0026 + z * 0.0032) * (1 + lit * 0.85);\n            // Trail length IS the velocity — sampled from the motion, so it\n            // stretches while travelling and collapses to a point on arrival.\n            const trail = Math.min(width * 0.09, speed * 1.5);\n            const angle = (Math.atan2(vy, vx) * 180) / Math.PI;\n\n            // Depth: far points are smaller, softer and dimmer.\n            const blur = (1 - z) * width * 0.0026;\n            const opacity = Math.min(1, (0.32 + z * 0.68) * (1 + lit * 1.1)) * Math.min(1, now.e * 6);\n            const colour = i < shapeCount ? t.accent : t.accentAlt;\n\n            return (\n              <div\n                key={i}\n                style={{\n                  position: \"absolute\",\n                  left: now.x,\n                  top: now.y,\n                  width: 0,\n                  height: 0,\n                  transform: `rotate(${angle}deg)`,\n                }}\n              >\n                <div\n                  style={{\n                    position: \"absolute\",\n                    right: 0,\n                    top: -dot / 2,\n                    width: dot + trail,\n                    height: dot,\n                    borderRadius: dot,\n                    opacity,\n                    filter: blur > 0.3 ? `blur(${blur}px)` : \"none\",\n                    background: `linear-gradient(90deg, ${alpha(colour, 0)} 0%, ${alpha(\n                      colour,\n                      0.35,\n                    )} 62%, ${alpha(\"#fff\", 0.95)} 100%)`,\n                  }}\n                />\n              </div>\n            );\n          })}\n        </div>\n\n        {/* the mark's own glow, strongest just after convergence */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: cx,\n            top: cy,\n            width: markSize * 1.9,\n            height: markSize * 1.9,\n            marginLeft: -markSize * 0.95,\n            marginTop: -markSize * 0.95,\n            borderRadius: \"50%\",\n            background: `radial-gradient(circle, ${alpha(t.accent, 0.3 * settled + flash)} 0%, ${alpha(\n              t.accent,\n              0.1 * settled,\n            )} 34%, ${alpha(t.accent, 0)} 68%)`,\n            mixBlendMode: \"screen\",\n          }}\n        />\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * 0.08,\n            right: width * 0.08,\n            top: cy + markSize * 0.52,\n            textAlign: \"center\",\n          }}\n        >\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: width * 0.115,\n              fontWeight: 700,\n              letterSpacing: width * 0.006,\n              color: t.ink,\n              opacity: wordIn,\n              transform: `translateY(${interpolate(wordIn, [0, 1], [width * 0.024, 0])}px)`,\n              filter: `blur(${interpolate(wordIn, [0, 1], [width * 0.006, 0])}px)`,\n            }}\n          >\n            {wordmark}\n          </div>\n          {tagline ? (\n            <div\n              style={{\n                marginTop: width * 0.03,\n                fontFamily: t.fontBody,\n                fontSize: width * 0.032,\n                lineHeight: 1.45,\n                color: t.inkMuted,\n                opacity: tagIn,\n                transform: `translateY(${interpolate(tagIn, [0, 1], [width * 0.016, 0])}px)`,\n              }}\n            >\n              {tagline}\n            </div>\n          ) : null}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          opacity: flash,\n          background: `radial-gradient(50% 32% at 50% ${(cy / height) * 100}%, ${alpha(\n            \"#fff\",\n            0.42,\n          )} 0%, ${alpha(\"#fff\", 0)} 70%)`,\n          mixBlendMode: \"screen\",\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(112% 76% at 50% 38%, ${alpha(\"#000\", 0)} 30%, ${alpha(\n            \"#000\",\n            0.16,\n          )} 60%, ${alpha(\"#000\", 0.48)} 90%, ${alpha(\"#000\", 0.68)} 100%)`,\n        }}\n      />\n      <Grain id=\"particle-converge-grain\" frame={frame} opacity={0.075} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"particle-converge\",\n  name: \"Particle Converge\",\n  category: \"brand\",\n  summary:\n    \"Points fly in from off frame and resolve into a mark, each trail sampled from its own velocity so it stretches in flight and collapses on arrival\",\n  tags: [\"logo\", \"particles\", \"converge\", \"motion trails\", \"vertical\", \"bloom\", \"premium\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: particleConvergeSchema,\n  defaults: {\n    wordmark: \"MOTIONSTASH\",\n    tagline: \"One timing grammar. Every format.\",\n    mark: \"chevron\",\n    halo: true,\n    count: 240,\n    brand: \"signal\",\n  },\n  component: ParticleConverge,\n  file: \"src/registry/items/particle-converge.tsx\",\n  origin: {\n    prompt:\n      \"A vertical logo reveal where a few hundred points fly in from off frame and converge into a geometric mark. Each point needs a motion trail whose length comes from its actual velocity, so it stretches while travelling and becomes a dot the instant it lands. Distribute the points evenly along the shape by arc length rather than per vertex, vary their depth so the far ones are softer and dimmer, and flash a bloom at the moment the last one arrives before the wordmark resolves underneath.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical logo reveal where a few hundred points fly in from off frame and converge into a geometric mark. Each point needs a motion trail whose length comes from its actual velocity, so it stretches while travelling and becomes a dot the instant it lands. Distribute the points evenly along the shape by arc length rather than per vertex, vary their depth so the far ones are softer and dimmer, and flash a bloom at the moment the last one arrives before the wordmark resolves underneath.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.6509,
      "props": [
        {
          "name": "wordmark",
          "type": "string",
          "description": "The name that resolves under the mark, once the points have landed.",
          "required": true,
          "defaultValue": "MOTIONSTASH",
          "options": null
        },
        {
          "name": "tagline",
          "type": "string",
          "description": "One quiet line under the wordmark. Arrives last.",
          "required": false,
          "defaultValue": "One timing grammar. Every format.",
          "options": null
        },
        {
          "name": "mark",
          "type": "\"chevron\" | \"ring\" | \"frame\"",
          "description": "The shape the points converge into. 'chevron' is a rising mark, 'ring' a closed circle, 'frame' a rounded square outline.",
          "required": false,
          "defaultValue": "chevron",
          "options": [
            "chevron",
            "ring",
            "frame"
          ]
        },
        {
          "name": "halo",
          "type": "boolean",
          "description": "Add a second, sparser ring of points orbiting the mark. Reads as depth around the shape.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "count",
          "type": "number",
          "description": "How many points. Below 120 the shape reads as dotted; above 320 the trails start to merge into a solid.",
          "required": false,
          "defaultValue": "240",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "signal",
          "options": null
        }
      ]
    },
    {
      "id": "path-follow",
      "name": "Path Follow",
      "category": "primitive",
      "categoryLabel": "Primitives",
      "summary": "A marker travels an SVG route, banking into curves because its rotation comes from the path tangent rather than a separate animation",
      "tags": [
        "primitive",
        "path",
        "tangent",
        "route",
        "evolvepath"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/path-follow.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { evolvePath, getLength, getPointAtLength, getTangentAtLength } from \"@remotion/paths\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const pathFollowSchema = z.object({\n  label: z\n    .string()\n    .default(\"Route\")\n    .describe(\"Small mono label above the path. Keep it to one or two words.\"),\n  waypoints: z\n    .number()\n    .min(0)\n    .max(6)\n    .default(4)\n    .describe(\"Dots placed along the route that light up as the marker passes them.\"),\n  trail: z\n    .number()\n    .min(0)\n    .max(24)\n    .default(14)\n    .describe(\"Fading dots left behind the marker. Zero disables the trail.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof pathFollowSchema>;\n\n/**\n * The primitive behind map routes, packet flows and any \"thing travelling a line\".\n *\n * Position AND rotation both come from the path: `getPointAtLength` places the\n * marker and `getTangentAtLength` orients it, so the marker banks into curves\n * instead of sliding sideways through them. Deriving rotation from the tangent\n * rather than animating it separately is the difference between this reading as\n * travel and reading as a sprite being dragged.\n */\n/**\n * Both path queries are nullable — a degenerate length or an unparsable segment\n * returns null rather than throwing. Falling back keeps a bad prop from taking\n * the whole render down, which matters when props arrive from an agent.\n */\nconst pointAt = (path: string, at: number) => getPointAtLength(path, at) ?? { x: 0, y: 0 };\nconst tangentAt = (path: string, at: number) => getTangentAtLength(path, at) ?? { x: 1, y: 0 };\n\nconst PathFollow: React.FC<Props> = ({ label, waypoints, trail, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  // A double-S route, laid out in frame pixels so the SVG needs no scaling.\n  const path = [\n    `M ${width * 0.1} ${height * 0.7}`,\n    `C ${width * 0.24} ${height * 0.28}, ${width * 0.38} ${height * 0.28}, ${width * 0.5} ${height * 0.5}`,\n    `C ${width * 0.62} ${height * 0.72}, ${width * 0.76} ${height * 0.72}, ${width * 0.9} ${height * 0.3}`,\n  ].join(\" \");\n\n  const total = getLength(path);\n  const travel = interpolate(stage.progress, [0.06, 0.92], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const at = Math.max(0.001, travel * total);\n\n  const point = pointAt(path, at);\n  const tangent = tangentAt(path, at);\n  const angle = (Math.atan2(tangent.y, tangent.x) * 180) / Math.PI;\n\n  const drawn = evolvePath(travel, path);\n  const marker = Math.max(8, width * 0.014);\n  const hair = Math.max(1.5, width * 0.0016);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: width * 0.1,\n          top: height * 0.12,\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n        }}\n      >\n        {label} · {Math.round(travel * 100)}%\n      </div>\n\n      <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }}>\n        {/* The full route, faint, so the destination is legible from frame one. */}\n        <path d={path} fill=\"none\" stroke={t.rule} strokeWidth={hair} />\n\n        {/* The travelled portion. */}\n        <path\n          d={path}\n          fill=\"none\"\n          stroke={t.accent}\n          strokeWidth={hair * 2.2}\n          strokeLinecap=\"round\"\n          strokeDasharray={drawn.strokeDasharray}\n          strokeDashoffset={drawn.strokeDashoffset}\n        />\n\n        {Array.from({ length: waypoints }, (_, i) => {\n          const frac = (i + 1) / (waypoints + 1);\n          const p = pointAt(path, frac * total);\n          const passed = travel >= frac;\n          return (\n            <g key={i}>\n              <circle\n                cx={p.x}\n                cy={p.y}\n                r={marker * (passed ? 0.42 : 0.3)}\n                fill={passed ? t.accent : t.bg}\n                stroke={passed ? t.accent : t.rule}\n                strokeWidth={hair}\n              />\n              {passed ? (\n                <circle cx={p.x} cy={p.y} r={marker * 0.8} fill=\"none\" stroke={t.accent} strokeWidth={hair} opacity={0.3} />\n              ) : null}\n            </g>\n          );\n        })}\n\n        {Array.from({ length: trail }, (_, i) => {\n          const back = at - (i + 1) * (total * 0.012);\n          if (back <= 0) return null;\n          const p = pointAt(path, back);\n          return (\n            <circle\n              key={i}\n              cx={p.x}\n              cy={p.y}\n              r={marker * 0.16}\n              fill={t.accent}\n              opacity={(1 - i / trail) * 0.5}\n            />\n          );\n        })}\n\n        {/* Marker, oriented by the tangent. */}\n        <g transform={`translate(${point.x} ${point.y}) rotate(${angle})`}>\n          <path\n            d={`M ${-marker * 0.7} ${-marker * 0.62} L ${marker} 0 L ${-marker * 0.7} ${marker * 0.62} L ${-marker * 0.32} 0 Z`}\n            fill={t.accent}\n          />\n        </g>\n      </svg>\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"path-follow\",\n  name: \"Path Follow\",\n  category: \"primitive\",\n  summary:\n    \"A marker travels an SVG route, banking into curves because its rotation comes from the path tangent rather than a separate animation\",\n  tags: [\"primitive\", \"path\", \"tangent\", \"route\", \"evolvepath\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: pathFollowSchema,\n  defaults: { label: \"Route\", waypoints: 4, trail: 14, brand: \"slate\" },\n  component: PathFollow,\n  file: \"src/registry/items/path-follow.tsx\",\n  origin: {\n    prompt:\n      \"An element travelling along a curving SVG path with correct auto-rotation from the tangent, drawing the route as it goes, leaving a fading trail, with waypoints that light up as it passes them.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An element travelling along a curving SVG path with correct auto-rotation from the tangent, drawing the route as it goes, leaving a fading trail, with waypoints that light up as it passes them.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3052,
      "props": [
        {
          "name": "label",
          "type": "string",
          "description": "Small mono label above the path. Keep it to one or two words.",
          "required": false,
          "defaultValue": "Route",
          "options": null
        },
        {
          "name": "waypoints",
          "type": "number",
          "description": "Dots placed along the route that light up as the marker passes them.",
          "required": false,
          "defaultValue": "4",
          "options": null
        },
        {
          "name": "trail",
          "type": "number",
          "description": "Fading dots left behind the marker. Zero disables the trail.",
          "required": false,
          "defaultValue": "14",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "phone-notification-stack",
      "name": "Phone Notification Stack",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A drawn phone glides up into a settled tilt while notification cards drop in and the stack behind them recedes",
      "tags": [
        "phone",
        "device mockup",
        "notification",
        "lock screen",
        "stack",
        "9:16",
        "ui"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/phone-notification-stack.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useSpringStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/* -------------------------------------------------------------------- schema */\n\nconst ICON_NAMES = [\"message\", \"mail\", \"calendar\", \"spark\", \"bell\", \"check\"] as const;\ntype IconName = (typeof ICON_NAMES)[number];\n\nconst notificationSchema = z.object({\n  app: z\n    .string()\n    .describe(\n      \"Bold title line — the app name or the sender. Under ~18 characters, or it crowds the timestamp on the same line.\",\n    ),\n  message: z\n    .string()\n    .describe(\n      \"The body line. One sentence, two at most: it clamps to two lines the way a real lock screen truncates.\",\n    ),\n  time: z\n    .string()\n    .default(\"now\")\n    .describe(\"Timestamp in the card's top-right corner. Short and unlabelled — 'now', '2m', '14:02'.\"),\n  icon: z\n    .enum(ICON_NAMES)\n    .default(\"message\")\n    .describe(\n      \"Glyph drawn into the accent-tinted app tile: 'message' for chat, 'mail', 'calendar', 'spark' for product/AI events, 'bell' for alerts, 'check' for completions.\",\n    ),\n});\n\nexport const phoneNotificationStackSchema = z.object({\n  notifications: z\n    .array(notificationSchema)\n    .min(1)\n    .max(4)\n    .describe(\n      \"The cards, in the order they land: the first entry arrives first and ends up at the top of the stack, dimming as the later ones drop in. Three reads best; four is the ceiling before the stack runs out of screen.\",\n    ),\n  clock: z\n    .string()\n    .default(\"9:41\")\n    .describe(\"Lock-screen time. Set large under the status row, and again small inside the status row itself.\"),\n  dateLabel: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono line above the big clock, e.g. 'Tuesday 12 March'. Omit for a barer screen.\"),\n  battery: z\n    .number()\n    .min(0)\n    .max(100)\n    .default(78)\n    .describe(\"Battery fill in percent, drawn into the status-row cell. Deliberately never written out as text.\"),\n  kicker: z\n    .string()\n    .optional()\n    .describe(\"Small mono label at the top left of the frame, beside an accent tick. Omit to leave the device alone.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\n      \"One display-face line at the top of the frame. Five to nine words: it wraps to two lines and must not reach down to the device.\",\n    ),\n  tilt: z\n    .number()\n    .min(0)\n    .max(14)\n    .default(7)\n    .describe(\n      \"Settled rotateX in degrees. 0 is a flat mockup; 7 reads as a device held slightly away; past 12 the screen starts to keystone and the type in it softens.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof phoneNotificationStackSchema>;\ntype NotificationItem = z.infer<typeof notificationSchema>;\n\n/* --------------------------------------------------------------------- utils */\n\n/**\n * A brand token at a given alpha. Every tint, hairline and frosted panel in\n * this file goes through here, so the component still has exactly one colour\n * source: swap the brand and the whole device retargets.\n */\nconst alpha = (hex: string, a: number) => {\n  const raw = hex.replace(\"#\", \"\");\n  const full = raw.length === 3 ? raw.split(\"\").map((c) => c + c).join(\"\") : raw;\n  const n = parseInt(full.slice(0, 6), 16);\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;\n};\n\n/** Real iPhone-class body proportion. Height over width. */\nconst DEVICE_RATIO = 2.05;\n\n/**\n * The arrival of one notification. This is internal motion, not presence: the\n * cards land inside the hold window and stay landed, so the component is still\n * visually at rest for the handoff. Pure function of the frame, as required.\n */\nconst arrivalOf = (frame: number, fps: number, start: number) =>\n  spring({\n    frame: Math.max(0, frame - start),\n    fps,\n    config: SPRINGS.snap,\n    durationInFrames: 16,\n  });\n\nconst clampPct = (v: number) => Math.min(100, Math.max(0, v));\n\n/* ----------------------------------------------------------------- component */\n\n/**\n * Phone Notification Stack — a device rising into frame while notifications\n * drop in behind the glass.\n *\n * The composition is two weights: a left-aligned label block in the top margin,\n * and the device bottom-weighted and cropped a hair by the frame edge, nudged\n * off centre so the frame reads as a designed spread rather than a product grid\n * cell. The bloom behind it is the only thing that is centred on the device.\n *\n * Motion is one gesture and one accent. The gesture: the body glides up from\n * below the frame on a heavy spring and settles into its resting tilt — it\n * arrives from where the eye expects a phone to come from, and the tilt is the\n * only thing that says \"device\" rather than \"rectangle\". The accent: the lens\n * inside the Dynamic Island rings in the accent colour on each arrival, which\n * is also the only element that moves once the stack is full.\n *\n * Everything inside the screen — status bar, signal, battery, app tiles — is\n * drawn from divs and inline SVG. No assets, so this file is the whole item.\n */\nconst PhoneNotificationStack: React.FC<Props> = ({\n  notifications,\n  clock,\n  dateLabel,\n  battery,\n  kicker,\n  caption,\n  tilt,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  // Heavy spring: the device has mass, and presence still gates the exit so the\n  // scene hands off cleanly to whatever follows it.\n  const stage = useSpringStage(BUDGETS.scene, \"glide\");\n\n  // One type unit for the frame furniture. Taking the smaller of the width and\n  // a portrait-equivalent of the height means a landscape re-crop scales the\n  // label block down with the device instead of shouting over it.\n  const unit = Math.min(width, height * 0.58);\n  const pad = Math.min(width, height) * 0.075;\n\n  // Device size is driven by the height, then capped so it can never crowd the\n  // side margins in a squarer cut.\n  const phoneH = Math.min(height * 0.78, width * 0.72 * DEVICE_RATIO);\n  const phoneW = phoneH / DEVICE_RATIO;\n  // Cropped by a sliver of the bottom edge: a product shot, not a sticker.\n  const phoneTop = height * 1.015 - phoneH;\n  // Off centre, to answer the left-aligned label block above it.\n  const nudge = width * 0.035;\n\n  const cards = notifications.slice(0, 4);\n  // Arrivals begin just after the body has settled, 15 frames apart — half a\n  // second between notifications, which is how fast a burst actually feels.\n  const arrivals = cards.map((_, i) => BUDGETS.scene.enter + 6 + i * 15);\n\n  const rise = (1 - stage.presence) * (height * 1.06 - phoneTop);\n  const rotateX = interpolate(stage.presence, [0, 1], [tilt + 9, tilt]);\n  const scale = interpolate(stage.presence, [0, 1], [0.955, 1]);\n\n  const kickerSize = unit * 0.022;\n  const captionSize = unit * 0.055;\n  const bloom = width * 1.5;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      {/* Light the device sits in. Reads as a screen glow, not a gradient. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: width / 2 + nudge - bloom / 2,\n          top: phoneTop - height * 0.14,\n          width: bloom,\n          height: bloom,\n          background: `radial-gradient(closest-side, ${alpha(t.accent, 0.26)}, ${alpha(\n            t.accent,\n            0.05,\n          )} 52%, ${alpha(t.accent, 0)} 76%)`,\n          opacity: stage.presence,\n        }}\n      />\n\n      {kicker || caption ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: pad,\n            top: pad,\n            width: width - pad * 2,\n            opacity: stage.presence,\n            transform: `translateY(${(1 - stage.presence) * unit * 0.05}px)`,\n          }}\n        >\n          {kicker ? (\n            <div\n              style={{\n                display: \"flex\",\n                alignItems: \"center\",\n                gap: kickerSize * 0.66,\n                marginBottom: kickerSize * 1.5,\n              }}\n            >\n              <div\n                style={{\n                  width: kickerSize * 0.4,\n                  height: kickerSize * 0.4,\n                  backgroundColor: t.accent,\n                  borderRadius: Math.max(1, kickerSize * 0.06),\n                }}\n              />\n              <span\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: kickerSize,\n                  fontWeight: 600,\n                  letterSpacing: kickerSize * 0.16,\n                  textTransform: \"uppercase\",\n                  color: t.inkMuted,\n                }}\n              >\n                {kicker}\n              </span>\n            </div>\n          ) : null}\n\n          {caption ? (\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: captionSize,\n                fontWeight: 700,\n                lineHeight: 1.08,\n                letterSpacing: captionSize * -0.022,\n                color: t.ink,\n                maxWidth: width * 0.74,\n              }}\n            >\n              {caption}\n            </div>\n          ) : null}\n        </div>\n      ) : null}\n\n      {/* The device. Perspective lives on the wrapper so the tilt is real\n          foreshortening rather than a scaled rectangle. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: phoneTop,\n          width,\n          display: \"flex\",\n          justifyContent: \"center\",\n          perspective: phoneH * 1.5,\n          opacity: stage.presence,\n        }}\n      >\n        <div\n          style={{\n            transform: `translate(${nudge}px, ${rise}px) rotateX(${rotateX}deg) scale(${scale})`,\n            transformOrigin: \"50% 100%\",\n          }}\n        >\n          <Device\n            t={t}\n            w={phoneW}\n            h={phoneH}\n            clock={clock}\n            dateLabel={dateLabel}\n            battery={battery}\n            cards={cards}\n            arrivals={arrivals}\n          />\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* -------------------------------------------------------------------- device */\n\nconst Device: React.FC<{\n  t: BrandTokens;\n  w: number;\n  h: number;\n  clock: string;\n  dateLabel?: string;\n  battery: number;\n  cards: NotificationItem[];\n  arrivals: number[];\n}> = ({ t, w, h, clock, dateLabel, battery, cards, arrivals }) => {\n  // Device radii come from the object, not from `t.radius`: a phone body is\n  // physical geometry, and a 2px corner on a 700px-wide slab reads as a bug.\n  const bezel = w * 0.028;\n  const bodyRadius = w * 0.145;\n  const hairline = Math.max(1, w * 0.0024);\n  const buttonW = Math.max(2, w * 0.009);\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: w,\n        height: h,\n        borderRadius: bodyRadius,\n        boxSizing: \"border-box\",\n        padding: bezel,\n        // Chrome: a shaded body one step off the canvas, with a hairline edge.\n        background: `linear-gradient(152deg, ${alpha(t.ink, 0.13)} 0%, ${alpha(t.ink, 0)} 40%, ${alpha(\n          t.ink,\n          0.07,\n        )} 100%), ${t.surface}`,\n        border: `${hairline}px solid ${alpha(t.ink, 0.16)}`,\n        boxShadow: `0 ${w * 0.02}px ${w * 0.05}px rgba(0,0,0,0.22), 0 ${w * 0.1}px ${\n          w * 0.24\n        }px rgba(0,0,0,0.34)`,\n      }}\n    >\n      {/* Side hardware. Two frames' work, and its absence is the first thing\n          that makes a drawn phone look like a wireframe. */}\n      <SideButton t={t} w={buttonW} h={w * 0.045} top={h * 0.19} side=\"left\" />\n      <SideButton t={t} w={buttonW} h={w * 0.085} top={h * 0.26} side=\"left\" />\n      <SideButton t={t} w={buttonW} h={w * 0.085} top={h * 0.37} side=\"left\" />\n      <SideButton t={t} w={buttonW} h={w * 0.12} top={h * 0.3} side=\"right\" />\n\n      <Screen\n        t={t}\n        w={w}\n        radius={bodyRadius - bezel}\n        clock={clock}\n        dateLabel={dateLabel}\n        battery={battery}\n        cards={cards}\n        arrivals={arrivals}\n      />\n    </div>\n  );\n};\n\nconst SideButton: React.FC<{\n  t: BrandTokens;\n  w: number;\n  h: number;\n  top: number;\n  side: \"left\" | \"right\";\n}> = ({ t, w, h, top, side }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      top,\n      [side]: -w * 0.55,\n      width: w,\n      height: h,\n      borderRadius: w,\n      background: `linear-gradient(90deg, ${alpha(t.ink, 0.22)}, ${alpha(t.ink, 0.1)})`,\n    }}\n  />\n);\n\nconst Screen: React.FC<{\n  t: BrandTokens;\n  w: number;\n  radius: number;\n  clock: string;\n  dateLabel?: string;\n  battery: number;\n  cards: NotificationItem[];\n  arrivals: number[];\n}> = ({ t, w, radius, clock, dateLabel, battery, cards, arrivals }) => {\n  const padX = w * 0.055;\n  const islandH = w * 0.052;\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: \"100%\",\n        height: \"100%\",\n        borderRadius: radius,\n        overflow: \"hidden\",\n        backgroundColor: t.bg,\n        boxShadow: `inset 0 0 0 ${Math.max(1, w * 0.002)}px ${alpha(t.ink, 0.1)}`,\n      }}\n    >\n      {/* Wallpaper: one wash of the accent falling off before it reaches the\n          stack, so the cards keep their contrast. */}\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `linear-gradient(172deg, ${alpha(t.accent, 0.34)} 0%, ${alpha(\n            t.accent,\n            0.1,\n          )} 32%, ${alpha(t.accent, 0)} 60%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"relative\",\n          height: \"100%\",\n          boxSizing: \"border-box\",\n          display: \"flex\",\n          flexDirection: \"column\",\n          padding: `${w * 0.024}px ${padX}px ${w * 0.038}px`,\n        }}\n      >\n        {/* Status row, with the island floating over its centre. */}\n        <div\n          style={{\n            position: \"relative\",\n            height: islandH,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n          }}\n        >\n          <span\n            style={{\n              fontFamily: t.fontBody,\n              fontSize: w * 0.04,\n              fontWeight: 600,\n              letterSpacing: w * -0.0004,\n              color: t.ink,\n              fontVariantNumeric: \"tabular-nums\",\n              paddingLeft: w * 0.03,\n            }}\n          >\n            {clock}\n          </span>\n\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: w * 0.022, paddingRight: w * 0.025 }}>\n            <SignalBars t={t} w={w} />\n            <Wifi t={t} w={w} />\n            <Battery t={t} w={w} level={battery} />\n          </div>\n\n          <Island t={t} w={w} h={islandH} arrivals={arrivals} />\n        </div>\n\n        {/* Lock clock. Centred inside the device, because that is what the\n            artefact looks like — the frame is composed outside the glass. */}\n        <div style={{ marginTop: w * 0.14, textAlign: \"center\" }}>\n          {dateLabel ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: w * 0.033,\n                fontWeight: 500,\n                letterSpacing: w * 0.005,\n                textTransform: \"uppercase\",\n                color: t.inkMuted,\n                marginBottom: w * 0.022,\n              }}\n            >\n              {dateLabel}\n            </div>\n          ) : null}\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: w * 0.215,\n              fontWeight: 700,\n              lineHeight: 1,\n              letterSpacing: w * -0.007,\n              color: t.ink,\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n          >\n            {clock}\n          </div>\n        </div>\n\n        {/* The stack sits on the bottom of the screen, where the eye already\n            expects notifications to pile up. */}\n        <div\n          style={{\n            marginTop: \"auto\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: w * 0.024,\n          }}\n        >\n          {cards.map((item, i) => (\n            <NotificationCard\n              key={`${item.app}-${i}`}\n              t={t}\n              w={w}\n              item={item}\n              start={arrivals[i]}\n              following={arrivals.slice(i + 1)}\n            />\n          ))}\n        </div>\n\n        <div\n          style={{\n            marginTop: w * 0.05,\n            width: w * 0.36,\n            height: Math.max(3, w * 0.0095),\n            borderRadius: w,\n            backgroundColor: alpha(t.ink, 0.5),\n            alignSelf: \"center\",\n          }}\n        />\n      </div>\n    </div>\n  );\n};\n\n/**\n * The Dynamic Island. It holds the component's one accent gesture: the lens\n * rings in the accent colour and the pill breathes a few percent wider as each\n * notification lands, so the hardware acknowledges the software.\n */\nconst Island: React.FC<{ t: BrandTokens; w: number; h: number; arrivals: number[] }> = ({\n  t,\n  w,\n  h,\n  arrivals,\n}) => {\n  const frame = useCurrentFrame();\n  const pulse = arrivals.reduce((acc, start) => {\n    const p = interpolate(frame, [start - 3, start + 3, start + 16], [0, 1, 0], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.travel,\n    });\n    return Math.max(acc, p);\n  }, 0);\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        top: 0,\n        width: w * 0.3 * (1 + pulse * 0.12),\n        height: h,\n        transform: \"translateX(-50%)\",\n        borderRadius: h,\n        backgroundColor: \"#000\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          right: h * 0.26,\n          top: \"50%\",\n          width: h * 0.3,\n          height: h * 0.3,\n          marginTop: h * -0.15,\n          borderRadius: \"50%\",\n          backgroundColor: alpha(t.ink, 0.28),\n          boxShadow: `0 0 0 ${Math.max(1.5, w * 0.0038)}px ${alpha(t.accent, 0.18 + pulse * 0.78)}`,\n        }}\n      />\n    </div>\n  );\n};\n\n/* -------------------------------------------------------------- status glyphs */\n\nconst SignalBars: React.FC<{ t: BrandTokens; w: number }> = ({ t, w }) => {\n  const bar = Math.max(2, w * 0.011);\n  return (\n    <div style={{ display: \"flex\", alignItems: \"flex-end\", gap: w * 0.008 }}>\n      {[0, 1, 2, 3].map((i) => (\n        <div\n          key={i}\n          style={{\n            width: bar,\n            height: w * 0.013 + i * w * 0.0068,\n            borderRadius: bar * 0.4,\n            // The last bar is unlit: a full-strength four-bar icon reads as a\n            // placeholder, a three-of-four reads as a real screenshot.\n            backgroundColor: i < 3 ? t.ink : alpha(t.ink, 0.3),\n          }}\n        />\n      ))}\n    </div>\n  );\n};\n\nconst Wifi: React.FC<{ t: BrandTokens; w: number }> = ({ t, w }) => {\n  const s = w * 0.046;\n  return (\n    <svg width={s} height={s * 0.7} viewBox=\"0 0 20 14\" fill=\"none\" aria-hidden>\n      <path d=\"M1.6 5.6Q10 -1.4 18.4 5.6\" stroke={t.ink} strokeWidth={2.1} strokeLinecap=\"round\" />\n      <path d=\"M5.2 8.8Q10 4.7 14.8 8.8\" stroke={t.ink} strokeWidth={2.1} strokeLinecap=\"round\" />\n      <circle cx=\"10\" cy=\"12\" r=\"1.6\" fill={t.ink} />\n    </svg>\n  );\n};\n\nconst Battery: React.FC<{ t: BrandTokens; w: number; level: number }> = ({ t, w, level }) => {\n  const h = w * 0.031;\n  const body = h * 2.05;\n  const border = Math.max(1.5, w * 0.0035);\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: border * 1.1 }}>\n      <div\n        style={{\n          width: body,\n          height: h,\n          boxSizing: \"border-box\",\n          borderRadius: h * 0.34,\n          border: `${border}px solid ${alpha(t.ink, 0.45)}`,\n          padding: border * 1.5,\n        }}\n      >\n        <div\n          style={{\n            width: `${clampPct(level)}%`,\n            height: \"100%\",\n            borderRadius: h * 0.14,\n            backgroundColor: t.ink,\n          }}\n        />\n      </div>\n      <div\n        style={{\n          width: border * 1.3,\n          height: h * 0.36,\n          borderRadius: border,\n          backgroundColor: alpha(t.ink, 0.45),\n        }}\n      />\n    </div>\n  );\n};\n\n/* --------------------------------------------------------------------- cards */\n\n/**\n * One notification. It drops in from just above its slot on a crisp spring,\n * and every card already in the stack recedes — a little smaller, a little\n * dimmer, a little higher — by one step for each card that lands after it.\n * That recession is what turns a list into a stack.\n */\nconst NotificationCard: React.FC<{\n  t: BrandTokens;\n  w: number;\n  item: NotificationItem;\n  start: number;\n  following: number[];\n}> = ({ t, w, item, start, following }) => {\n  const frame = useCurrentFrame();\n  const { fps } = useVideoConfig();\n\n  const arrival = arrivalOf(frame, fps, start);\n  // Continuous, not stepped: the recession animates with the arrival that\n  // causes it instead of snapping when a card lands.\n  const recede = following.reduce((acc, s) => acc + arrivalOf(frame, fps, s), 0);\n\n  const padding = w * 0.048;\n  const tile = w * 0.125;\n  // Short travel, and opaque well before it lands: a translucent card crossing\n  // the one above it doubles the type for a few frames and reads as a mistake.\n  const drop = (tile + padding * 2) * 0.62;\n  const hairline = Math.max(1, w * 0.0024);\n\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-start\",\n        gap: padding * 0.8,\n        padding,\n        boxSizing: \"border-box\",\n        borderRadius: w * 0.062,\n        backgroundColor: alpha(t.surface, 0.94),\n        border: `${hairline}px solid ${alpha(t.ink, 0.1)}`,\n        boxShadow: `0 ${w * 0.012}px ${w * 0.04}px rgba(0,0,0,0.22)`,\n        opacity: Math.min(1, arrival * 2.6) * (1 - recede * 0.16),\n        transform: `translateY(${(1 - arrival) * -drop - recede * w * 0.006}px) scale(${\n          (0.965 + 0.035 * arrival) * (1 - recede * 0.028)\n        })`,\n        transformOrigin: \"50% 50%\",\n      }}\n    >\n      <AppTile t={t} w={w} size={tile} icon={item.icon} />\n\n      <div style={{ flex: 1, minWidth: 0 }}>\n        <div style={{ display: \"flex\", alignItems: \"baseline\", gap: w * 0.02 }}>\n          <span\n            style={{\n              flex: 1,\n              minWidth: 0,\n              fontFamily: t.fontBody,\n              fontSize: w * 0.046,\n              fontWeight: 600,\n              letterSpacing: w * -0.0006,\n              color: t.ink,\n              whiteSpace: \"nowrap\",\n              overflow: \"hidden\",\n              textOverflow: \"ellipsis\",\n            }}\n          >\n            {item.app}\n          </span>\n          <span\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: w * 0.033,\n              fontWeight: 500,\n              color: t.inkMuted,\n              flexShrink: 0,\n            }}\n          >\n            {item.time}\n          </span>\n        </div>\n\n        <div\n          style={{\n            marginTop: w * 0.012,\n            fontFamily: t.fontBody,\n            fontSize: w * 0.042,\n            fontWeight: 400,\n            lineHeight: 1.34,\n            color: t.inkMuted,\n            display: \"-webkit-box\",\n            WebkitBoxOrient: \"vertical\",\n            WebkitLineClamp: 2,\n            overflow: \"hidden\",\n          }}\n        >\n          {item.message}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nconst AppTile: React.FC<{ t: BrandTokens; w: number; size: number; icon: IconName }> = ({\n  t,\n  w,\n  size,\n  icon,\n}) => (\n  <div\n    style={{\n      width: size,\n      height: size,\n      flexShrink: 0,\n      borderRadius: size * 0.28,\n      backgroundColor: alpha(t.accent, 0.16),\n      border: `${Math.max(1, w * 0.002)}px solid ${alpha(t.accent, 0.32)}`,\n      display: \"grid\",\n      placeItems: \"center\",\n    }}\n  >\n    <svg\n      width={size * 0.56}\n      height={size * 0.56}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      stroke={t.accent}\n      strokeWidth={1.8}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      aria-hidden\n    >\n      {GLYPHS[icon]}\n    </svg>\n  </div>\n);\n\n/**\n * App glyphs, drawn rather than imported. Six is enough vocabulary for a\n * notification stack, and every one of them is two elements or fewer so they\n * hold their weight against each other at any size.\n */\nconst GLYPHS: Record<IconName, React.ReactNode> = {\n  message: (\n    <>\n      <rect x=\"3.2\" y=\"4.8\" width=\"17.6\" height=\"12.4\" rx=\"4\" />\n      <path d=\"M8.6 17.2v3.9l4.6-3.9\" />\n    </>\n  ),\n  mail: (\n    <>\n      <rect x=\"3\" y=\"5.4\" width=\"18\" height=\"13.2\" rx=\"3\" />\n      <path d=\"M4.4 8.2 12 14l7.6-5.8\" />\n    </>\n  ),\n  calendar: (\n    <>\n      <rect x=\"3.4\" y=\"6\" width=\"17.2\" height=\"14.6\" rx=\"3.4\" />\n      <path d=\"M8.2 3.4v4.4M15.8 3.4v4.4M3.4 11.2h17.2\" />\n    </>\n  ),\n  spark: <path d=\"M12 3.4l2 5.4 5.4 2-5.4 2-2 5.4-2-5.4-5.4-2 5.4-2z\" />,\n  bell: (\n    <>\n      <path d=\"M6.8 10.4a5.2 5.2 0 0 1 10.4 0c0 4.1 1.5 5.3 2 6.2H4.8c.5-.9 2-2.1 2-6.2Z\" />\n      <path d=\"M9.9 19.4a2.3 2.3 0 0 0 4.2 0\" />\n    </>\n  ),\n  check: (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"8.6\" />\n      <path d=\"M8.3 12.3l2.7 2.7 5-5.6\" />\n    </>\n  ),\n};\n\n/* ------------------------------------------------------------------ registry */\n\nexport default defineMotionItem({\n  id: \"phone-notification-stack\",\n  name: \"Phone Notification Stack\",\n  category: \"product\",\n  summary: \"A drawn phone glides up into a settled tilt while notification cards drop in and the stack behind them recedes\",\n  tags: [\"phone\", \"device mockup\", \"notification\", \"lock screen\", \"stack\", \"9:16\", \"ui\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: phoneNotificationStackSchema,\n  defaults: {\n    notifications: [\n      {\n        app: \"Deploy\",\n        message: \"Build 2.4.1 is live in production.\",\n        time: \"now\",\n        icon: \"spark\",\n      },\n      {\n        app: \"Ada Ferraro\",\n        message: \"Ship it — the release notes read beautifully.\",\n        time: \"now\",\n        icon: \"message\",\n      },\n      {\n        app: \"Calendar\",\n        message: \"Launch retro at 4:30 with the whole team.\",\n        time: \"now\",\n        icon: \"calendar\",\n      },\n    ],\n    clock: \"9:41\",\n    dateLabel: \"Tuesday 12 March\",\n    battery: 78,\n    kicker: \"Push\",\n    caption: \"Everyone knows the moment it ships.\",\n    tilt: 7,\n    brand: \"slate\",\n  },\n  component: PhoneNotificationStack,\n  file: \"src/registry/items/phone-notification-stack.tsx\",\n  origin: {\n    prompt:\n      \"A 9:16 product shot of a phone rising up into frame and settling with a slight backward tilt, drawn entirely in code — thick bezel, Dynamic Island cutout, a status bar with signal bars and a battery — and then three notification cards sliding down and stacking on the lock screen, each with an accent-tinted app icon, a bold title, a message line and a 'now' timestamp, staggered, with the cards behind scaling down and dimming as new ones land.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A 9:16 product shot of a phone rising up into frame and settling with a slight backward tilt, drawn entirely in code — thick bezel, Dynamic Island cutout, a status bar with signal bars and a battery — and then three notification cards sliding down and stacking on the lock screen, each with an accent-tinted app icon, a bold title, a message line and a 'now' timestamp, staggered, with the cards behind scaling down and dimming as new ones land.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3141,
      "props": [
        {
          "name": "notifications",
          "type": "array",
          "description": "The cards, in the order they land: the first entry arrives first and ends up at the top of the stack, dimming as the later ones drop in. Three reads best; four is the ceiling before the stack runs out of screen.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "clock",
          "type": "string",
          "description": "Lock-screen time. Set large under the status row, and again small inside the status row itself.",
          "required": false,
          "defaultValue": "9:41",
          "options": null
        },
        {
          "name": "dateLabel",
          "type": "string",
          "description": "Small uppercase mono line above the big clock, e.g. 'Tuesday 12 March'. Omit for a barer screen.",
          "required": false,
          "defaultValue": "Tuesday 12 March",
          "options": null
        },
        {
          "name": "battery",
          "type": "number",
          "description": "Battery fill in percent, drawn into the status-row cell. Deliberately never written out as text.",
          "required": false,
          "defaultValue": "78",
          "options": null
        },
        {
          "name": "kicker",
          "type": "string",
          "description": "Small mono label at the top left of the frame, beside an accent tick. Omit to leave the device alone.",
          "required": false,
          "defaultValue": "Push",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "One display-face line at the top of the frame. Five to nine words: it wraps to two lines and must not reach down to the device.",
          "required": false,
          "defaultValue": "Everyone knows the moment it ships.",
          "options": null
        },
        {
          "name": "tilt",
          "type": "number",
          "description": "Settled rotateX in degrees. 0 is a flat mockup; 7 reads as a device held slightly away; past 12 the screen starts to keystone and the type in it softens.",
          "required": false,
          "defaultValue": "7",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "reel-hook-frame",
      "name": "Reel Hook Frame",
      "category": "social",
      "categoryLabel": "Social formats",
      "summary": "A vertical opener whose hook lands inside twenty frames, laid out clear of the platform's top and bottom chrome",
      "tags": [
        "social",
        "vertical",
        "hook",
        "safe-area",
        "reels"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/reel-hook-frame.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const reelHookFrameSchema = z.object({\n  hook: z\n    .string()\n    .default(\"You are rendering video the slow way\")\n    .describe(\"The hook. It has to land in under a second, so keep it short and concrete.\"),\n  support: z\n    .string()\n    .default(\"Here is what changed\")\n    .describe(\"Supporting line beneath the hook.\"),\n  nudge: z.string().default(\"Swipe up\").describe(\"Bottom affordance text.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof reelHookFrameSchema>;\n\n/**\n * A vertical opener built for the first second.\n *\n * Two constraints drive the layout. Platform chrome eats roughly the top 12% and\n * the bottom 20% of a 9:16 frame — captions, handles, action rails — so content\n * outside those insets is content nobody sees. And it has to be readable with the\n * sound off, which is why the hook is type rather than a voiceover line: the\n * words arrive within about 20 frames, faster than anywhere else in this library.\n */\nconst TOP_SAFE = 0.12;\nconst BOTTOM_SAFE = 0.2;\n\nconst ReelHookFrame: React.FC<Props> = ({ hook, support, nudge, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  const words = hook.split(/\\s+/).filter(Boolean);\n  const size = width * (words.length > 6 ? 0.105 : 0.125);\n\n  const supportIn = interpolate(stage.frame, [14, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // A slow loop on the nudge so the bottom of the frame never sits dead.\n  const bob = Math.sin(stage.progress * Math.PI * 2 * 3) * height * 0.006;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          backgroundImage: `linear-gradient(165deg, ${t.surface} 0%, ${t.bg} 48%, ${t.accent}26 100%)`,\n        }}\n      />\n      <Grain width={width} height={height} frame={stage.frame} />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: width * 0.08,\n          right: width * 0.08,\n          top: height * (TOP_SAFE + 0.06),\n          opacity: stage.presence,\n        }}\n      >\n        <div style={{ display: \"flex\", flexWrap: \"wrap\", gap: `0 ${size * 0.24}px` }}>\n          {words.map((w, i) => (\n            <HookWord key={`${w}-${i}`} word={w} index={i} size={size} tokens={t} />\n          ))}\n        </div>\n\n        <div\n          style={{\n            marginTop: size * 0.5,\n            fontFamily: t.fontMono,\n            fontSize: size * 0.24,\n            letterSpacing: size * 0.03,\n            textTransform: \"uppercase\",\n            color: t.accent,\n            opacity: supportIn,\n            transform: `translateY(${(1 - supportIn) * size * 0.2}px)`,\n          }}\n        >\n          {support}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          bottom: height * (BOTTOM_SAFE - 0.06),\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          gap: height * 0.012,\n          opacity: stage.presence * 0.85,\n          transform: `translateY(${bob}px)`,\n        }}\n      >\n        <svg width={width * 0.07} height={width * 0.07} viewBox=\"0 0 24 24\">\n          <path\n            d=\"M12 19V5 M6 11l6-6 6 6\"\n            fill=\"none\"\n            stroke={t.ink}\n            strokeWidth={2.2}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n        <span\n          style={{\n            fontFamily: t.fontMono,\n            fontSize: width * 0.032,\n            letterSpacing: width * 0.004,\n            textTransform: \"uppercase\",\n            color: t.ink,\n          }}\n        >\n          {nudge}\n        </span>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst HookWord: React.FC<{\n  word: string;\n  index: number;\n  size: number;\n  tokens: BrandTokens;\n}> = ({ word, index, size, tokens }) => {\n  // 2-frame stagger: the whole hook is legible inside ~20 frames.\n  const stage = useStaggeredStage(BUDGETS.scene, index, 2);\n  const y = interpolate(stage.enter, [0, 1], [100, 0]);\n  return (\n    <span style={{ display: \"block\", overflow: \"hidden\", paddingBottom: size * 0.06 }}>\n      <span\n        style={{\n          display: \"block\",\n          fontFamily: tokens.fontDisplay,\n          fontSize: size,\n          fontWeight: 700,\n          lineHeight: 1.04,\n          letterSpacing: size * -0.026,\n          color: tokens.ink,\n          transform: `translateY(${y}%)`,\n        }}\n      >\n        {word}\n      </span>\n    </span>\n  );\n};\n\nconst Grain: React.FC<{ width: number; height: number; frame: number }> = ({\n  width,\n  height,\n  frame,\n}) => {\n  const cell = width / 34;\n  const rows = Math.ceil(height / cell);\n  return (\n    <svg width={width} height={height} style={{ position: \"absolute\", inset: 0, opacity: 0.16 }}>\n      {Array.from({ length: rows }, (_, r) =>\n        Array.from({ length: 34 }, (_, c) => {\n          const a = random(`reel-${frame}-${r}-${c}`);\n          if (a < 0.72) return null;\n          return (\n            <rect\n              key={`${r}-${c}`}\n              x={c * cell}\n              y={r * cell}\n              width={cell}\n              height={cell}\n              fill=\"#ffffff\"\n              opacity={(a - 0.72) * 0.5}\n            />\n          );\n        }),\n      )}\n    </svg>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"reel-hook-frame\",\n  name: \"Reel Hook Frame\",\n  category: \"social\",\n  summary:\n    \"A vertical opener whose hook lands inside twenty frames, laid out clear of the platform's top and bottom chrome\",\n  tags: [\"social\", \"vertical\", \"hook\", \"safe-area\", \"reels\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: reelHookFrameSchema,\n  defaults: {\n    hook: \"You are rendering video the slow way\",\n    support: \"Here is what changed\",\n    nudge: \"Swipe up\",\n    brand: \"slate\",\n  },\n  component: ReelHookFrame,\n  file: \"src/registry/items/reel-hook-frame.tsx\",\n  origin: {\n    prompt:\n      \"A 9:16 opener built for the first second: hook copy set large in the upper third above the caption zone, a supporting line, and a looping swipe-up nudge at the bottom, readable with sound off.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A 9:16 opener built for the first second: hook copy set large in the upper third above the caption zone, a supporting line, and a looping swipe-up nudge at the bottom, readable with sound off.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.8248,
      "props": [
        {
          "name": "hook",
          "type": "string",
          "description": "The hook. It has to land in under a second, so keep it short and concrete.",
          "required": false,
          "defaultValue": "You are rendering video the slow way",
          "options": null
        },
        {
          "name": "support",
          "type": "string",
          "description": "Supporting line beneath the hook.",
          "required": false,
          "defaultValue": "Here is what changed",
          "options": null
        },
        {
          "name": "nudge",
          "type": "string",
          "description": "Bottom affordance text.",
          "required": false,
          "defaultValue": "Swipe up",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "ribbon-wave",
      "name": "Ribbon Wave",
      "category": "background",
      "categoryLabel": "Backgrounds",
      "summary": "A volumetric ribbon of phase-shifted strands travelling across frame, built outside-in with three depth groups and a moving hot spot",
      "tags": [
        "background",
        "ribbon",
        "wave",
        "volumetric",
        "hero",
        "atmosphere",
        "premium"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/ribbon-wave.tsx",
      "source": "import React from \"react\";\nimport {\n  AbsoluteFill,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const ribbonWaveSchema = z.object({\n  headline: z\n    .string()\n    .optional()\n    .describe(\"Optional line over the ribbon. Omit to use this purely as a background layer under another component.\"),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small tracked label above the headline. Ignored when there is no headline.\"),\n  amplitude: z\n    .number()\n    .min(0.02)\n    .max(0.3)\n    .default(0.12)\n    .describe(\"Wave height as a fraction of frame height. Above 0.2 the ribbon starts leaving the frame.\"),\n  density: z\n    .number()\n    .int()\n    .min(16)\n    .max(96)\n    .default(64)\n    .describe(\"Number of strands in the ribbon. More strands read as a solid surface; fewer read as lines. 64 is the point where it becomes a surface.\"),\n  speed: z\n    .number()\n    .min(0)\n    .max(2)\n    .default(0.55)\n    .describe(\"Travel speed of the wave. 0 freezes it into a static sculpture.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof ribbonWaveSchema>;\n\nconst BUDGET = BUDGETS.feature;\nconst SAMPLES = 72;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.83\"\n        numOctaves={3}\n        seed={Math.floor(frame / 2) % 64}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * One strand of the ribbon.\n *\n * The ribbon is a stack of open curves, not a filled shape, because striation\n * is what gives it a readable surface: a filled blob of gradient has no\n * indication of which way it is twisting. Each strand is phase-shifted from its\n * neighbour, so the stack shears as it travels and the surface appears to\n * rotate without any 3D transform being involved.\n */\nconst strandPath = (\n  i: number,\n  strands: number,\n  w: number,\n  cy: number,\n  amp: number,\n  spacing: number,\n  phase: number,\n) => {\n  const shear = i * 0.055;\n  let d = \"\";\n  for (let s = 0; s <= SAMPLES; s += 1) {\n    const u = s / SAMPLES;\n    const x = -w * 0.08 + u * w * 1.16;\n    // Two waves of different wavelength, plus a travelling waist that pinches\n    // the whole ribbon. One sine alone reads as a corrugated sheet.\n    const waist = 0.42 + 0.58 * Math.abs(Math.sin(u * 2.1 + phase * 0.42));\n    const y =\n      cy +\n      (i - strands / 2) * spacing +\n      Math.sin(u * 5.4 + phase + shear) * amp * waist +\n      Math.sin(u * 2.1 - phase * 0.7 + shear * 0.5) * amp * 0.36;\n    d += `${s === 0 ? \"M\" : \"L\"}${x.toFixed(1)} ${y.toFixed(1)}`;\n  }\n  return d;\n};\n\nconst RibbonWave: React.FC<Props> = ({ headline, eyebrow, amplitude, density, speed, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  const amp = height * amplitude;\n  const spacing = height * 0.0058;\n  const cy = height * (headline ? 0.63 : 0.5);\n  const phase = time * speed;\n\n  /**\n   * The ribbon builds by strand rather than by fading in: strands arrive from\n   * the outside of the stack toward the middle, so the surface appears to\n   * thicken into existence. A group opacity fade would have looked like a\n   * dissolve, which is not how a physical thing arrives.\n   */\n  const build = interpolate(frame, [0, BUDGET.enter * 2.2], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // Three depth groups. The middle one is sharp; the outer two are progressively\n  // defocused, which reads as thickness rather than as a blurred copy.\n  const groups = [\n    { from: 0, to: 0.28, blur: width * 0.009, opacity: 0.55 },\n    { from: 0.28, to: 0.72, blur: 0, opacity: 1 },\n    { from: 0.72, to: 1, blur: width * 0.005, opacity: 0.7 },\n  ];\n\n  const headIn = interpolate(frame, [14, 44], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const eyebrowIn = interpolate(frame, [6, 28], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  // The camera rolls a fraction of a degree across the shot. Almost invisible,\n  // and the reason the ribbon never looks like a screensaver.\n  const roll = interpolate(stage.progress, [0, 1], [-0.8, 0.8]);\n  const push = interpolate(stage.progress, [0, 1], [1.04, 1]);\n\n  // A hot spot travelling along the ribbon, standing in for a light the strands\n  // are passing through.\n  const hotX = 0.5 + Math.sin(time * 0.31) * 0.34;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(100% 62% at 50% 8%, ${alpha(t.surface, 0.7)} 0%, ${alpha(\n            t.surface,\n            0.24,\n          )} 36%, ${alpha(t.bg, 0.9)} 70%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `rotate(${roll}deg) scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {groups.map((g, gi) => (\n          <svg\n            key={gi}\n            width={width}\n            height={height}\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              filter: g.blur > 0 ? `blur(${g.blur}px)` : \"none\",\n              opacity: g.opacity,\n              mixBlendMode: \"screen\",\n            }}\n            aria-hidden\n          >\n            {Array.from({ length: density }).map((_, k) => {\n              const u = k / (density - 1);\n              if (u < g.from || u >= g.to) return null;\n\n              // Strands arrive outside-in.\n              const arrive = Math.abs(u - 0.5) * 2;\n              const on = interpolate(build, [arrive * 0.55, arrive * 0.55 + 0.45], [0, 1], {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n              });\n              if (on <= 0.001) return null;\n\n              const colour = interpolateColors(u, [0, 0.5, 1], [t.accent, t.accentAlt, t.accent]);\n              // The stack fades at its own edges so the ribbon has no cut end.\n              const edge = Math.sin(u * Math.PI) ** 0.7;\n\n              return (\n                <path\n                  key={k}\n                  d={strandPath(k, density, width, cy, amp, spacing, phase)}\n                  fill=\"none\"\n                  stroke={colour}\n                  strokeWidth={Math.max(1, height * 0.0016)}\n                  strokeLinecap=\"round\"\n                  opacity={0.5 * edge * on}\n                />\n              );\n            })}\n          </svg>\n        ))}\n\n        {/* the travelling hot spot */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: hotX * width,\n            top: cy,\n            width: width * 0.5,\n            height: height * 0.42,\n            marginLeft: -width * 0.25,\n            marginTop: -height * 0.21,\n            background: `radial-gradient(circle, ${alpha(\"#fff\", 0.16)} 0%, ${alpha(\n              \"#fff\",\n              0.05,\n            )} 30%, ${alpha(\"#fff\", 0)} 64%)`,\n            mixBlendMode: \"screen\",\n            opacity: build,\n          }}\n        />\n\n        {headline ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: width * 0.09,\n              right: width * 0.09,\n              top: height * 0.17,\n              textAlign: \"center\",\n            }}\n          >\n            {eyebrow ? (\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: width * 0.0135,\n                  fontWeight: 500,\n                  letterSpacing: width * 0.0034,\n                  textTransform: \"uppercase\",\n                  color: t.accent,\n                  opacity: eyebrowIn,\n                  marginBottom: width * 0.018,\n                }}\n              >\n                {eyebrow}\n              </div>\n            ) : null}\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: width * 0.072,\n                fontWeight: 700,\n                lineHeight: 1.04,\n                letterSpacing: width * -0.0026,\n                color: t.ink,\n                opacity: headIn,\n                transform: `translateY(${interpolate(headIn, [0, 1], [width * 0.018, 0])}px)`,\n                filter: `blur(${interpolate(headIn, [0, 1], [width * 0.005, 0])}px)`,\n              }}\n            >\n              {headline}\n            </div>\n          </div>\n        ) : null}\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(120% 88% at 50% 48%, ${alpha(\"#000\", 0)} 32%, ${alpha(\n            \"#000\",\n            0.14,\n          )} 62%, ${alpha(\"#000\", 0.44)} 90%, ${alpha(\"#000\", 0.62)} 100%)`,\n        }}\n      />\n      <Grain id=\"ribbon-wave-grain\" frame={frame} opacity={0.08} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"ribbon-wave\",\n  name: \"Ribbon Wave\",\n  category: \"background\",\n  summary:\n    \"A volumetric ribbon of phase-shifted strands travelling across frame, built outside-in with three depth groups and a moving hot spot\",\n  tags: [\"background\", \"ribbon\", \"wave\", \"volumetric\", \"hero\", \"atmosphere\", \"premium\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: ribbonWaveSchema,\n  defaults: {\n    headline: \"Composition, in time\",\n    eyebrow: \"Motionstash\",\n    amplitude: 0.12,\n    density: 64,\n    speed: 0.55,\n    brand: \"circuit\",\n  },\n  component: RibbonWave,\n  file: \"src/registry/items/ribbon-wave.tsx\",\n  origin: {\n    prompt:\n      \"A flowing volumetric ribbon across a wide frame, the kind used behind a silicon or OS announcement. Build it from many thin phase-shifted strands rather than one filled shape, so the surface has visible striation and appears to twist. Give it a travelling waist so it pinches and swells, split it into depth groups with different blur so it reads as thick, and pass a soft hot spot along it. It has to work both as a hero with a headline and as a background layer on its own.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A flowing volumetric ribbon across a wide frame, the kind used behind a silicon or OS announcement. Build it from many thin phase-shifted strands rather than one filled shape, so the surface has visible striation and appears to twist. Give it a travelling waist so it pinches and swells, split it into depth groups with different blur so it reads as thick, and pass a soft hot spot along it. It has to work both as a hero with a headline and as a background layer on its own.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7648,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "Optional line over the ribbon. Omit to use this purely as a background layer under another component.",
          "required": false,
          "defaultValue": "Composition, in time",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small tracked label above the headline. Ignored when there is no headline.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "amplitude",
          "type": "number",
          "description": "Wave height as a fraction of frame height. Above 0.2 the ribbon starts leaving the frame.",
          "required": false,
          "defaultValue": "0.12",
          "options": null
        },
        {
          "name": "density",
          "type": "number",
          "description": "Number of strands in the ribbon. More strands read as a solid surface; fewer read as lines. 64 is the point where it becomes a surface.",
          "required": false,
          "defaultValue": "64",
          "options": null
        },
        {
          "name": "speed",
          "type": "number",
          "description": "Travel speed of the wave. 0 freezes it into a static sculpture.",
          "required": false,
          "defaultValue": "0.55",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "scramble-decode",
      "name": "Scramble Decode",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "A phrase resolves out of cycling glyphs left to right, each character locking to full contrast on a staggered beat",
      "tags": [
        "scramble",
        "decode",
        "monospace",
        "kinetic type",
        "glitch",
        "terminal"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/scramble-decode.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, interpolateColors, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\n/**\n * Glyph pools. Each is a fixed string, so the set a character cycles through is\n * part of the component's declared API rather than an accident of the font.\n * All four are ASCII on purpose: Geist Mono covers them at a single advance\n * width, which is what keeps the line from breathing while it scrambles.\n */\nconst CHARSETS = {\n  alphanumeric: \"ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\",\n  symbols: \"#%&@$*+=<>?/\\\\|{}[]~^\",\n  hex: \"0123456789ABCDEF\",\n  binary: \"01\",\n} as const;\n\n/** Side padding as a fraction of frame width. */\nconst PAD = 0.09;\n/** Cell advance as a fraction of font size — Geist Mono's 0.6em plus tracking. */\nconst CELL = 0.66;\n/** Frames between glyph swaps. 2 at 30fps ≈ 15 changes/second: busy, not noise. */\nconst CYCLE = 2;\n/** The decode window, inside the `statement` budget's 90 frames. */\nconst DECODE_START = 6;\nconst DECODE_END = 46;\n\nexport const scrambleDecodeSchema = z.object({\n  text: z\n    .string()\n    .min(1)\n    .describe(\n      \"The phrase that resolves. Uppercase reads best in mono; spaces never scramble, so word shape is visible from the first frame. Up to ~24 characters keeps the type large.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase label above the line, in the accent colour. Omit for a bare decode.\"),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase caption under the rule, bottom-left. Good for a source, a brand or a channel name.\"),\n  charset: z\n    .enum([\"alphanumeric\", \"symbols\", \"hex\", \"binary\"])\n    .default(\"alphanumeric\")\n    .describe(\n      \"Which glyphs a character cycles through before it locks. 'alphanumeric' is the classic decode; 'hex' and 'binary' read as machine output; 'symbols' is the noisiest.\",\n    ),\n  lockStagger: z\n    .number()\n    .min(1)\n    .max(6)\n    .default(3)\n    .describe(\n      \"Frames between one character locking and the next, left to right. 2–4 reads as one sweeping gesture; above that the line resolves letter by letter. Compressed automatically for long strings so the decode always finishes inside the hold.\",\n    ),\n  grid: z\n    .boolean()\n    .default(true)\n    .describe(\"Show the monospace cell grid as hairlines behind the type. Each line fades as its column locks.\"),\n  showProgress: z\n    .boolean()\n    .default(true)\n    .describe(\"Show a percentage readout, right-aligned to the end of the line, that turns accent at 100%.\"),\n  align: z\n    .enum([\"left\", \"center\"])\n    .default(\"left\")\n    .describe(\"Position of the block in the frame. 'left' is the editorial default; 'center' suits 1:1 and 9:16.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof scrambleDecodeSchema>;\n\nconst ScrambleDecode: React.FC<Props> = ({\n  text,\n  eyebrow,\n  footnote,\n  charset,\n  lockStagger,\n  grid,\n  showProgress,\n  align,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n\n  const parsed = Array.from(text);\n  const chars = parsed.length > 0 ? parsed : [\" \"];\n  const n = chars.length;\n\n  // Type scale: the smaller of a statement size and whatever fits the column.\n  // Because every glyph sits in a fixed-width cell, the line's width is known\n  // exactly — `n * size * CELL` — before a single character has resolved. That\n  // is the whole reason this effect can be trusted: nothing reflows, ever.\n  const size = Math.min(width * 0.085, (width * (1 - PAD * 2)) / (n * CELL));\n  const cell = size * CELL;\n  const hair = Math.max(1, size * 0.012);\n  const eyebrowSize = size * 0.155;\n  const metaSize = size * 0.145;\n\n  // Lock points march left to right. For long strings the stagger compresses so\n  // the last character still lands inside DECODE_END, which keeps the tail of\n  // the hold a fully resolved still rather than a halfway state.\n  const perChar = n > 1 ? Math.min(lockStagger, (DECODE_END - DECODE_START) / (n - 1)) : lockStagger;\n  const lockFrames = chars.map(\n    (_, i) => DECODE_START + i * perChar + (random(`scramble-lock-${text}-${i}`) - 0.5) * perChar * 0.9,\n  );\n\n  // The read head, in cells: each character contributes a 0→1 ramp after its own\n  // lock, so the sum is a smooth monotone position rather than a stair. The rule\n  // and the percentage both read from this, so they can never disagree.\n  const head = lockFrames.reduce(\n    (acc, lock) =>\n      acc +\n      interpolate(stage.frame, [lock, lock + perChar], [0, 1], {\n        extrapolateLeft: \"clamp\",\n        extrapolateRight: \"clamp\",\n      }),\n    0,\n  );\n  const decoded = head / n;\n\n  const pct = String(Math.round(decoded * 100)).padStart(3, \"0\");\n  const pctColor = interpolateColors(\n    interpolate(decoded, [0.86, 1], [0, 1], { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" }),\n    [0, 1],\n    [t.inkMuted, t.accent],\n  );\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n        paddingLeft: width * PAD,\n        paddingRight: width * PAD,\n        // Sits a hair above optical centre, so the block reads as placed.\n        paddingBottom: height * 0.03,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n          opacity: stage.presence,\n          transform: `translateY(${interpolate(stage.enter, [0, 1], [size * 0.12, 0])}px)`,\n        }}\n      >\n        {eyebrow ? (\n          <div style={{ display: \"flex\", alignItems: \"center\", marginBottom: eyebrowSize * 1.7 }}>\n            <div\n              style={{\n                width: eyebrowSize * 2.6,\n                height: Math.max(1, eyebrowSize * 0.11),\n                backgroundColor: t.accent,\n                marginRight: eyebrowSize,\n                transform: `scaleX(${stage.enter})`,\n                transformOrigin: \"left\",\n              }}\n            />\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: eyebrowSize,\n                fontWeight: 500,\n                letterSpacing: eyebrowSize * 0.18,\n                textTransform: \"uppercase\",\n                color: t.accent,\n              }}\n            >\n              {eyebrow}\n            </span>\n          </div>\n        ) : null}\n\n        <div style={{ position: \"relative\", display: \"flex\", height: size * 1.18 }}>\n          {grid\n            ? // One hairline per cell boundary. They vanish left to right with the\n              // read head: the grid is scaffolding, and scaffolding comes down.\n              Array.from({ length: n + 1 }).map((_, i) => {\n                const lock = lockFrames[Math.min(i, n - 1)];\n                const gone = interpolate(stage.frame, [lock, lock + perChar * 2], [0, 1], {\n                  extrapolateLeft: \"clamp\",\n                  extrapolateRight: \"clamp\",\n                  easing: EASING.travel,\n                });\n                return (\n                  <div\n                    key={`cell-rule-${i}`}\n                    style={{\n                      position: \"absolute\",\n                      left: i * cell,\n                      top: 0,\n                      height: \"100%\",\n                      width: Math.max(1, size * 0.008),\n                      backgroundColor: t.rule,\n                      opacity: (1 - gone) * 0.8,\n                    }}\n                  />\n                );\n              })\n            : null}\n\n          {chars.map((ch, i) => (\n            <Glyph\n              key={`glyph-${i}`}\n              target={ch}\n              frame={stage.frame}\n              lockAt={lockFrames[i]}\n              pool={CHARSETS[charset]}\n              size={size}\n              cell={cell}\n              seed={`scramble-${text}-${i}`}\n              tokens={t}\n            />\n          ))}\n        </div>\n\n        {/* The rule doubles as the progress track: the accent is drawn exactly\n            under the part of the line that has resolved, and at rest it is a\n            full-width accent hairline under the type. */}\n        <div\n          style={{\n            alignSelf: \"stretch\",\n            position: \"relative\",\n            marginTop: size * 0.3,\n            height: hair,\n            backgroundColor: t.rule,\n          }}\n        >\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              width: \"100%\",\n              height: \"100%\",\n              backgroundColor: t.accent,\n              transform: `scaleX(${decoded})`,\n              transformOrigin: \"left\",\n            }}\n          />\n        </div>\n\n        {footnote || showProgress ? (\n          <div\n            style={{\n              alignSelf: \"stretch\",\n              display: \"flex\",\n              justifyContent: \"space-between\",\n              alignItems: \"baseline\",\n              marginTop: size * 0.2,\n              fontFamily: t.fontMono,\n              fontSize: metaSize,\n              fontWeight: 500,\n              letterSpacing: metaSize * 0.16,\n              textTransform: \"uppercase\",\n            }}\n          >\n            <span style={{ color: t.inkMuted }}>{footnote ?? \"\"}</span>\n            {showProgress ? (\n              <span style={{ color: pctColor, fontVariantNumeric: \"tabular-nums\" }}>{`${pct}%`}</span>\n            ) : null}\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One character cell.\n *\n * The cell is a fixed-width box, not a run of text, so the glyph inside can\n * change every other frame — and even jitter vertically — without moving a\n * single neighbour. Everything here is a pure function of `frame`: the glyph is\n * picked with `random(seed + tick)`, so the same frame always renders the same\n * character on every machine and every retry of a distributed render.\n */\nconst Glyph: React.FC<{\n  target: string;\n  frame: number;\n  lockAt: number;\n  pool: string;\n  size: number;\n  cell: number;\n  seed: string;\n  tokens: BrandTokens;\n}> = ({ target, frame, lockAt, pool, size, cell, seed, tokens }) => {\n  const isSpace = target === \" \" || target === \"\\u00A0\";\n  const locked = frame >= lockAt;\n\n  // The lock is a 5-frame settle rather than a hard cut, so contrast arrives\n  // with the letter instead of snapping a frame later.\n  const settle = interpolate(frame, [lockAt, lockAt + 5], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const tick = Math.floor(frame / CYCLE);\n  const draw = pool.charAt(Math.min(pool.length - 1, Math.floor(random(`${seed}-${tick}`) * pool.length)));\n  // Scramble in the target's own case, so a lowercase phrase never flickers\n  // into caps and never changes its apparent weight mid-scramble.\n  const isLower = target === target.toLowerCase() && target !== target.toUpperCase();\n  const glyph = locked ? target : isLower ? draw.toLowerCase() : draw;\n\n  // Unresolved glyphs sit fractionally off the baseline — the line is stable,\n  // the characters in it are not yet.\n  const jitter = locked ? 0 : (random(`${seed}-y-${tick}`) - 0.5) * size * 0.06;\n\n  return (\n    <span\n      style={{\n        flex: \"0 0 auto\",\n        width: cell,\n        height: \"100%\",\n        display: \"flex\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        fontFamily: tokens.fontMono,\n        fontSize: size,\n        fontWeight: 600,\n        lineHeight: 1,\n        color: interpolateColors(settle, [0, 1], [tokens.inkMuted, tokens.ink]),\n        opacity: interpolate(settle, [0, 1], [0.55, 1]),\n        transform: `translateY(${jitter}px)`,\n      }}\n    >\n      {isSpace ? null : glyph}\n    </span>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"scramble-decode\",\n  name: \"Scramble Decode\",\n  category: \"typography\",\n  summary:\n    \"A phrase resolves out of cycling glyphs left to right, each character locking to full contrast on a staggered beat\",\n  tags: [\"scramble\", \"decode\", \"monospace\", \"kinetic type\", \"glitch\", \"terminal\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: scrambleDecodeSchema,\n  defaults: {\n    text: \"SIGNAL FROM NOISE\",\n    eyebrow: \"Resolving\",\n    footnote: \"Motionstash\",\n    charset: \"alphanumeric\",\n    lockStagger: 3,\n    grid: true,\n    showProgress: true,\n    align: \"left\",\n    brand: \"slate\",\n  },\n  component: ScrambleDecode,\n  file: \"src/registry/items/scramble-decode.tsx\",\n  origin: {\n    prompt:\n      \"A monospace phrase that decodes out of scrambled glyphs left to right — each character cycling through random letters then locking into its final one, locked characters at full contrast and the rest muted, with a hairline rule underneath that draws in as the line resolves.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A monospace phrase that decodes out of scrambled glyphs left to right — each character cycling through random letters then locking into its final one, locked characters at full contrast and the rest muted, with a hairline rule underneath that draws in as the line resolves.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3233,
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The phrase that resolves. Uppercase reads best in mono; spaces never scramble, so word shape is visible from the first frame. Up to ~24 characters keeps the type large.",
          "required": true,
          "defaultValue": "SIGNAL FROM NOISE",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase label above the line, in the accent colour. Omit for a bare decode.",
          "required": false,
          "defaultValue": "Resolving",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "Small uppercase caption under the rule, bottom-left. Good for a source, a brand or a channel name.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "charset",
          "type": "\"alphanumeric\" | \"symbols\" | \"hex\" | \"binary\"",
          "description": "Which glyphs a character cycles through before it locks. 'alphanumeric' is the classic decode; 'hex' and 'binary' read as machine output; 'symbols' is the noisiest.",
          "required": false,
          "defaultValue": "alphanumeric",
          "options": [
            "alphanumeric",
            "symbols",
            "hex",
            "binary"
          ]
        },
        {
          "name": "lockStagger",
          "type": "number",
          "description": "Frames between one character locking and the next, left to right. 2–4 reads as one sweeping gesture; above that the line resolves letter by letter. Compressed automatically for long strings so the decode always finishes inside the hold.",
          "required": false,
          "defaultValue": "3",
          "options": null
        },
        {
          "name": "grid",
          "type": "boolean",
          "description": "Show the monospace cell grid as hairlines behind the type. Each line fades as its column locks.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "showProgress",
          "type": "boolean",
          "description": "Show a percentage readout, right-aligned to the end of the line, that turns accent at 100%.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "description": "Position of the block in the frame. 'left' is the editorial default; 'center' suits 1:1 and 9:16.",
          "required": false,
          "defaultValue": "left",
          "options": [
            "left",
            "center"
          ]
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "split-flap-board",
      "name": "Split Flap Board",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "An airport flap display: seamed tiles churn through their drum, cell by cell, and land on the string",
      "tags": [
        "split flap",
        "flip board",
        "airport",
        "mono",
        "stagger",
        "mechanical"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/split-flap-board.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const splitFlapBoardSchema = z.object({\n  text: z\n    .string()\n    .describe(\n      \"The string the board lands on. Uppercased automatically. One tile per character, spaces included — 6 to 14 characters reads best; beyond that the tiles get small.\",\n    ),\n  label: z\n    .string()\n    .optional()\n    .describe(\"Small mono label above the board, e.g. 'DEPARTURES' or 'TODAY'S FARE'. Omit for a bare board.\"),\n  meta: z\n    .string()\n    .optional()\n    .describe(\"Short status set hard right on the label line, e.g. 'ON TIME' or 'GATE OPEN'. Omit to leave the line to the label alone.\"),\n  subline: z\n    .string()\n    .optional()\n    .describe(\"One line of detail under the board rule, set right. Use it for the human-readable version of what the tiles abbreviate.\"),\n  highlight: z\n    .string()\n    .optional()\n    .describe(\n      \"A substring of `text` whose tiles carry the accent colour — the destination code, the gate, the amount. Case-insensitive, first match wins.\",\n    ),\n  charset: z\n    .enum([\"auto\", \"letters\", \"digits\", \"mixed\"])\n    .default(\"auto\")\n    .describe(\n      \"Which drum the flaps travel through on the way to the target. 'auto' gives digits a digit drum and letters a letter drum (punctuation sits still), which is the most legible. 'mixed' churns letters and numbers together for a noisier, more mechanical run.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof splitFlapBoardSchema>;\ntype Charset = Props[\"charset\"];\n\n/**\n * The drums. A cell can only land on a glyph that exists on the drum it is\n * turning, which is what keeps the run reading as one mechanism rather than as\n * random characters: every intermediate glyph is a real neighbour of the last.\n */\nconst DRUMS = {\n  letters: \" ABCDEFGHIJKLMNOPQRSTUVWXYZ\",\n  digits: \"0123456789\",\n  mixed: \" ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\",\n} as const;\n\n/** Frames for one flap to fall. Two is the whole trick — a flap is a snap, not a tween. */\nconst FLAP_FRAMES = 2;\n/** Frames a cell sits still after it arrives, before its drum starts turning. */\nconst FLAP_LEAD = 2;\nconst FLAPS_MIN = 3;\nconst FLAPS_RANGE = 7;\n/**\n * Every cell has landed by this frame of the 90-frame budget, which leaves the\n * back half of `hold` genuinely at rest — the board has to read as a still.\n */\nconst RESOLVE_BY = 46;\n/** Worst-case run length, used to size the stagger so the slowest cell still lands in time. */\nconst LONGEST_RUN = FLAP_LEAD + (FLAPS_MIN + FLAPS_RANGE - 1) * FLAP_FRAMES;\n\n/** The drum this character turns on, or `null` if it is fixed furniture (a colon, a dash). */\nconst drumFor = (char: string, charset: Charset): string | null => {\n  const drum =\n    charset === \"letters\"\n      ? DRUMS.letters\n      : charset === \"digits\"\n        ? DRUMS.digits\n        : charset === \"mixed\"\n          ? DRUMS.mixed\n          : /[0-9]/.test(char)\n            ? DRUMS.digits\n            : /[A-Z ]/.test(char)\n              ? DRUMS.letters\n              : null;\n  if (drum === null || !drum.includes(char)) return null;\n  return drum;\n};\n\nconst SplitFlapBoard: React.FC<Props> = ({ text, label, meta, subline, highlight, charset, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n\n  const chars = (text.length > 0 ? text : \" \").toUpperCase().split(\"\");\n  const count = chars.length;\n\n  // Geometry. The board fills the frame width, capped by height so a short\n  // string becomes a big board rather than a poster. Everything else in the\n  // scene is derived from the tile, so the whole composition rescales together.\n  const boardW = width * 0.86;\n  const gapRatio = 0.09;\n  const cellW = Math.min(boardW / (count + (count - 1) * gapRatio), height * 0.215);\n  const cellH = cellW * 1.42;\n  const gap = cellW * gapRatio;\n  // The glyph nearly fills the tile, the way a real flap does — capped against\n  // tile width so a narrow tile never clips a mono advance.\n  const glyphSize = Math.min(cellH * 0.8, cellW * 1.3);\n  const labelSize = width * 0.0135;\n  const radius = Math.min(t.radius, cellW * 0.09);\n\n  // Stagger: cell 0 starts immediately, the last cell starts late enough to\n  // cascade and early enough to land by RESOLVE_BY even on a full-length run.\n  const perCell = count > 1 ? Math.min(3, Math.max(1, (RESOLVE_BY - LONGEST_RUN) / (count - 1))) : 0;\n\n  // Accent range. Matched against the uppercased string so the prop can be\n  // typed casually, and silently inert when it does not match.\n  const needle = (highlight ?? \"\").trim().toUpperCase();\n  const hlStart = needle.length > 0 ? chars.join(\"\").indexOf(needle) : -1;\n  const accented = (i: number) => hlStart >= 0 && i >= hlStart && i < hlStart + needle.length;\n\n  // The rule under the board draws left to right while the first tiles are\n  // still turning, so the board has a baseline before it has an answer.\n  const ruleScale = interpolate(stage.frame, [FLAP_LEAD + 2, FLAP_LEAD + 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"flex-start\",\n        padding: `0 ${(width - boardW) / 2}px`,\n      }}\n    >\n      <div style={{ width: boardW, marginTop: -height * 0.02 }}>\n        {label || meta ? (\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"space-between\",\n              gap: labelSize,\n              marginBottom: cellH * 0.24,\n              opacity: stage.presence,\n              // Everything in the frame rises the same distance in the same\n              // direction as the tiles: one arrival, not three.\n              transform: `translateY(${interpolate(stage.enter, [0, 1], [labelSize * 0.7, 0])}px)`,\n            }}\n          >\n            {label ? (\n              <div style={{ display: \"flex\", alignItems: \"center\", gap: labelSize * 0.7 }}>\n                {/* The board's one indicator lamp. The only accent mark outside the tiles. */}\n                <div\n                  style={{\n                    width: labelSize * 0.5,\n                    height: labelSize * 0.5,\n                    borderRadius: labelSize * 0.12,\n                    backgroundColor: t.accent,\n                  }}\n                />\n                <span\n                  style={{\n                    fontFamily: t.fontMono,\n                    fontSize: labelSize,\n                    fontWeight: 600,\n                    letterSpacing: labelSize * 0.22,\n                    textTransform: \"uppercase\",\n                    color: t.ink,\n                  }}\n                >\n                  {label}\n                </span>\n              </div>\n            ) : (\n              <span />\n            )}\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: labelSize,\n                fontWeight: 500,\n                letterSpacing: labelSize * 0.18,\n                textTransform: \"uppercase\",\n                color: t.inkMuted,\n              }}\n            >\n              {meta}\n            </span>\n          </div>\n        ) : null}\n\n        <div style={{ display: \"flex\", gap }}>\n          {chars.map((char, i) => (\n            <FlapCell\n              key={`${char}-${i}`}\n              char={char}\n              index={i}\n              perCell={perCell}\n              charset={charset}\n              w={cellW}\n              h={cellH}\n              glyphSize={glyphSize}\n              radius={radius}\n              font={t.fontMono}\n              face={t.surface}\n              ink={accented(i) ? t.accent : t.ink}\n              rule={t.rule}\n            />\n          ))}\n        </div>\n\n        <div\n          style={{\n            marginTop: cellH * 0.2,\n            height: Math.max(1, cellH * 0.008),\n            width: \"100%\",\n            backgroundColor: t.rule,\n            transform: `scaleX(${ruleScale})`,\n            transformOrigin: \"left\",\n            opacity: stage.presence,\n          }}\n        />\n\n        {subline ? (\n          <div\n            style={{\n              marginTop: labelSize * 0.9,\n              textAlign: \"right\",\n              fontFamily: t.fontBody,\n              fontSize: labelSize * 0.94,\n              fontWeight: 400,\n              letterSpacing: labelSize * 0.04,\n              color: t.inkMuted,\n              opacity: stage.presence,\n            }}\n          >\n            {subline}\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One character cell.\n *\n * A real split-flap cell is a stack of hinged flaps: the front of a flap carries\n * the top half of the outgoing glyph, its back carries the bottom half of the\n * incoming one, and it swings about the seam. So each flip is two moves — the\n * top half collapsing onto the seam, then the bottom half opening off it — and\n * the seam is where the whole illusion lives. Both moves are a plain scaleY,\n * which stays crisp at 4K where a rotateY with perspective would not.\n */\nconst FlapCell: React.FC<{\n  char: string;\n  index: number;\n  perCell: number;\n  charset: Charset;\n  w: number;\n  h: number;\n  glyphSize: number;\n  radius: number;\n  font: string;\n  face: string;\n  ink: string;\n  rule: string;\n}> = ({ char, index, perCell, charset, w, h, glyphSize, radius, font, face, ink, rule }) => {\n  const stage = useStaggeredStage(BUDGETS.statement, index, perCell);\n  const drum = drumFor(char, charset);\n\n  // Seeded run length: cells that all stopped together would read as one\n  // element flickering. The unevenness is most of the charm, and seeding it\n  // keeps every render of this frame identical.\n  const flaps = drum === null ? 0 : FLAPS_MIN + Math.floor(random(`split-flap-${index}-${char}`) * FLAPS_RANGE);\n\n  const clock = (stage.frame - FLAP_LEAD) / FLAP_FRAMES;\n  const travelled = Math.max(0, Math.min(flaps, clock));\n  const landed = drum === null || travelled >= flaps;\n  const step = Math.min(Math.max(flaps - 1, 0), Math.floor(travelled));\n\n  // Walk backwards from the target so step `flaps` is always the answer.\n  const glyphAt = (k: number) => {\n    if (drum === null) return char;\n    const len = drum.length;\n    const home = drum.indexOf(char);\n    return drum.charAt((((home - flaps + k) % len) + len) % len);\n  };\n\n  const from = landed ? char : glyphAt(step);\n  const to = landed ? char : glyphAt(step + 1);\n\n  // Ease-in on the way down to the seam, ease-out on the way off it: one\n  // travel curve split at its midpoint gives both halves of the swing.\n  const eased = interpolate(landed ? 1 : travelled - step, [0, 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const falling = eased < 0.5;\n  const swing = falling ? eased * 2 : (eased - 0.5) * 2;\n\n  const seam = Math.max(2, h * 0.011);\n  const lift = interpolate(stage.enter, [0, 1], [h * 0.16, 0]);\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width: w,\n        height: h,\n        borderRadius: radius,\n        backgroundColor: face,\n        overflow: \"hidden\",\n        opacity: stage.presence,\n        transform: `translateY(${lift}px)`,\n        boxShadow: `0 ${h * 0.024}px ${h * 0.07}px rgba(0,0,0,0.26)`,\n      }}\n    >\n      {/* The cell at rest: incoming glyph above the seam, outgoing below it. */}\n      <Half half=\"top\" char={to} w={w} h={h} glyphSize={glyphSize} font={font} ink={ink} />\n      <Half half=\"bottom\" char={from} w={w} h={h} glyphSize={glyphSize} font={font} ink={ink} />\n\n      {/* The flap in flight. */}\n      {landed ? null : (\n        <Half\n          half={falling ? \"top\" : \"bottom\"}\n          char={falling ? from : to}\n          w={w}\n          h={h}\n          glyphSize={glyphSize}\n          font={font}\n          ink={ink}\n          face={face}\n          scaleY={falling ? 1 - swing : swing}\n          shade={falling ? 0.34 * swing : 0.34 * (1 - swing)}\n        />\n      )}\n\n      {/* The seam: a hairline of gap plus the shadow it throws downward. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: h / 2 - seam / 2,\n          width: \"100%\",\n          height: seam,\n          backgroundColor: rule,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: h / 2 + seam / 2,\n          width: \"100%\",\n          height: Math.max(1, seam * 0.6),\n          backgroundColor: \"rgba(0,0,0,0.32)\",\n        }}\n      />\n\n      {/* Housing. Kept as an overlay so the recess reads over the flap, not under it. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          right: 0,\n          bottom: 0,\n          borderRadius: radius,\n          boxShadow: `inset 0 ${h * 0.02}px ${h * 0.08}px rgba(0,0,0,0.38), inset 0 0 0 1px ${rule}`,\n        }}\n      />\n    </div>\n  );\n};\n\n/**\n * Half a glyph, clipped. The glyph is laid out at full cell height inside a\n * half-height window, so its optical centre always sits exactly on the seam and\n * the two halves line up perfectly when the cell is at rest.\n */\nconst Half: React.FC<{\n  half: \"top\" | \"bottom\";\n  char: string;\n  w: number;\n  h: number;\n  glyphSize: number;\n  font: string;\n  ink: string;\n  /** Only the flap in flight paints a face — the static halves let the cell show through. */\n  face?: string;\n  scaleY?: number;\n  shade?: number;\n}> = ({ half, char, w, h, glyphSize, font, ink, face, scaleY, shade }) => {\n  const isTop = half === \"top\";\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: isTop ? 0 : h / 2,\n        width: w,\n        height: h / 2,\n        overflow: \"hidden\",\n        backgroundColor: face,\n        transform: scaleY === undefined ? undefined : `scaleY(${scaleY})`,\n        transformOrigin: isTop ? \"bottom\" : \"top\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: isTop ? 0 : -h / 2,\n          width: w,\n          height: h,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: font,\n          fontSize: glyphSize,\n          fontWeight: 500,\n          lineHeight: 1,\n          fontVariantNumeric: \"tabular-nums\",\n          color: ink,\n        }}\n      >\n        {char}\n      </div>\n      {shade === undefined ? null : (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            right: 0,\n            bottom: 0,\n            backgroundColor: `rgba(0,0,0,${shade})`,\n          }}\n        />\n      )}\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"split-flap-board\",\n  name: \"Split Flap Board\",\n  category: \"typography\",\n  summary: \"An airport flap display: seamed tiles churn through their drum, cell by cell, and land on the string\",\n  tags: [\"split flap\", \"flip board\", \"airport\", \"mono\", \"stagger\", \"mechanical\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: splitFlapBoardSchema,\n  defaults: {\n    text: \"17:42  KEF\",\n    label: \"Departures\",\n    meta: \"On time\",\n    subline: \"Reykjavík · Gate B14 · LH 4021\",\n    highlight: \"KEF\",\n    charset: \"auto\",\n    brand: \"slate\",\n  },\n  component: SplitFlapBoard,\n  file: \"src/registry/items/split-flap-board.tsx\",\n  origin: {\n    prompt:\n      \"An airport split-flap board. A row of dark tiles with a seam across the middle, each character flipping through intermediate glyphs to reach its target, staggered cell by cell, mono type, and it should settle into a still I could hold on.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "An airport split-flap board. A row of dark tiles with a seam across the middle, each character flipping through intermediate glyphs to reach its target, staggered cell by cell, mono type, and it should settle into a still I could hold on.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2925,
      "props": [
        {
          "name": "text",
          "type": "string",
          "description": "The string the board lands on. Uppercased automatically. One tile per character, spaces included — 6 to 14 characters reads best; beyond that the tiles get small.",
          "required": true,
          "defaultValue": "17:42  KEF",
          "options": null
        },
        {
          "name": "label",
          "type": "string",
          "description": "Small mono label above the board, e.g. 'DEPARTURES' or 'TODAY'S FARE'. Omit for a bare board.",
          "required": false,
          "defaultValue": "Departures",
          "options": null
        },
        {
          "name": "meta",
          "type": "string",
          "description": "Short status set hard right on the label line, e.g. 'ON TIME' or 'GATE OPEN'. Omit to leave the line to the label alone.",
          "required": false,
          "defaultValue": "On time",
          "options": null
        },
        {
          "name": "subline",
          "type": "string",
          "description": "One line of detail under the board rule, set right. Use it for the human-readable version of what the tiles abbreviate.",
          "required": false,
          "defaultValue": "Reykjavík · Gate B14 · LH 4021",
          "options": null
        },
        {
          "name": "highlight",
          "type": "string",
          "description": "A substring of `text` whose tiles carry the accent colour — the destination code, the gate, the amount. Case-insensitive, first match wins.",
          "required": false,
          "defaultValue": "KEF",
          "options": null
        },
        {
          "name": "charset",
          "type": "\"auto\" | \"letters\" | \"digits\" | \"mixed\"",
          "description": "Which drum the flaps travel through on the way to the target. 'auto' gives digits a digit drum and letters a letter drum (punctuation sits still), which is the most legible. 'mixed' churns letters and numbers together for a noisier, more mechanical run.",
          "required": false,
          "defaultValue": "auto",
          "options": [
            "auto",
            "letters",
            "digits",
            "mixed"
          ]
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "spotlight-callout",
      "name": "Spotlight Callout",
      "category": "titles",
      "categoryLabel": "Titles & lower thirds",
      "summary": "A scrim closes over a synthesised product panel and a soft-edged hole opens on one control, labelled by a leader line",
      "tags": [
        "spotlight",
        "callout",
        "product tour",
        "scrim",
        "svg mask",
        "leader line",
        "annotation",
        "onboarding"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/spotlight-callout.tsx",
      "source": "import React from \"react\";\nimport { evolvePath, translatePath } from \"@remotion/paths\";\nimport { makeRect } from \"@remotion/shapes\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nexport const spotlightCalloutSchema = z.object({\n  label: z\n    .string()\n    .describe(\n      \"The callout headline — what the spotlit thing is or does. Three to six words; it sets on one or two lines above the leader line.\",\n    ),\n  note: z\n    .string()\n    .optional()\n    .describe(\n      \"One supporting sentence under the label. A callout is a pointer, not a paragraph — keep it to a single clause.\",\n    ),\n  step: z\n    .string()\n    .optional()\n    .describe(\n      \"Tiny mono kicker above the label, e.g. 'Step 02' or 'Start here'. Omit for an unnumbered annotation.\",\n    ),\n  target: z\n    .enum([\"action\", \"search\", \"nav\", \"row\", \"custom\"])\n    .default(\"action\")\n    .describe(\n      \"Which element of the synthesised panel the hole opens on. 'action' is the primary button, 'search' the field beside it, 'nav' the current sidebar item, 'row' a list row, 'custom' the `focus` rect.\",\n    ),\n  targetRow: z\n    .number()\n    .int()\n    .min(1)\n    .max(7)\n    .default(2)\n    .describe(\"Which list row to spotlight, 1-based, when `target` is 'row'. Clamped to the number of rows drawn.\"),\n  focus: z\n    .object({\n      x: z.number().min(0).max(1).describe(\"Hole centre X, as a fraction of frame width.\"),\n      y: z.number().min(0).max(1).describe(\"Hole centre Y, as a fraction of frame height.\"),\n      w: z.number().min(0.02).max(1).describe(\"Hole width, as a fraction of frame width.\"),\n      h: z.number().min(0.02).max(1).describe(\"Hole height, as a fraction of frame height.\"),\n    })\n    .default({ x: 0.68, y: 0.3, w: 0.19, h: 0.11 })\n    .describe(\n      \"The spotlight rect, in frame fractions so it survives a change of aspect. Only read when `target` is 'custom'.\",\n    ),\n  panelTitle: z\n    .string()\n    .default(\"Workspace\")\n    .describe(\n      \"Section heading in the panel behind the scrim. One or two words — it is set dressing, and its initial doubles as the app's logo mark.\",\n    ),\n  actionLabel: z\n    .string()\n    .default(\"Render scene\")\n    .describe(\n      \"Label on the panel's primary button. With the default target this is the one piece of UI copy the spotlight actually lets you read, so make it the verb you are teaching.\",\n    ),\n  rows: z\n    .number()\n    .int()\n    .min(3)\n    .max(7)\n    .default(5)\n    .describe(\"How many list rows the panel draws. They divide the panel body, so more rows means tighter rows.\"),\n  dim: z\n    .number()\n    .min(0.2)\n    .max(0.95)\n    .default(0.58)\n    .describe(\n      \"How far the scrim darkens everything outside the hole. On a dark brand this bites hard — much past 0.7 the surrounding UI stops reading as context at all; a light brand will take 0.8.\",\n    ),\n  softness: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.5)\n    .describe(\n      \"Feather on the hole edge. 0 is a near-hard cut that looks like a mask; 1 is a wide falloff that looks like a lamp. Mid values read as light.\",\n    ),\n  ring: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw a hairline accent rectangle just outside the hole. Turn it off when the light alone is enough.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof spotlightCalloutSchema>;\n\n/* ---------------------------------------------------------------- geometry */\n\ntype Rect = { x: number; y: number; w: number; h: number };\n\ntype Layout = {\n  panel: Rect;\n  chromeH: number;\n  bodyY: number;\n  bodyH: number;\n  sidebar: Rect;\n  main: Rect;\n  headerH: number;\n  action: Rect;\n  search: Rect;\n  columns: Rect;\n  rows: Rect[];\n  rowGap: number;\n  chip: Rect;\n  nav: Rect[];\n  navFoot: Rect;\n  hair: number;\n  radius: { panel: number; control: number; row: number };\n};\n\nconst NAV_ITEMS = 5;\n\n/** Which sidebar item reads as the current page. Also the 'nav' spotlight target. */\nconst CURRENT_NAV = 1;\n\nconst box = (r: Rect): React.CSSProperties => ({\n  position: \"absolute\",\n  left: r.x,\n  top: r.y,\n  width: r.w,\n  height: r.h,\n});\n\nconst outset = (r: Rect, by: number): Rect => ({\n  x: r.x - by,\n  y: r.y - by,\n  w: r.w + by * 2,\n  h: r.h + by * 2,\n});\n\nconst midX = (r: Rect) => r.x + r.w / 2;\nconst midY = (r: Rect) => r.y + r.h / 2;\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\n/** Rough relative luminance of a `#rrggbb` token. Unparseable input reads as light. */\nconst luminance = (hex: string) => {\n  const raw = hex.replace(\"#\", \"\");\n  const six = raw.length === 3 ? raw.split(\"\").map((c) => c + c).join(\"\") : raw.slice(0, 6);\n  const v = Number.parseInt(six, 16);\n  if (!Number.isFinite(v)) return 1;\n  return (0.2126 * ((v >> 16) & 255) + 0.7152 * ((v >> 8) & 255) + 0.0722 * (v & 255)) / 255;\n};\n\n/**\n * The type colour for anything sitting *on* the scrim.\n *\n * The annotation is not on the brand's surface, it is on a black veil, so it\n * has to be picked for the veil. On a dark brand `ink` is already the light\n * token; on a light brand `ink` is near-black and would vanish, while `surface`\n * is the light one. Choosing between two tokens by luminance keeps this\n * retargetable — no literal white anywhere.\n */\nconst inkOnScrim = (t: BrandTokens) => (luminance(t.ink) > 0.5 ? t.ink : t.surface);\n\n/** An eased 0→1 window on the scene's own frame. Internal beats only — presence is the stage's job. */\nconst beat = (frame: number, from: number, to: number, easing = EASING.enter) =>\n  interpolate(frame, [from, to], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing,\n  });\n\n/**\n * The panel is laid out once, in absolute frame coordinates, and every element\n * is then drawn at the rect the layout gives it. Doing it this way rather than\n * with nested flex is what lets the spotlight address a *named* element —\n * `target: \"action\"` — instead of asking the caller to guess at fractions that\n * would break the moment the aspect ratio changed.\n */\nconst buildLayout = (width: number, height: number, rowCount: number, tokenRadius: number): Layout => {\n  const min = Math.min(width, height);\n  const hair = Math.max(1, min * 0.0012);\n  // Token radii are authored against a 1080-tall surface; scale, never assume.\n  const rs = min / 1080;\n\n  const panel: Rect = { x: width * 0.055, y: height * 0.075, w: width * 0.89, h: height * 0.85 };\n  const chromeH = panel.h * 0.085;\n  const bodyY = panel.y + chromeH;\n  const bodyH = panel.h - chromeH;\n\n  const sidebar: Rect = { x: panel.x, y: bodyY, w: panel.w * 0.185, h: bodyH };\n  const padX = panel.w * 0.04;\n  const padY = bodyH * 0.075;\n  const main: Rect = {\n    x: sidebar.x + sidebar.w + padX,\n    y: bodyY + padY,\n    w: panel.w - sidebar.w - padX * 2,\n    h: bodyH - padY * 2,\n  };\n\n  const headerH = main.h * 0.145;\n  const controlH = headerH * 0.62;\n  const actionW = main.w * 0.175;\n  const action: Rect = {\n    x: main.x + main.w - actionW,\n    y: main.y + (headerH - controlH) / 2,\n    w: actionW,\n    h: controlH,\n  };\n  const searchW = main.w * 0.215;\n  const search: Rect = {\n    x: action.x - main.w * 0.045 - searchW,\n    y: action.y,\n    w: searchW,\n    h: controlH,\n  };\n\n  const columns: Rect = { x: main.x, y: main.y + headerH + main.h * 0.05, w: main.w, h: main.h * 0.05 };\n\n  // Rows divide whatever is left of the main area exactly, so 3 rows and 7 rows\n  // both bottom out on the same line.\n  const rowsTop = columns.y + columns.h + main.h * 0.025;\n  const available = main.y + main.h - rowsTop;\n  const rowGap = available * 0.022;\n  const rowH = (available - rowGap * (rowCount - 1)) / rowCount;\n  const rows: Rect[] = Array.from({ length: rowCount }, (_, i) => ({\n    x: main.x,\n    y: rowsTop + i * (rowH + rowGap),\n    w: main.w,\n    h: rowH,\n  }));\n\n  const navPadX = sidebar.w * 0.155;\n  const chip: Rect = {\n    x: sidebar.x + navPadX,\n    y: bodyY + bodyH * 0.055,\n    w: sidebar.w - navPadX * 2,\n    h: bodyH * 0.085,\n  };\n  const navH = bodyH * 0.062;\n  const navGap = bodyH * 0.02;\n  const navTop = chip.y + chip.h + bodyH * 0.05;\n  const nav: Rect[] = Array.from({ length: NAV_ITEMS }, (_, i) => ({\n    x: chip.x,\n    y: navTop + i * (navH + navGap),\n    w: chip.w,\n    h: navH,\n  }));\n  // One item pinned to the foot of the sidebar. Real products have one; the\n  // gap above it is what makes the sidebar read as a sidebar.\n  const navFoot: Rect = { x: chip.x, y: bodyY + bodyH - bodyH * 0.055 - navH, w: chip.w, h: navH };\n\n  return {\n    panel,\n    chromeH,\n    bodyY,\n    bodyH,\n    sidebar,\n    main,\n    headerH,\n    action,\n    search,\n    columns,\n    rows,\n    rowGap,\n    chip,\n    nav,\n    navFoot,\n    hair,\n    radius: { panel: tokenRadius * 1.9 * rs, control: tokenRadius * 0.95 * rs, row: tokenRadius * 0.85 * rs },\n  };\n};\n\ntype Point = { x: number; y: number };\n\ntype LeaderPlan = {\n  path: string;\n  anchor: Point;\n  /**\n   * Where the label block sets: its left edge, its measure, the y its baseline\n   * rule sits on, and how much clear frame it has above that rule to grow into.\n   */\n  label: { left: number; width: number; rule: number; room: number };\n};\n\nconst poly = (points: Point[]) =>\n  points.map((p, i) => `${i === 0 ? \"M\" : \"L\"} ${p.x.toFixed(2)} ${p.y.toFixed(2)}`).join(\" \");\n\n/**\n * Where the leader goes.\n *\n * The shape is always the same three moves — straight out of the target, a 45°\n * elbow, then a long horizontal run the label sets on — but the *direction* is\n * chosen from the room left in the frame rather than hard-coded, because a\n * callout that walks off the edge is worse than no callout. Two orientations:\n *\n *  - Sideways, when the flank of the target has room for the whole elbow.\n *    Preferred: it reads as pointing *at* something.\n *  - Down (or up) out of the target's long edge, when it does not — a spotlit\n *    table row is nearly frame-wide and has no flank to leave from. The run\n *    then travels along the foot of the panel.\n *\n * In both, the run lands in a chosen *band* of the panel rather than a fixed\n * distance from the target: low for a high target, high for a low one. The\n * diagonal absorbs the difference, staying at 45° by travelling the same\n * distance in both axes. That is what keeps the label off the panel's own\n * header — a label crossing the section title is the one thing that would give\n * the whole device away as automatic.\n */\nconst planLeader = (width: number, height: number, ring: Rect, panel: Rect): LeaderPlan => {\n  const min = Math.min(width, height);\n  const marginX = width * 0.055;\n  const gap = min * 0.012;\n  const stub = min * 0.035;\n  const minRun = width * 0.15;\n\n  const anchorY = ring.y + ring.h / 2;\n  const dirY = anchorY > height * 0.52 ? -1 : 1;\n  const ruleY = clamp(\n    dirY > 0\n      ? Math.max(anchorY + min * 0.2, panel.y + panel.h * 0.64)\n      : Math.min(anchorY - min * 0.2, panel.y + panel.h * 0.36),\n    panel.y + panel.h * 0.06,\n    panel.y + panel.h * 0.955,\n  );\n  const travel = Math.abs(ruleY - anchorY);\n\n  const roomLeft = ring.x - marginX;\n  const roomRight = width - marginX - (ring.x + ring.w);\n\n  if (Math.max(roomLeft, roomRight) >= gap + stub + travel + minRun) {\n    const dirX = roomRight > roomLeft ? 1 : -1;\n    const a: Point = { x: dirX < 0 ? ring.x - gap : ring.x + ring.w + gap, y: anchorY };\n    const b: Point = { x: a.x + dirX * stub, y: a.y };\n    const c: Point = { x: b.x + dirX * travel, y: ruleY };\n    const run = clamp(dirX < 0 ? c.x - marginX : width - marginX - c.x, minRun, width * 0.29);\n    const d: Point = { x: c.x + dirX * run, y: c.y };\n    // Sideways, the label sits across the frame from the target, so the only\n    // thing above it is the frame edge.\n    return {\n      path: poly([a, b, c, d]),\n      anchor: a,\n      label: { left: Math.min(c.x, d.x), width: run, rule: d.y, room: d.y - height * 0.05 },\n    };\n  }\n\n  // Same idea, rotated: leave the long edge, and land the run in the panel's\n  // opposite band. The band — not a fixed distance — is what guarantees the\n  // label still has frame above it to grow into, whichever way it went.\n  const drop = panel.y + panel.h - (ring.y + ring.h) >= ring.y - panel.y ? 1 : -1;\n  // Leave the long edge off-centre — a leader dropping from the exact middle of\n  // a row reads as a dimension line, not an annotation.\n  const a: Point = { x: ring.x + ring.w * 0.18, y: drop > 0 ? ring.y + ring.h + gap : ring.y - gap };\n  const dirX = width - marginX - a.x >= a.x - marginX ? 1 : -1;\n  // Floors for the two moves, so the leader always reads as leaving the target\n  // and always turns a real corner instead of kinking.\n  const minStub = min * 0.03;\n  const minElbow = min * 0.06;\n  const band = drop > 0 ? panel.y + panel.h * 0.94 : panel.y + panel.h * 0.34;\n  // A target big enough to swallow its own band — an oversized custom rect —\n  // would put the run back on the side we just left. Pull it in front instead.\n  const ruleAt = clamp(\n    drop > 0 ? Math.max(band, a.y + minStub + minElbow) : Math.min(band, a.y - minStub - minElbow),\n    panel.y + panel.h * 0.05,\n    panel.y + panel.h * 0.95,\n  );\n  const b: Point = {\n    x: a.x,\n    y:\n      drop > 0\n        ? Math.max(a.y + minStub, ruleAt - min * 0.185)\n        : Math.min(a.y - minStub, ruleAt + min * 0.185),\n  };\n  const c: Point = { x: b.x + dirX * Math.abs(ruleAt - b.y), y: ruleAt };\n  // Dropping out of the long edge puts the target itself above the label, so\n  // that edge — not the frame — is the ceiling the type has to respect.\n  const run = clamp(dirX < 0 ? c.x - marginX : width - marginX - c.x, minRun, width * 0.34);\n  const d: Point = { x: c.x + dirX * run, y: c.y };\n  const ceiling = drop > 0 ? ring.y + ring.h : height * 0.05;\n  return {\n    path: poly([a, b, c, d]),\n    anchor: a,\n    label: { left: Math.min(c.x, d.x), width: run, rule: d.y, room: d.y - ceiling },\n  };\n};\n\n/**\n * SVG ids have to be unique per *document*, and the gallery puts several items\n * on one page — a fixed id would let one card's mask capture another's paint.\n * Hashing the geometry keeps the id a pure function of the props (no random, no\n * state, stable across every frame of a render), and two instances that collide\n * are by definition asking for the same mask.\n */\nconst scopedId = (parts: (string | number)[]): string => {\n  let h = 2166136261;\n  const key = parts.join(\"|\");\n  for (let i = 0; i < key.length; i += 1) {\n    h ^= key.charCodeAt(i);\n    h = Math.imul(h, 16777619);\n  }\n  return `spot-${(h >>> 0).toString(36)}`;\n};\n\n/* --------------------------------------------------------------- component */\n\n/**\n * Spotlight Callout — dim the frame, open a soft hole on one control, name it.\n *\n * Three layers, in order: a synthesised product panel drawn entirely with divs;\n * a full-frame scrim carrying an SVG mask whose hole is a rounded rect run\n * through a Gaussian blur, so the edge is light rather than a cut; and the\n * annotation — a hairline ring, an elbowed leader that draws out of the hole,\n * and a label sitting on the leader's final run as if the run were its rule.\n *\n * The gesture is convergence. The hole starts wide and soft and closes onto the\n * target while the scrim darkens, which is what a lamp does; the ring and the\n * leader then draw in that order, so the eye is walked from the light to the\n * words. By the end of the enter window everything is still: the held frame is\n * a designed annotation diagram, not a halfway state.\n */\nconst SpotlightCallout: React.FC<Props> = ({\n  label,\n  note,\n  step,\n  target,\n  targetRow,\n  focus,\n  panelTitle,\n  actionLabel,\n  rows,\n  dim,\n  softness,\n  ring,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  // One type unit for the whole scene. Taking the larger of the width and a\n  // 16:9-equivalent of the height stops a 9:16 or 4:5 cut rendering at half size.\n  const unit = Math.max(width, height * 0.72);\n  const min = Math.min(width, height);\n  const l = buildLayout(width, height, rows, t.radius);\n\n  const rowIndex = clamp(Math.round(targetRow), 1, l.rows.length) - 1;\n\n  const targetRect: Rect =\n    target === \"search\"\n      ? l.search\n      : target === \"nav\"\n        ? l.nav[Math.min(CURRENT_NAV, l.nav.length - 1)]\n        : target === \"row\"\n          ? l.rows[rowIndex]\n          : target === \"custom\"\n            ? {\n                x: (focus.x - focus.w / 2) * width,\n                y: (focus.y - focus.h / 2) * height,\n                w: focus.w * width,\n                h: focus.h * height,\n              }\n            : l.action;\n\n  // The hole is the target plus a margin of light, so the control sits inside\n  // the lit area instead of being cropped by it.\n  const holePad = min * 0.022;\n  const hole = outset(targetRect, holePad);\n  // The hole follows the target's own corner, opened out by the margin of light.\n  // Letting it run to half the short side would turn every button into a pill.\n  const holeR = Math.min(Math.min(hole.w, hole.h) * 0.3, l.radius.control + holePad);\n\n  // Convergence: the hole comes in oversized and over-soft and closes onto the\n  // target. Expanding by a constant distance rather than scaling keeps a wide\n  // row target and a small button target feeling like the same lamp.\n  const open = beat(stage.frame, 3, 26);\n  const spread = (1 - open) * min * 0.07;\n  const lit = outset(hole, spread);\n  const blur = min * (0.005 + softness * 0.024) * (1 + (1 - open) * 1.1);\n\n  const scrim = dim * beat(stage.frame, 2, 24, EASING.travel);\n  const uid = scopedId([target, rowIndex, rows, Math.round(hole.x), Math.round(hole.y), Math.round(hole.w), Math.round(hole.h), label]);\n\n  // The ring rides the settled hole, not the converging one — it draws at frame\n  // 20+, by which point the light has landed, and a ring that chased the hole\n  // would read as two gestures instead of one.\n  const ringPad = holePad * 0.85;\n  const ringRect = outset(hole, ringPad);\n  const ringR = Math.min(Math.min(ringRect.w, ringRect.h) * 0.3, holeR + ringPad);\n\n  const leader = planLeader(width, height, ringRect, l.panel);\n  const strokeW = Math.max(1.4, l.hair * 1.35);\n\n  // A pool of deeper shadow under the label, gathering just before the type\n  // lands on it. Light in one place means shadow somewhere else, so this is\n  // physical rather than decorative — and it is what holds the annotation clear\n  // of the dimmed rows it crosses. It carries the same hole mask, so it can\n  // never reach into the lit area.\n  const pool = {\n    cx: leader.label.left + leader.label.width / 2,\n    cy: leader.label.rule - unit * 0.055,\n    rx: leader.label.width * 1.3,\n    ry: Math.max(unit * 0.17, height * 0.24),\n  };\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      {/* Presence lives on this one group: the scene arrives and leaves as a\n          single object, so overlapping it with a neighbour cross-fades cleanly.\n          The slight scale is the whole frame settling, not a flourish. */}\n      <AbsoluteFill\n        style={{\n          opacity: stage.presence,\n          transform: `scale(${interpolate(stage.enter, [0, 1], [1.018, 1])})`,\n        }}\n      >\n        <SynthPanel\n          t={t}\n          l={l}\n          unit={unit}\n          panelTitle={panelTitle}\n          actionLabel={actionLabel}\n          activeRow={target === \"row\" ? rowIndex : null}\n        />\n\n        <Scrim\n          uid={uid}\n          width={width}\n          height={height}\n          hole={lit}\n          radius={holeR + spread * 0.5}\n          blur={blur}\n          opacity={scrim}\n          glow={open * 0.9}\n          pool={pool}\n          poolOpacity={beat(stage.frame, 30, 54, EASING.travel)}\n        />\n\n        <svg\n          width={width}\n          height={height}\n          viewBox={`0 0 ${width} ${height}`}\n          style={{ position: \"absolute\", left: 0, top: 0, overflow: \"visible\" }}\n        >\n          {ring ? <TargetRing rect={ringRect} radius={ringR} colour={t.accent} weight={strokeW} frame={stage.frame} /> : null}\n          <Leader\n            path={leader.path}\n            anchor={leader.anchor}\n            colour={t.accent}\n            weight={strokeW}\n            dotR={strokeW * 2.3}\n            frame={stage.frame}\n          />\n        </svg>\n\n        <CalloutLabel\n          t={t}\n          unit={unit}\n          label={label}\n          note={note}\n          step={step}\n          left={leader.label.left}\n          width={leader.label.width}\n          bottom={height - (leader.label.rule - unit * 0.014)}\n          room={leader.label.room - unit * 0.014}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------- scrim */\n\n/**\n * The scrim, and the hole in it.\n *\n * A `radial-gradient` mask would be one line, but it can only ever be an\n * ellipse: UI targets are rounded rectangles, and an ellipse over a button\n * either clips the corners or floods the neighbours. So the mask is a real\n * rounded rect blurred by `feGaussianBlur`, which gives a rectangular hole\n * with a genuine falloff — soft edge, correct shape. `sRGB` interpolation\n * keeps that falloff looking the way a designer expects rather than the way\n * the SVG spec's default linearRGB would render it, and the filter region is\n * declared in user space because the default bounding box would crop the blur.\n */\nconst Scrim: React.FC<{\n  uid: string;\n  width: number;\n  height: number;\n  hole: Rect;\n  radius: number;\n  blur: number;\n  opacity: number;\n  glow: number;\n  pool: { cx: number; cy: number; rx: number; ry: number };\n  poolOpacity: number;\n}> = ({ uid, width, height, hole, radius, blur, opacity, glow, pool, poolOpacity }) => (\n  <svg\n    width={width}\n    height={height}\n    viewBox={`0 0 ${width} ${height}`}\n    style={{ position: \"absolute\", left: 0, top: 0 }}\n  >\n    <defs>\n      <filter\n        id={`${uid}-soft`}\n        filterUnits=\"userSpaceOnUse\"\n        colorInterpolationFilters=\"sRGB\"\n        x={hole.x - blur * 4}\n        y={hole.y - blur * 4}\n        width={hole.w + blur * 8}\n        height={hole.h + blur * 8}\n      >\n        <feGaussianBlur stdDeviation={blur} />\n      </filter>\n\n      <mask id={`${uid}-hole`} maskUnits=\"userSpaceOnUse\" x={0} y={0} width={width} height={height}>\n        <rect x={0} y={0} width={width} height={height} fill=\"#fff\" />\n        <rect\n          x={hole.x}\n          y={hole.y}\n          width={hole.w}\n          height={hole.h}\n          rx={radius}\n          ry={radius}\n          fill=\"#000\"\n          filter={`url(#${uid}-soft)`}\n        />\n      </mask>\n\n      {/* A whisper of light on the surface inside the hole. Elliptical falloff in\n          bounding-box units, so it stretches with a wide target instead of\n          leaving a circular hotspot on it. */}\n      <radialGradient id={`${uid}-glow`}>\n        <stop offset=\"0\" stopColor=\"#fff\" stopOpacity={0.09} />\n        <stop offset=\"1\" stopColor=\"#fff\" stopOpacity={0} />\n      </radialGradient>\n\n      <radialGradient id={`${uid}-pool`}>\n        <stop offset=\"0\" stopColor=\"#000\" stopOpacity={0.4} />\n        <stop offset=\"0.45\" stopColor=\"#000\" stopOpacity={0.26} />\n        <stop offset=\"1\" stopColor=\"#000\" stopOpacity={0} />\n      </radialGradient>\n    </defs>\n\n    <rect x={0} y={0} width={width} height={height} fill=\"#000\" opacity={opacity} mask={`url(#${uid}-hole)`} />\n    <ellipse\n      cx={pool.cx}\n      cy={pool.cy}\n      rx={pool.rx}\n      ry={pool.ry}\n      fill={`url(#${uid}-pool)`}\n      opacity={poolOpacity}\n      mask={`url(#${uid}-hole)`}\n    />\n    <ellipse\n      cx={hole.x + hole.w / 2}\n      cy={hole.y + hole.h / 2}\n      rx={hole.w * 0.72}\n      ry={hole.h * 0.95}\n      fill={`url(#${uid}-glow)`}\n      opacity={glow}\n    />\n  </svg>\n);\n\n/* -------------------------------------------------------------- annotation */\n\n/** The hairline target, drawn on clockwise once the light has landed. */\nconst TargetRing: React.FC<{\n  rect: Rect;\n  radius: number;\n  colour: string;\n  weight: number;\n  frame: number;\n}> = ({ rect, radius, colour, weight, frame }) => {\n  const path = translatePath(\n    makeRect({ width: rect.w, height: rect.h, cornerRadius: radius }).path,\n    rect.x,\n    rect.y,\n  );\n  const draw = beat(frame, 20, 40, EASING.travel);\n  return (\n    <path\n      d={path}\n      fill=\"none\"\n      stroke={colour}\n      strokeWidth={weight}\n      strokeLinecap=\"round\"\n      opacity={0.85}\n      {...evolvePath(draw, path)}\n    />\n  );\n};\n\n/**\n * The leader. It draws from the hole outward, which is the direction the eye\n * has to travel; a leader that drew from the label back to the target would\n * read as the label pointing at itself. The dot lands first, as the anchor the\n * line leaves from.\n */\nconst Leader: React.FC<{\n  path: string;\n  anchor: { x: number; y: number };\n  colour: string;\n  weight: number;\n  dotR: number;\n  frame: number;\n}> = ({ path, anchor, colour, weight, dotR, frame }) => {\n  const pin = beat(frame, 24, 32, EASING.accent);\n  const draw = beat(frame, 27, 50, EASING.travel);\n  return (\n    <>\n      <circle cx={anchor.x} cy={anchor.y} r={dotR * pin} fill={colour} />\n      <path\n        d={path}\n        fill=\"none\"\n        stroke={colour}\n        strokeWidth={weight}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n        {...evolvePath(draw, path)}\n      />\n    </>\n  );\n};\n\n/**\n * The label block. It is anchored by its *bottom* edge, just above the leader's\n * final horizontal run, so the run doubles as the rule the type sits on and the\n * block can grow upward as the copy wraps without the rule moving. Each line\n * arrives on its own beat, in reading order, after the line has reached it.\n */\nconst CalloutLabel: React.FC<{\n  t: BrandTokens;\n  unit: number;\n  label: string;\n  note?: string;\n  step?: string;\n  left: number;\n  width: number;\n  bottom: number;\n  room: number;\n}> = ({ t, unit, label, note, step, left, width, bottom, room }) => {\n  const stage = useStage(BUDGETS.scene);\n  // Fit to the room the leader left it. A full block of label needs about this\n  // much height; where the frame cannot give it — a spotlight so wide it barely\n  // dims anything — the whole block steps down together rather than colliding\n  // with the light. In every ordinary composition `fit` is exactly 1.\n  const fit = clamp(room / (unit * 0.155), 0.64, 1);\n  const titleSize = unit * 0.029 * fit;\n  const onScrim = inkOnScrim(t);\n\n  return (\n    <div style={{ position: \"absolute\", left, bottom, width, maxWidth: width }}>\n      {step ? (\n        <Reveal frame={stage.frame} at={44} shift={unit * 0.008}>\n          <div\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: unit * 0.0125 * fit,\n              fontWeight: 600,\n              letterSpacing: unit * 0.0125 * fit * 0.22,\n              textTransform: \"uppercase\",\n              color: t.accent,\n              marginBottom: titleSize * 0.34,\n            }}\n          >\n            {step}\n          </div>\n        </Reveal>\n      ) : null}\n\n      <Reveal frame={stage.frame} at={46} shift={unit * 0.012}>\n        <div\n          style={{\n            fontFamily: t.fontDisplay,\n            fontSize: titleSize,\n            fontWeight: 700,\n            lineHeight: 1.08,\n            letterSpacing: titleSize * -0.022,\n            color: onScrim,\n          }}\n        >\n          {label}\n        </div>\n      </Reveal>\n\n      {note ? (\n        <Reveal frame={stage.frame} at={52} shift={unit * 0.01}>\n          <div\n            style={{\n              fontFamily: t.fontBody,\n              fontSize: unit * 0.0155 * fit,\n              fontWeight: 400,\n              lineHeight: 1.45,\n              // The brand's muted ink is tuned for the brand's surface, not for\n              // a veil; the light ink held back is the muted step that reads on\n              // either polarity.\n              color: onScrim,\n              opacity: 0.72,\n              marginTop: titleSize * 0.36,\n            }}\n          >\n            {note}\n          </div>\n        </Reveal>\n      ) : null}\n    </div>\n  );\n};\n\n/** One internal beat: fade up while rising a little. */\nconst Reveal: React.FC<{ frame: number; at: number; shift: number; children: React.ReactNode }> = ({\n  frame,\n  at,\n  shift,\n  children,\n}) => {\n  const p = beat(frame, at, at + 14);\n  return <div style={{ opacity: p, transform: `translateY(${(1 - p) * shift}px)` }}>{children}</div>;\n};\n\n/* ------------------------------------------------------------- synthesised UI */\n\n/**\n * The product behind the scrim, drawn with divs and one inline SVG glyph — no\n * screenshot, no assets. Almost all of it is abstract bars: the scrim is about\n * to bury this layer, and inventing rows of fake names would only compete with\n * the two strings that matter, the section title and the button. Bar widths\n * come from `random()` with fixed seeds, so the panel looks written rather than\n * ruled and renders identically on every machine and every frame.\n */\nconst SynthPanel: React.FC<{\n  t: BrandTokens;\n  l: Layout;\n  unit: number;\n  panelTitle: string;\n  actionLabel: string;\n  activeRow: number | null;\n}> = ({ t, l, unit, panelTitle, actionLabel, activeRow }) => {\n  const dot = l.chromeH * 0.13;\n  const avatar = l.chromeH * 0.4;\n  const markSize = l.chip.h * 0.62;\n  const chipTextX = l.chip.x + markSize + l.chip.w * 0.09;\n  const chipTextW = l.chip.w - markSize - l.chip.w * 0.09;\n  const glyph = l.search.h * 0.42;\n\n  return (\n    <>\n      {/* Window */}\n      <div\n        style={{\n          ...box(l.panel),\n          backgroundColor: t.surface,\n          borderRadius: l.radius.panel,\n          border: `${l.hair}px solid ${t.rule}`,\n        }}\n      />\n\n      {/* Chrome: three dots, a session avatar, and the hairline that closes the bar */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: l.panel.x + l.panel.w * 0.018,\n          top: l.bodyY - l.chromeH / 2 - dot / 2,\n          display: \"flex\",\n          gap: dot * 0.85,\n        }}\n      >\n        {[0, 1, 2].map((i) => (\n          <div key={i} style={{ width: dot, height: dot, borderRadius: dot, backgroundColor: t.rule }} />\n        ))}\n      </div>\n      <div\n        style={{\n          ...box({\n            x: l.panel.x + l.panel.w - l.panel.w * 0.018 - avatar,\n            y: l.bodyY - l.chromeH / 2 - avatar / 2,\n            w: avatar,\n            h: avatar,\n          }),\n          borderRadius: avatar,\n          backgroundColor: t.rule,\n        }}\n      />\n      <div style={{ ...box({ x: l.panel.x, y: l.bodyY, w: l.panel.w, h: l.hair }), backgroundColor: t.rule }} />\n\n      {/* Sidebar */}\n      <div\n        style={{\n          ...box({ x: l.sidebar.x + l.sidebar.w, y: l.bodyY, w: l.hair, h: l.bodyH }),\n          backgroundColor: t.rule,\n        }}\n      />\n      <div\n        style={{\n          ...box({ x: l.chip.x, y: l.chip.y + (l.chip.h - markSize) / 2, w: markSize, h: markSize }),\n          backgroundColor: t.accent,\n          borderRadius: l.radius.control,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: t.fontDisplay,\n          fontSize: markSize * 0.52,\n          fontWeight: 700,\n          lineHeight: 1,\n          color: t.accentInk,\n        }}\n      >\n        {(panelTitle.trim().charAt(0) || \"M\").toUpperCase()}\n      </div>\n      <Bar\n        rect={{ x: chipTextX, y: midY(l.chip) - l.chip.h * 0.19, w: chipTextW * 0.72, h: l.chip.h * 0.14 }}\n        colour={t.inkMuted}\n        opacity={0.75}\n      />\n      <Bar\n        rect={{ x: chipTextX, y: midY(l.chip) + l.chip.h * 0.04, w: chipTextW * 0.46, h: l.chip.h * 0.11 }}\n        colour={t.rule}\n      />\n\n      {[...l.nav, l.navFoot].map((rect, i) => (\n        <NavItem\n          key={i}\n          t={t}\n          l={l}\n          rect={rect}\n          index={i}\n          active={i === CURRENT_NAV}\n        />\n      ))}\n\n      {/* Main header: the section, the field, the button */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: l.main.x,\n          top: l.main.y,\n          width: l.main.w * 0.4,\n          height: l.headerH,\n          display: \"flex\",\n          alignItems: \"center\",\n          fontFamily: t.fontDisplay,\n          fontSize: unit * 0.023,\n          fontWeight: 600,\n          letterSpacing: unit * 0.023 * -0.018,\n          color: t.ink,\n        }}\n      >\n        {panelTitle}\n      </div>\n\n      <div\n        style={{\n          ...box(l.search),\n          borderRadius: l.radius.control,\n          border: `${l.hair}px solid ${t.rule}`,\n        }}\n      />\n      <svg\n        width={glyph}\n        height={glyph}\n        viewBox=\"0 0 24 24\"\n        style={{\n          position: \"absolute\",\n          left: l.search.x + l.search.h * 0.42,\n          top: midY(l.search) - glyph / 2,\n        }}\n        fill=\"none\"\n        stroke={t.inkMuted}\n        strokeWidth={2.4}\n        strokeLinecap=\"round\"\n      >\n        <circle cx=\"10\" cy=\"10\" r=\"6.5\" />\n        <path d=\"M15 15 L20.5 20.5\" />\n      </svg>\n      <Bar\n        rect={{\n          x: l.search.x + l.search.h * 0.42 + glyph + l.search.h * 0.3,\n          y: midY(l.search) - l.search.h * 0.075,\n          w: l.search.w * 0.42,\n          h: l.search.h * 0.15,\n        }}\n        colour={t.rule}\n      />\n\n      <div\n        style={{\n          ...box(l.action),\n          borderRadius: l.radius.control,\n          backgroundColor: t.accent,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          gap: l.action.h * 0.2,\n        }}\n      >\n        <svg\n          width={l.action.h * 0.3}\n          height={l.action.h * 0.3}\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke={t.accentInk}\n          strokeWidth={3}\n          strokeLinecap=\"round\"\n        >\n          <path d=\"M12 4 L12 20 M4 12 L20 12\" />\n        </svg>\n        <span\n          style={{\n            fontFamily: t.fontBody,\n            fontSize: unit * 0.0135,\n            fontWeight: 600,\n            letterSpacing: unit * 0.0135 * 0.01,\n            color: t.accentInk,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {actionLabel}\n        </span>\n      </div>\n\n      {/* Column heads, on the same three columns the rows use */}\n      <Bar\n        rect={{\n          x: l.main.x + l.main.w * 0.022 + l.rows[0].h * 0.52 + l.main.w * 0.016,\n          y: midY(l.columns) - l.columns.h * 0.07,\n          w: l.main.w * 0.07,\n          h: l.columns.h * 0.14,\n        }}\n        colour={t.rule}\n      />\n      <Bar\n        rect={{\n          x: l.main.x + l.main.w * 0.62,\n          y: midY(l.columns) - l.columns.h * 0.07,\n          w: l.main.w * 0.05,\n          h: l.columns.h * 0.14,\n        }}\n        colour={t.rule}\n      />\n      <Bar\n        rect={{\n          x: l.main.x + l.main.w - l.main.w * 0.022 - l.main.w * 0.04,\n          y: midY(l.columns) - l.columns.h * 0.07,\n          w: l.main.w * 0.04,\n          h: l.columns.h * 0.14,\n        }}\n        colour={t.rule}\n      />\n      <div\n        style={{\n          ...box({ x: l.main.x, y: l.columns.y + l.columns.h, w: l.main.w, h: l.hair }),\n          backgroundColor: t.rule,\n        }}\n      />\n\n      {l.rows.map((rect, i) => (\n        <PanelRow\n          key={i}\n          t={t}\n          l={l}\n          rect={rect}\n          index={i}\n          active={activeRow === i}\n          last={i === l.rows.length - 1}\n        />\n      ))}\n    </>\n  );\n};\n\n/** A rounded bar. Stands in for a line of text everywhere the copy is not the point. */\nconst Bar: React.FC<{ rect: Rect; colour: string; opacity?: number }> = ({ rect, colour, opacity }) => (\n  <div\n    style={{\n      ...box(rect),\n      backgroundColor: colour,\n      borderRadius: rect.h,\n      opacity: opacity ?? 1,\n    }}\n  />\n);\n\nconst NavItem: React.FC<{\n  t: BrandTokens;\n  l: Layout;\n  rect: Rect;\n  index: number;\n  active: boolean;\n}> = ({ t, l, rect, index, active }) => {\n  const g = rect.h * 0.42;\n  const tint = active ? t.inkMuted : t.rule;\n  return (\n    <>\n      {active ? (\n        <div style={{ ...box(rect), backgroundColor: t.rule, opacity: 0.55, borderRadius: l.radius.row }} />\n      ) : null}\n      <div\n        style={{\n          ...box({ x: rect.x + rect.h * 0.28, y: midY(rect) - g / 2, w: g, h: g }),\n          backgroundColor: tint,\n          borderRadius: g * 0.28,\n        }}\n      />\n      <Bar\n        rect={{\n          x: rect.x + rect.h * 0.28 + g + rect.h * 0.3,\n          y: midY(rect) - rect.h * 0.08,\n          w: rect.w * (0.38 + random(`spot-nav-${index}`) * 0.26),\n          h: rect.h * 0.16,\n        }}\n        colour={tint}\n      />\n    </>\n  );\n};\n\n/**\n * One list row: avatar, two lines of name, a status chip and a trailing figure.\n * The active row carries a lift, which is what makes a spotlight on it read as\n * \"this row\", and one status dot takes the second accent so the table is not\n * entirely grey.\n */\nconst PanelRow: React.FC<{\n  t: BrandTokens;\n  l: Layout;\n  rect: Rect;\n  index: number;\n  active: boolean;\n  last: boolean;\n}> = ({ t, l, rect, index, active, last }) => {\n  const pad = rect.w * 0.022;\n  const av = rect.h * 0.52;\n  const textX = rect.x + pad + av + rect.w * 0.016;\n  const cy = midY(rect);\n  const pill: Rect = { x: rect.x + rect.w * 0.62, y: cy - rect.h * 0.13, w: rect.w * 0.105, h: rect.h * 0.26 };\n  const pillDot = pill.h * 0.3;\n\n  return (\n    <>\n      {active ? (\n        <div style={{ ...box(rect), backgroundColor: t.rule, opacity: 0.5, borderRadius: l.radius.row }} />\n      ) : null}\n\n      <div\n        style={{\n          ...box({ x: rect.x + pad, y: cy - av / 2, w: av, h: av }),\n          borderRadius: av,\n          backgroundColor: t.rule,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          fontFamily: t.fontMono,\n          fontSize: av * 0.33,\n          fontWeight: 500,\n          letterSpacing: av * 0.01,\n          lineHeight: 1,\n          color: t.inkMuted,\n        }}\n      >\n        {INITIALS[index % INITIALS.length]}\n      </div>\n\n      <Bar\n        rect={{\n          x: textX,\n          y: cy - rect.h * 0.155,\n          w: rect.w * (0.12 + random(`spot-name-${index}`) * 0.085),\n          h: rect.h * 0.135,\n        }}\n        colour={t.inkMuted}\n        opacity={active ? 0.95 : 0.8}\n      />\n      <Bar\n        rect={{\n          x: textX,\n          y: cy + rect.h * 0.045,\n          w: rect.w * (0.075 + random(`spot-meta-${index}`) * 0.05),\n          h: rect.h * 0.1,\n        }}\n        colour={t.rule}\n      />\n\n      <div style={{ ...box(pill), borderRadius: pill.h, border: `${l.hair}px solid ${t.rule}` }} />\n      <div\n        style={{\n          ...box({ x: pill.x + pill.h * 0.34, y: cy - pillDot / 2, w: pillDot, h: pillDot }),\n          borderRadius: pillDot,\n          backgroundColor: index === 0 ? t.accentAlt : t.rule,\n        }}\n      />\n      <Bar\n        rect={{\n          x: pill.x + pill.h * 0.34 + pillDot + pill.h * 0.26,\n          y: cy - pill.h * 0.075,\n          w: pill.w * 0.42,\n          h: pill.h * 0.15,\n        }}\n        colour={t.rule}\n      />\n\n      <Bar\n        rect={{\n          x: rect.x + rect.w - pad - rect.w * 0.055,\n          y: cy - rect.h * 0.06,\n          w: rect.w * 0.055,\n          h: rect.h * 0.12,\n        }}\n        colour={t.rule}\n      />\n\n      {last ? null : (\n        <div\n          style={{\n            ...box({ x: rect.x, y: rect.y + rect.h + l.rowGap / 2, w: rect.w, h: l.hair }),\n            backgroundColor: t.rule,\n            opacity: 0.7,\n          }}\n        />\n      )}\n    </>\n  );\n};\n\n/** Fixed initials, so avatars are drawn type rather than an asset. */\nconst INITIALS = [\"AK\", \"MR\", \"JT\", \"LP\", \"DS\", \"NB\", \"CV\"];\n\nexport default defineMotionItem({\n  id: \"spotlight-callout\",\n  name: \"Spotlight Callout\",\n  category: \"titles\",\n  summary:\n    \"A scrim closes over a synthesised product panel and a soft-edged hole opens on one control, labelled by a leader line\",\n  tags: [\"spotlight\", \"callout\", \"product tour\", \"scrim\", \"svg mask\", \"leader line\", \"annotation\", \"onboarding\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: spotlightCalloutSchema,\n  defaults: {\n    label: \"Start the render here\",\n    note: \"One button turns the typed brief into a finished scene.\",\n    step: \"Step 02\",\n    target: \"action\",\n    targetRow: 2,\n    focus: { x: 0.68, y: 0.3, w: 0.19, h: 0.11 },\n    panelTitle: \"Workspace\",\n    actionLabel: \"Render scene\",\n    rows: 5,\n    dim: 0.58,\n    softness: 0.5,\n    ring: true,\n    brand: \"slate\",\n  },\n  component: SpotlightCallout,\n  file: \"src/registry/items/spotlight-callout.tsx\",\n  origin: {\n    prompt:\n      \"A product-tour callout: draw a fake app panel with a sidebar, a list of rows and a primary button, then drop a dark scrim over the whole frame with a soft-edged hole punched around the button so it looks like a spotlight rather than a cut-out. The hole should start wide and close onto the button as the scrim darkens. Then draw a thin accent line out of the hole, elbow it down and across, and set a short label on the end of it.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A product-tour callout: draw a fake app panel with a sidebar, a list of rows and a primary button, then drop a dark scrim over the whole frame with a soft-edged hole punched around the button so it looks like a spotlight rather than a cut-out. The hole should start wide and close onto the button as the scrim darkens. Then draw a thin accent line out of the hole, elbow it down and across, and set a short label on the end of it.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2241,
      "props": [
        {
          "name": "label",
          "type": "string",
          "description": "The callout headline — what the spotlit thing is or does. Three to six words; it sets on one or two lines above the leader line.",
          "required": true,
          "defaultValue": "Start the render here",
          "options": null
        },
        {
          "name": "note",
          "type": "string",
          "description": "One supporting sentence under the label. A callout is a pointer, not a paragraph — keep it to a single clause.",
          "required": false,
          "defaultValue": "One button turns the typed brief into a finished scene.",
          "options": null
        },
        {
          "name": "step",
          "type": "string",
          "description": "Tiny mono kicker above the label, e.g. 'Step 02' or 'Start here'. Omit for an unnumbered annotation.",
          "required": false,
          "defaultValue": "Step 02",
          "options": null
        },
        {
          "name": "target",
          "type": "\"action\" | \"search\" | \"nav\" | \"row\" | \"custom\"",
          "description": "Which element of the synthesised panel the hole opens on. 'action' is the primary button, 'search' the field beside it, 'nav' the current sidebar item, 'row' a list row, 'custom' the `focus` rect.",
          "required": false,
          "defaultValue": "action",
          "options": [
            "action",
            "search",
            "nav",
            "row",
            "custom"
          ]
        },
        {
          "name": "targetRow",
          "type": "number",
          "description": "Which list row to spotlight, 1-based, when `target` is 'row'. Clamped to the number of rows drawn.",
          "required": false,
          "defaultValue": "2",
          "options": null
        },
        {
          "name": "focus",
          "type": "object",
          "description": "The spotlight rect, in frame fractions so it survives a change of aspect. Only read when `target` is 'custom'.",
          "required": false,
          "defaultValue": "object",
          "options": null
        },
        {
          "name": "panelTitle",
          "type": "string",
          "description": "Section heading in the panel behind the scrim. One or two words — it is set dressing, and its initial doubles as the app's logo mark.",
          "required": false,
          "defaultValue": "Workspace",
          "options": null
        },
        {
          "name": "actionLabel",
          "type": "string",
          "description": "Label on the panel's primary button. With the default target this is the one piece of UI copy the spotlight actually lets you read, so make it the verb you are teaching.",
          "required": false,
          "defaultValue": "Render scene",
          "options": null
        },
        {
          "name": "rows",
          "type": "number",
          "description": "How many list rows the panel draws. They divide the panel body, so more rows means tighter rows.",
          "required": false,
          "defaultValue": "5",
          "options": null
        },
        {
          "name": "dim",
          "type": "number",
          "description": "How far the scrim darkens everything outside the hole. On a dark brand this bites hard — much past 0.7 the surrounding UI stops reading as context at all; a light brand will take 0.8.",
          "required": false,
          "defaultValue": "0.58",
          "options": null
        },
        {
          "name": "softness",
          "type": "number",
          "description": "Feather on the hole edge. 0 is a near-hard cut that looks like a mask; 1 is a wide falloff that looks like a lamp. Mid values read as light.",
          "required": false,
          "defaultValue": "0.5",
          "options": null
        },
        {
          "name": "ring",
          "type": "boolean",
          "description": "Draw a hairline accent rectangle just outside the hole. Turn it off when the light alone is enough.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "stacked-headline-beats",
      "name": "Stacked Headline Beats",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "One word per beat, hard cut on the count, with a slow punch-in that lands on an accent payoff word",
      "tags": [
        "hard cut",
        "one word",
        "beats",
        "punch in",
        "kinetic type",
        "rhythm"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/stacked-headline-beats.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, totalFrames, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nexport const stackedHeadlineBeatsSchema = z.object({\n  words: z\n    .array(z.string())\n    .min(1)\n    .default([\"MOTION\", \"AS\", \"CODE.\", \"ONE\", \"FILE.\", \"ZERO\", \"TIMELINE.\", \"RENDER.\"])\n    .describe(\n      \"One word per beat, in order. Six to ten reads best. Short words hit hardest; anything over nine characters is scaled down to fit. Trailing full stops are a deliberate part of this style. The final entry is the payoff and lands in the accent colour.\",\n    ),\n  beat: z\n    .number()\n    .int()\n    .min(4)\n    .max(72)\n    .default(20)\n    .describe(\n      \"Frames each word holds before the hard cut, at 30fps. 15 = ~120bpm and feels urgent, 20 is confident, 30+ starts to feel like a slideshow. Clamped down automatically if the word count would otherwise crowd out the final hold.\",\n    ),\n  label: z\n    .string()\n    .optional()\n    .describe(\"Small mono label in the top-left corner. Omit it for a bare, poster-like frame.\"),\n  letterCase: z\n    .enum([\"upper\", \"asis\"])\n    .default(\"upper\")\n    .describe(\"'upper' forces caps, which is the intended look. 'asis' respects the casing you pass.\"),\n  punch: z\n    .number()\n    .min(0)\n    .max(0.2)\n    .default(0.04)\n    .describe(\n      \"How much the word grows across its own hold — 0.04 is a 4% push, felt rather than seen. 0 freezes each word dead still.\",\n    ),\n  ticks: z\n    .boolean()\n    .default(true)\n    .describe(\"Show the footer: hairline rule, beat ticks and the beat counter. Off leaves only the word.\"),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof stackedHeadlineBeatsSchema>;\n\nconst BUDGET = BUDGETS.feature;\nconst TOTAL = totalFrames(BUDGET);\n\n/**\n * Rough uppercase advance width of the display face as a fraction of font size.\n * Used only to pick a size that cannot overflow the measure — it is a fit\n * guard, not a layout engine, so it errs small.\n */\nconst ADVANCE = 0.66;\n\n/**\n * The beat plan.\n *\n * Words cut on a fixed interval, but the last one absorbs everything left in\n * the budget: the payoff has to outlast the rhythm that set it up, and the\n * sequence has to end exactly on the budget so `exit` still owns the handoff.\n * If the requested beat is too slow to fit the words and still leave the\n * finale its share, the beat is clamped rather than the word list truncated —\n * dropping a word the caller asked for would be the worse surprise.\n */\nconst resolveBeat = (count: number, requested: number, total: number) => {\n  if (count <= 1) return total;\n  const finale = Math.round(total * 0.24);\n  const ceiling = Math.max(4, Math.floor((total - finale) / (count - 1)));\n  return Math.max(4, Math.min(Math.round(requested), ceiling));\n};\n\nconst pad = (n: number) => String(n).padStart(2, \"0\");\n\nconst StackedHeadlineBeats: React.FC<Props> = ({\n  words,\n  beat,\n  label,\n  letterCase,\n  punch,\n  ticks,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  // The schema enforces at least one word; this keeps a stray empty string\n  // from rendering an empty frame in a preview.\n  const cleaned = words.map((w) => w.trim()).filter(Boolean);\n  const list = cleaned.length > 0 ? cleaned : [\"MOTION\"];\n\n  const span = resolveBeat(list.length, beat, TOTAL);\n  const index = Math.min(list.length - 1, Math.max(0, Math.floor(stage.frame / span)));\n  const isLast = index === list.length - 1;\n  const word = list[index];\n\n  const start = index * span;\n  const end = isLast ? TOTAL : start + span;\n\n  /**\n   * The punch. Runs across whatever this word's own hold turns out to be, on\n   * the travel curve, so it starts and ends at rest: a snap on a 15-frame beat\n   * and a slow push on the long final hold. Same gesture, read at two speeds.\n   */\n  const push = interpolate(stage.frame, [start, Math.max(start + 1, end)], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  // Everything is derived from the frame so one file serves 16:9, 9:16, 1:1 and 4:5.\n  const gutter = width * 0.062;\n  const inset = height * 0.078;\n  const meta = Math.max(11, width * 0.0105);\n  const hair = Math.max(1, height * 0.0012);\n\n  // Size to the measure, then cap on height so a vertical cut cannot overscale.\n  const size = Math.min(height * 0.3, (width * 0.82) / Math.max(1, word.length * ADVANCE));\n\n  // The word sits above true centre only because the footer weights the bottom\n  // of the frame. Drop the footer and true centre is correct again.\n  const lift = ticks ? -height * 0.026 : 0;\n\n  // The two furniture blocks arrive from just outside their own edge, so the\n  // frame opens from the outside in. Offsets scale with the meta type size\n  // rather than being fixed px, so a vertical cut moves the same amount.\n  const metaLift = interpolate(stage.enter, [0, 1], [-meta * 0.8, 0]);\n  const footerLift = interpolate(stage.enter, [0, 1], [meta * 1.2, 0]);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        // One step of token-safe atmosphere: surface at the optical centre\n        // falling to canvas at the edges. Seats the type without a black scrim,\n        // so it reads the same on light brands as on dark ones.\n        backgroundImage: `radial-gradient(circle at 50% 44%, ${t.surface} 0%, ${t.bg} 64%)`,\n      }}\n    >\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: gutter,\n            top: inset,\n            display: \"flex\",\n            alignItems: \"center\",\n            gap: meta * 0.9,\n            opacity: stage.presence,\n            transform: `translateY(${metaLift}px)`,\n          }}\n        >\n          <span style={{ display: \"block\", width: meta * 2.4, height: hair, backgroundColor: t.rule }} />\n          <span\n            style={{\n              fontFamily: t.fontMono,\n              fontSize: meta,\n              fontWeight: 500,\n              letterSpacing: meta * 0.2,\n              textTransform: \"uppercase\",\n              color: t.inkMuted,\n            }}\n          >\n            {label}\n          </span>\n        </div>\n      ) : null}\n\n      {/* The word. Centred on purpose — the whole point of the device is that\n          the frame has exactly one thing in it — but lifted off true centre to\n          sit optically centred in the field left above the footer. */}\n      <AbsoluteFill\n        style={{\n          justifyContent: \"center\",\n          alignItems: \"center\",\n          padding: `0 ${gutter}px`,\n        }}\n      >\n        <div\n          style={{\n            opacity: stage.presence,\n            transform: `translateY(${lift}px) scale(${1 + punch * push})`,\n          }}\n        >\n          <span\n            style={{\n              display: \"block\",\n              fontFamily: t.fontDisplay,\n              fontSize: size,\n              // 700 is the heaviest Archivo weight the registry loads; asking\n              // for 800 would only get a synthesised fake bold.\n              fontWeight: 700,\n              lineHeight: 0.92,\n              letterSpacing: size * -0.035,\n              textTransform: letterCase === \"upper\" ? \"uppercase\" : \"none\",\n              whiteSpace: \"nowrap\",\n              textAlign: \"center\",\n              // Negative tracking is also applied after the last glyph, which\n              // pushes the ink right of the box centre. Balance it back.\n              marginRight: size * 0.035,\n              color: isLast ? t.accent : t.ink,\n            }}\n          >\n            {word}\n          </span>\n        </div>\n      </AbsoluteFill>\n\n      {ticks ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: gutter,\n            right: gutter,\n            bottom: inset,\n            opacity: stage.presence,\n            transform: `translateY(${footerLift}px)`,\n          }}\n        >\n          <div\n            style={{\n              height: hair,\n              backgroundColor: t.rule,\n              transform: `scaleX(${stage.enter})`,\n              transformOrigin: \"left\",\n            }}\n          />\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"space-between\",\n              marginTop: meta * 1.1,\n            }}\n          >\n            <BeatTicks\n              count={list.length}\n              index={index}\n              fill={push}\n              unit={Math.max(10, width * 0.019)}\n              thickness={Math.max(2, height * 0.0055)}\n              track={t.rule}\n              done={t.inkMuted}\n              live={isLast ? t.accent : t.ink}\n            />\n            <span\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: meta,\n                fontWeight: 500,\n                letterSpacing: meta * 0.16,\n                color: t.inkMuted,\n                fontVariantNumeric: \"tabular-nums\",\n              }}\n            >\n              {pad(index + 1)}\n              <span style={{ color: t.rule }}> / </span>\n              {pad(list.length)}\n            </span>\n          </div>\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n};\n\n/**\n * The beat ticks — a metronome, not decoration. Spent beats are solid, the live\n * beat fills left to right across its own hold, and the beats still to come are\n * empty track. On the last word the live fill turns accent with the type, so\n * the whole frame commits to one colour at the payoff and never before.\n */\nconst BeatTicks: React.FC<{\n  count: number;\n  index: number;\n  fill: number;\n  unit: number;\n  thickness: number;\n  track: string;\n  done: string;\n  live: string;\n}> = ({ count, index, fill, unit, thickness, track, done, live }) => (\n  <div style={{ display: \"flex\", alignItems: \"center\", gap: unit * 0.45 }}>\n    {Array.from({ length: count }, (_, i) => (\n      <span\n        key={i}\n        style={{\n          display: \"block\",\n          width: unit,\n          height: thickness,\n          backgroundColor: i < index ? done : track,\n          overflow: \"hidden\",\n        }}\n      >\n        <span\n          style={{\n            display: \"block\",\n            width: \"100%\",\n            height: \"100%\",\n            backgroundColor: live,\n            transform: `scaleX(${i === index ? fill : 0})`,\n            transformOrigin: \"left\",\n          }}\n        />\n      </span>\n    ))}\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"stacked-headline-beats\",\n  name: \"Stacked Headline Beats\",\n  category: \"typography\",\n  summary: \"One word per beat, hard cut on the count, with a slow punch-in that lands on an accent payoff word\",\n  tags: [\"hard cut\", \"one word\", \"beats\", \"punch in\", \"kinetic type\", \"rhythm\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: stackedHeadlineBeatsSchema,\n  defaults: {\n    words: [\"MOTION\", \"AS\", \"CODE.\", \"ONE\", \"FILE.\", \"ZERO\", \"TIMELINE.\", \"RENDER.\"],\n    beat: 20,\n    label: \"Motionstash\",\n    letterCase: \"upper\",\n    punch: 0.04,\n    ticks: true,\n    brand: \"slate\",\n  },\n  component: StackedHeadlineBeats,\n  file: \"src/registry/items/stacked-headline-beats.tsx\",\n  origin: {\n    prompt:\n      \"One huge word at a time, centred, hard-cutting to the next word every twenty frames like a cut on the beat — no cross-fades. Each word pushes in about four percent while it holds so it never sits still, and the last word lands in the accent colour and holds much longer than the rest. Give it a small mono label and a row of beat ticks so the frame looks composed rather than like a slide.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "One huge word at a time, centred, hard-cutting to the next word every twenty frames like a cut on the beat — no cross-fades. Each word pushes in about four percent while it holds so it never sits still, and the last word lands in the accent colour and holds much longer than the rest. Give it a small mono label and a row of beat ticks so the frame looks composed rather than like a slide.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7529,
      "props": [
        {
          "name": "words",
          "type": "array",
          "description": "One word per beat, in order. Six to ten reads best. Short words hit hardest; anything over nine characters is scaled down to fit. Trailing full stops are a deliberate part of this style. The final entry is the payoff and lands in the accent colour.",
          "required": false,
          "defaultValue": "8 items",
          "options": null
        },
        {
          "name": "beat",
          "type": "number",
          "description": "Frames each word holds before the hard cut, at 30fps. 15 = ~120bpm and feels urgent, 20 is confident, 30+ starts to feel like a slideshow. Clamped down automatically if the word count would otherwise crowd out the final hold.",
          "required": false,
          "defaultValue": "20",
          "options": null
        },
        {
          "name": "label",
          "type": "string",
          "description": "Small mono label in the top-left corner. Omit it for a bare, poster-like frame.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "letterCase",
          "type": "\"upper\" | \"asis\"",
          "description": "'upper' forces caps, which is the intended look. 'asis' respects the casing you pass.",
          "required": false,
          "defaultValue": "upper",
          "options": [
            "upper",
            "asis"
          ]
        },
        {
          "name": "punch",
          "type": "number",
          "description": "How much the word grows across its own hold — 0.04 is a 4% push, felt rather than seen. 0 freezes each word dead still.",
          "required": false,
          "defaultValue": "0.04",
          "options": null
        },
        {
          "name": "ticks",
          "type": "boolean",
          "description": "Show the footer: hairline rule, beat ticks and the beat counter. Off leaves only the word.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "stage-block-chart",
      "name": "Stage Block Chart",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "Extruded blocks grow in sequence with their figures above them, the last one deliberately running off the top of the frame",
      "tags": [
        "bar chart",
        "3d",
        "extruded",
        "stages",
        "growth",
        "callout",
        "finance"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/stage-block-chart.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * Extruded blocks that grow in sequence, each with its figure above it.\n *\n * The spine of every \"how a company scales\" explainer: four stages, each an\n * order of magnitude past the last, and the whole point is that the last block\n * is off the top of the frame. So the y scale is deliberately not fitted to the\n * data — the tallest block is allowed to run out of the frame, because being\n * unable to show it is the argument.\n *\n * Each block is three flat faces rather than a CSS 3D transform: a front, a top\n * and a right side, drawn as polygons off one shared vanishing offset. That\n * keeps the geometry exact at any frame size and, unlike a perspective\n * transform, it never softens the type on the front face.\n *\n * The camera pushes in slowly across the hold, so a chart that has finished\n * building is still moving.\n */\n\nconst BUDGET = BUDGETS.feature;\n\nexport const stageBlockChartSchema = z.object({\n  stages: z\n    .array(\n      z.object({\n        label: z.string().describe(\"Set on the block's front face — 'Stage 01'.\"),\n        sub: z.string().default(\"\").describe(\"Small line under the label, usually a year or a range.\"),\n        value: z.string().describe(\"The figure above the block, in the accent colour.\"),\n        height: z\n          .number()\n          .min(0.05)\n          .max(1.6)\n          .describe(\n            \"Block height as a fraction of the plot. Values above 1 deliberately run past the top of the frame — that is the point of the last stage.\",\n          ),\n        note: z\n          .string()\n          .default(\"\")\n          .describe(\"Optional callout text to the upper-left of the block, joined by an elbow line.\"),\n      }),\n    )\n    .min(2)\n    .max(5)\n    .describe(\"Two to five stages, drawn left to right and revealed in order.\"),\n  push: z\n    .number()\n    .min(0)\n    .max(0.3)\n    .default(0.08)\n    .describe(\"How far the camera pushes in across the shot, as a fraction of scale. 0 locks it off, which the house rules discourage.\"),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof stageBlockChartSchema>;\n\nexport const StageBlockChart: React.FC<Props> = ({ stages, push, brand }) => {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  const u = Math.min(width, height) / 1080;\n  const n = stages.length;\n\n  // Plot geometry. The baseline sits low; the blocks grow up from it.\n  const baseY = height * 0.86;\n  const plotH = height * 0.52;\n  const slotW = (width * 0.78) / n;\n  const blockW = slotW * 0.52;\n  const x0 = width * 0.13;\n\n  // Depth: one shared offset for every face, which is what makes them read as\n  // one scene rather than as separate boxes.\n  const dx = blockW * 0.26;\n  const dy = -blockW * 0.2;\n\n  const camera = 1 + push * interpolate(stage.progress, [0, 1], [0, 1]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill\n        style={{\n          opacity: stage.presence,\n          transform: `scale(${camera})`,\n          transformOrigin: \"50% 70%\",\n        }}\n      >\n        <svg width={width} height={height} viewBox={`0 0 ${width} ${height}`}>\n          {stages.map((s, i) => (\n            <Block\n              key={s.label + i}\n              index={i}\n              s={s}\n              x={x0 + slotW * i + (slotW - blockW) / 2}\n              baseY={baseY}\n              plotH={plotH}\n              w={blockW}\n              dx={dx}\n              dy={dy}\n              u={u}\n              t={t}\n              frame={frame}\n              frameW={width}\n            />\n          ))}\n        </svg>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nconst Block: React.FC<{\n  index: number;\n  s: { label: string; sub: string; value: string; height: number; note: string };\n  x: number;\n  baseY: number;\n  plotH: number;\n  w: number;\n  dx: number;\n  dy: number;\n  u: number;\n  t: (typeof BRANDS)[string];\n  frame: number;\n  frameW: number;\n}> = ({ index, s, x, baseY, plotH, w, dx, dy, u, t, frame, frameW }) => {\n  const st = useStaggeredStage(BUDGET, index, 11);\n\n  const grow = interpolate(st.enter, [0, 1], [0, 1], { easing: EASING.enter });\n  const h = plotH * s.height * grow;\n  const topY = baseY - h;\n\n  // Faces. The top and side are lit off the same offset, so the light is\n  // consistent across the row.\n  const front = `${x},${baseY} ${x},${topY} ${x + w},${topY} ${x + w},${baseY}`;\n  const top = `${x},${topY} ${x + dx},${topY + dy} ${x + w + dx},${topY + dy} ${x + w},${topY}`;\n  const side = `${x + w},${topY} ${x + w + dx},${topY + dy} ${x + w + dx},${baseY + dy} ${x + w},${baseY}`;\n\n  const valueOpacity = interpolate(st.enter, [0.55, 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const noteOpacity = interpolate(st.enter, [0.8, 1], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const labelY = topY + Math.min(h * 0.62, plotH * 0.3);\n\n  return (\n    <g>\n      {/*\n       * Three faces off one light, sampled from the reference rather than\n       * guessed: the front is nearly black, the right side is lifted and the top\n       * is lifted furthest, which puts the key light high and to the right. The\n       * intuitive ordering — front mid, side darkest — reads as a box with its\n       * lit face turned away, and is what this looked like before measuring.\n       */}\n      <polygon points={side} fill={shade(t.surface, 0.066)} />\n      <polygon points={top} fill={shade(t.surface, 0.128)} />\n      <polygon points={front} fill={shade(t.surface, -0.57)} />\n\n      {/* the figure, above the block */}\n      <text\n        x={x + w / 2}\n        y={topY - 34 * u}\n        textAnchor=\"middle\"\n        fill={t.accent}\n        fontFamily={t.fontDisplay}\n        fontWeight={500}\n        fontSize={82 * u}\n        opacity={valueOpacity}\n        style={{ letterSpacing: \"-0.03em\" }}\n      >\n        {s.value}\n      </text>\n\n      {/* label on the front face, skewed to sit on it */}\n      {h > plotH * 0.12 ? (\n        <g opacity={valueOpacity}>\n          <text\n            x={x + w / 2}\n            y={labelY}\n            textAnchor=\"middle\"\n            fill={t.ink}\n            fontFamily={t.fontDisplay}\n            fontWeight={500}\n            fontSize={30 * u}\n          >\n            {s.label}\n          </text>\n          {s.sub ? (\n            <text\n              x={x + w / 2}\n              y={labelY + 24 * u}\n              textAnchor=\"middle\"\n              fill={t.inkMuted}\n              fontFamily={t.fontBody}\n              fontSize={16 * u}\n            >\n              {s.sub}\n            </text>\n          ) : null}\n        </g>\n      ) : null}\n\n      {/* callout: text, a horizontal run, an elbow down, a square terminator */}\n      {s.note ? (\n        (() => {\n          /*\n           * Callouts wrap to two lines and are clamped inside the frame.\n           * Right-aligning a one-line note off the leftmost block ran it\n           * straight off the edge — the first stage is the one most likely to\n           * carry a note and the one with the least room for it.\n           */\n          const lines = wrapTwo(s.note);\n          const longest = Math.max(...lines.map((l) => l.length));\n          const textW = longest * 12.4 * u;\n          const end = Math.max(x - w * 0.55, frameW * 0.03 + textW);\n          const elbowX = end + 14 * u;\n          const dropX = Math.min(x + w * 0.3, elbowX + w * 0.5);\n          const topLine = topY - (lines.length > 1 ? 150 : 128) * u;\n\n          return (\n            <g opacity={noteOpacity}>\n              {lines.map((l, li) => (\n                <text\n                  key={li}\n                  x={end}\n                  y={topLine + li * 32 * u}\n                  textAnchor=\"end\"\n                  fill={t.ink}\n                  fontFamily={t.fontBody}\n                  fontSize={26 * u}\n                >\n                  {l}\n                </text>\n              ))}\n              <path\n                d={`M ${elbowX} ${topLine - 8 * u} H ${dropX - w * 0.16} L ${dropX} ${topY - 96 * u}`}\n                fill=\"none\"\n                stroke={t.accent}\n                strokeWidth={Math.max(1, 1.6 * u)}\n              />\n              <rect x={dropX - 4 * u} y={topY - 100 * u} width={8 * u} height={8 * u} fill={t.accent} />\n            </g>\n          );\n        })()\n      ) : null}\n    </g>\n  );\n};\n\n/** Balance a note over at most two lines, so a long callout is not one long run. */\nfunction wrapTwo(note: string) {\n  if (note.length <= 26) return [note];\n  const words = note.split(\" \");\n  let best = 1;\n  let bestDiff = Infinity;\n  for (let i = 1; i < words.length; i++) {\n    const a = words.slice(0, i).join(\" \").length;\n    const b = words.slice(i).join(\" \").length;\n    if (Math.abs(a - b) < bestDiff) {\n      bestDiff = Math.abs(a - b);\n      best = i;\n    }\n  }\n  return [words.slice(0, best).join(\" \"), words.slice(best).join(\" \")];\n}\n\n/** Lighten or darken a hex by a ratio, for the three faces of one block. */\nfunction shade(hex: string, amount: number) {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = parseInt(full, 16);\n  const ch = [(n >> 16) & 255, (n >> 8) & 255, n & 255].map((v) =>\n    Math.max(0, Math.min(255, Math.round(amount >= 0 ? v + (255 - v) * amount : v * (1 + amount)))),\n  );\n  return `rgb(${ch[0]}, ${ch[1]}, ${ch[2]})`;\n}\n\nexport default defineMotionItem({\n  id: \"stage-block-chart\",\n  name: \"Stage Block Chart\",\n  category: \"data\",\n  summary:\n    \"Extruded blocks grow in sequence with their figures above them, the last one deliberately running off the top of the frame\",\n  tags: [\"bar chart\", \"3d\", \"extruded\", \"stages\", \"growth\", \"callout\", \"finance\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: stageBlockChartSchema,\n  defaults: {\n    stages: [\n      { label: \"Stage 01\", sub: \"2016–2017\", value: \"$1M\", height: 0.28, note: \"running out of money before the product works\" },\n      { label: \"Stage 02\", sub: \"2018\", value: \"$10M\", height: 0.5, note: \"spending on growth that doesn't convert\" },\n      { label: \"Stage 03\", sub: \"2019\", value: \"$50M\", height: 0.78, note: \"\" },\n      { label: \"Stage 04\", sub: \"2021\", value: \"$100M+\", height: 1.08, note: \"costs you never saw coming\" },\n    ],\n    push: 0.08,\n    brand: \"ledger\",\n  },\n  component: StageBlockChart,\n  file: \"src/registry/items/stage-block-chart.tsx\",\n  origin: {\n    prompt:\n      \"Four extruded blocks growing left to right, each an order of magnitude past the last, with its figure set large above it in the accent colour and its stage label on the front face. Each block is three flat polygons — front, top and right side — sharing one vanishing offset, so the light is consistent across the row and the type on the front face stays crisp. The y scale is not fitted to the data: the last block runs past the top of the frame on purpose. Thin elbow callout lines point at individual stages, and the camera pushes in slowly across the whole shot.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Four extruded blocks growing left to right, each an order of magnitude past the last, with its figure set large above it in the accent colour and its stage label on the front face. Each block is three flat polygons — front, top and right side — sharing one vanishing offset, so the light is consistent across the row and the type on the front face stays crisp. The y scale is not fitted to the data: the last block runs past the top of the frame on purpose. Thin elbow callout lines point at individual stages, and the camera pushes in slowly across the whole shot.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.5134,
      "props": [
        {
          "name": "stages",
          "type": "array",
          "description": "Two to five stages, drawn left to right and revealed in order.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "push",
          "type": "number",
          "description": "How far the camera pushes in across the shot, as a fraction of scale. 0 locks it off, which the house rules discourage.",
          "required": false,
          "defaultValue": "0.08",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "stagger-distributions",
      "name": "Stagger Distributions",
      "category": "primitive",
      "categoryLabel": "Primitives",
      "summary": "Three delay distributions across one shared span, so linear, from-centre and eased staggers can be compared directly",
      "tags": [
        "primitive",
        "stagger",
        "timing",
        "documentation",
        "comparison"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/stagger-distributions.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const staggerDistributionsSchema = z.object({\n  count: z.number().min(6).max(20).default(12).describe(\"Blocks per row.\"),\n  span: z\n    .number()\n    .min(12)\n    .max(60)\n    .default(30)\n    .describe(\"Frames the whole stagger occupies. All three rows share it, so they stay comparable.\"),\n  playhead: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw a marker travelling across, so the timing relationship is visible.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof staggerDistributionsSchema>;\n\nconst ROWS = [\n  { key: \"linear\", label: \"linear\", note: \"even — reads as a sweep\" },\n  { key: \"centre\", label: \"from centre\", note: \"outward — reads as an impact\" },\n  { key: \"eased\", label: \"eased\", note: \"dense then loose — reads as a settle\" },\n] as const;\n\n/**\n * The same twelve blocks, three delay distributions, one shared span.\n *\n * Sharing the span is what makes this a comparison rather than three animations:\n * every row finishes on the same frame, so the only difference a viewer sees is\n * the shape of the delay, which is exactly the thing being taught.\n */\nconst delayFor = (kind: (typeof ROWS)[number][\"key\"], i: number, n: number, span: number) => {\n  const p = n <= 1 ? 0 : i / (n - 1);\n  if (kind === \"linear\") return p * span;\n  if (kind === \"centre\") {\n    const mid = (n - 1) / 2;\n    return (Math.abs(i - mid) / mid) * span;\n  }\n  // easeInOutCubic on the delay itself: clustered at both ends, spread in the middle.\n  const e = p < 0.5 ? 4 * p * p * p : 1 - (-2 * p + 2) ** 3 / 2;\n  return e * span;\n};\n\nconst StaggerDistributions: React.FC<Props> = ({ count, span, playhead, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n\n  const cycle = 80;\n  const local = stage.frame % cycle;\n  const trackW = width * 0.7;\n  const rowGap = height * 0.13;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        paddingLeft: width * 0.09,\n        opacity: stage.presence,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: t.fontMono,\n          fontSize: width * 0.0115,\n          letterSpacing: width * 0.0024,\n          textTransform: \"uppercase\",\n          color: t.inkMuted,\n          marginBottom: height * 0.06,\n        }}\n      >\n        Stagger distributions · {span}f span\n      </div>\n\n      <div style={{ position: \"relative\", width: trackW }}>\n        {playhead ? (\n          <div\n            style={{\n              position: \"absolute\",\n              top: -height * 0.03,\n              bottom: -height * 0.03,\n              left: (Math.min(local, span) / span) * trackW,\n              width: Math.max(1, width * 0.0011),\n              backgroundColor: t.accent,\n              opacity: local <= span ? 0.6 : 0,\n            }}\n          />\n        ) : null}\n\n        {ROWS.map((row, r) => (\n          <div key={row.key} style={{ marginBottom: r === ROWS.length - 1 ? 0 : rowGap }}>\n            <div style={{ display: \"flex\", gap: trackW * 0.012, height: height * 0.075 }}>\n              {Array.from({ length: count }, (_, i) => (\n                <Block\n                  key={i}\n                  delay={delayFor(row.key, i, count, span)}\n                  local={local}\n                  width={(trackW - trackW * 0.012 * (count - 1)) / count}\n                  tokens={t}\n                  accent={row.key === \"centre\"}\n                />\n              ))}\n            </div>\n            <div style={{ display: \"flex\", alignItems: \"baseline\", gap: width * 0.012, marginTop: height * 0.014 }}>\n              <span\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: width * 0.0105,\n                  letterSpacing: width * 0.0016,\n                  color: t.ink,\n                }}\n              >\n                {row.label}\n              </span>\n              <span style={{ fontFamily: t.fontBody, fontSize: width * 0.0098, color: t.inkMuted }}>\n                {row.note}\n              </span>\n            </div>\n          </div>\n        ))}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Block: React.FC<{\n  delay: number;\n  local: number;\n  width: number;\n  tokens: BrandTokens;\n  accent: boolean;\n}> = ({ delay, local, width, tokens, accent }) => {\n  const t = interpolate(local - delay, [0, 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  return (\n    <div\n      style={{\n        width,\n        height: \"100%\",\n        borderRadius: Math.max(2, width * 0.14),\n        backgroundColor: accent ? tokens.accent : tokens.surface,\n        border: `1px solid ${accent ? tokens.accent : tokens.rule}`,\n        opacity: t,\n        transform: `translateY(${(1 - t) * width * 0.5}px)`,\n      }}\n    />\n  );\n};\n\nexport default defineMotionItem({\n  id: \"stagger-distributions\",\n  name: \"Stagger Distributions\",\n  category: \"primitive\",\n  summary:\n    \"Three delay distributions across one shared span, so linear, from-centre and eased staggers can be compared directly\",\n  tags: [\"primitive\", \"stagger\", \"timing\", \"documentation\", \"comparison\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: staggerDistributionsSchema,\n  defaults: { count: 12, span: 30, playhead: true, brand: \"slate\" },\n  component: StaggerDistributions,\n  file: \"src/registry/items/stagger-distributions.tsx\",\n  origin: {\n    prompt:\n      \"Three rows of twelve blocks demonstrating linear, from-centre and eased stagger distributions simultaneously, with a marker travelling across so the timing relationship is visible.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Three rows of twelve blocks demonstrating linear, from-centre and eased stagger distributions simultaneously, with a marker travelling across so the timing relationship is visible.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2702,
      "props": [
        {
          "name": "count",
          "type": "number",
          "description": "Blocks per row.",
          "required": false,
          "defaultValue": "12",
          "options": null
        },
        {
          "name": "span",
          "type": "number",
          "description": "Frames the whole stagger occupies. All three rows share it, so they stay comparable.",
          "required": false,
          "defaultValue": "30",
          "options": null
        },
        {
          "name": "playhead",
          "type": "boolean",
          "description": "Draw a marker travelling across, so the timing relationship is visible.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "stat-side-panel",
      "name": "Stat Side Panel",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "A ring gauge, a heading and a list that builds one point at a time, sized as a hard-edged column beside a talking head",
      "tags": [
        "split screen",
        "panel",
        "ring",
        "gauge",
        "bullets",
        "explainer",
        "alpha"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/stat-side-panel.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\n\n/**\n * A ring, a heading and a list that builds — sized to sit beside a talking head.\n *\n * This is the split-screen panel every explainer channel uses: the footage keeps\n * the left of the frame and the panel owns a hard-edged column on the right,\n * so the cut is a wipe rather than a dissolve and the presenter is never covered.\n *\n * `fill` off renders the whole thing on a transparent ground, which matters more\n * than it sounds: with an alpha channel the panel drops straight onto a timeline\n * over the existing shot, and that is the only way a component like this is\n * usable by someone editing somewhere else. It defaults on so the component\n * stands up on its own in the catalogue.\n *\n * The bullets arrive one at a time and none of them animates again afterwards,\n * so the ring is the only thing still moving during the hold — which is the\n * whole reason the ring sweeps for as long as it does.\n */\n\nconst BUDGET = BUDGETS.feature;\n\nexport const statSidePanelSchema = z.object({\n  percent: z\n    .number()\n    .min(0)\n    .max(100)\n    .describe(\"Ring value. The number in the middle counts up to it as the arc sweeps.\"),\n  title: z.string().describe(\"The heading under the ring. Two or three words, set large.\"),\n  bullets: z\n    .array(z.string())\n    .min(1)\n    .max(6)\n    .describe(\"Points that appear one at a time under the heading. Keep each to a line or two — this column is narrow on purpose.\"),\n  side: z\n    .enum([\"right\", \"left\"])\n    .default(\"right\")\n    .describe(\"Which edge the panel occupies. The presenter takes the other one.\"),\n  widthFraction: z\n    .number()\n    .min(0.25)\n    .max(0.6)\n    .default(0.36)\n    .describe(\"Panel width as a fraction of the frame. Below about a third the bullets start wrapping badly.\"),\n  fill: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Paint the panel's ground. Turn it off to render with an alpha channel and drop the panel straight over your own footage in any editor.\",\n    ),\n  brand: z.string().default(\"ledger\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof statSidePanelSchema>;\n\nexport const StatSidePanel: React.FC<Props> = ({\n  percent,\n  title,\n  bullets,\n  side,\n  widthFraction,\n  fill,\n  brand,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const t = BRANDS[brand] ?? BRANDS.ledger;\n\n  const u = Math.min(width, height) / 1080;\n  const panelW = width * widthFraction;\n\n  // The panel wipes in from its own edge, so the seam moves with the content.\n  const wipe = interpolate(stage.enter, [0, 1], [0, 1], { easing: EASING.enter });\n\n  // The ring keeps sweeping well past the panel's arrival — it is the only thing\n  // still alive once the bullets have landed.\n  const sweep = interpolate(frame, [BUDGET.enter * 0.6, BUDGET.enter + 64], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  // The panel is locked to its own edge, so the contents drift inside it rather\n  // than the panel scaling — a hard edge that moves stops reading as a wipe.\n  const drift = Math.sin((frame / 30) * 0.45) * 5 * u;\n\n  const r = 78 * u;\n  const circ = 2 * Math.PI * r;\n  const shown = Math.round(percent * sweep);\n\n  return (\n    /*\n     * With `fill` on, the rest of the frame gets the brand surface rather than\n     * nothing — the panel is meant to sit against footage, and against nothing\n     * at all its hard edge is invisible. With `fill` off the whole frame stays\n     * transparent, which is the alpha-overlay case this exists for.\n     */\n    <AbsoluteFill style={{ backgroundColor: fill ? t.surface : \"transparent\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          top: 0,\n          bottom: 0,\n          [side]: 0,\n          width: panelW,\n          backgroundColor: fill ? t.bg : \"transparent\",\n          clipPath:\n            side === \"right\"\n              ? `inset(0 0 0 ${(1 - wipe) * 100}%)`\n              : `inset(0 ${(1 - wipe) * 100}% 0 0)`,\n          opacity: stage.presence,\n          display: \"flex\",\n          flexDirection: \"column\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          padding: `0 ${panelW * 0.09}px`,\n          gap: 0,\n          transform: `translateY(${drift}px)`,\n        } as React.CSSProperties}\n      >\n        {/* ring */}\n        <svg width={r * 2.5} height={r * 2.5} viewBox={`0 0 ${r * 2.5} ${r * 2.5}`}>\n          <circle\n            cx={r * 1.25}\n            cy={r * 1.25}\n            r={r}\n            fill=\"none\"\n            stroke={t.surface}\n            strokeWidth={20 * u}\n          />\n          <circle\n            cx={r * 1.25}\n            cy={r * 1.25}\n            r={r}\n            fill=\"none\"\n            stroke={t.accent}\n            strokeWidth={20 * u}\n            strokeLinecap=\"round\"\n            strokeDasharray={circ}\n            strokeDashoffset={circ * (1 - (percent / 100) * sweep)}\n            transform={`rotate(-90 ${r * 1.25} ${r * 1.25})`}\n          />\n          <text\n            x={r * 1.25}\n            y={r * 1.25}\n            textAnchor=\"middle\"\n            dominantBaseline=\"central\"\n            fill={t.ink}\n            fontFamily={t.fontDisplay}\n            fontWeight={500}\n            fontSize={40 * u}\n            style={{ fontVariantNumeric: \"tabular-nums\" }}\n          >\n            {shown}%\n          </text>\n        </svg>\n\n        <div\n          style={{\n            marginTop: 12 * u,\n            fontFamily: t.fontDisplay,\n            fontWeight: 500,\n            fontSize: 52 * u,\n            letterSpacing: \"-0.02em\",\n            color: t.ink,\n            textAlign: \"center\",\n          }}\n        >\n          {title}\n        </div>\n\n        {/* the chevron divider — a glow under a soft V */}\n        <svg width={112 * u} height={34 * u} viewBox=\"0 0 112 34\" style={{ marginTop: 18 * u }}>\n          <defs>\n            <linearGradient id=\"chevglow\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"0\">\n              <stop offset=\"0%\" stopColor={t.accent} stopOpacity=\"0\" />\n              <stop offset=\"50%\" stopColor={t.accent} stopOpacity=\"0.85\" />\n              <stop offset=\"100%\" stopColor={t.accent} stopOpacity=\"0\" />\n            </linearGradient>\n          </defs>\n          <path\n            d=\"M8 6 L56 26 L104 6\"\n            fill=\"none\"\n            stroke=\"url(#chevglow)\"\n            strokeWidth=\"3.5\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n        </svg>\n\n        <div\n          style={{\n            marginTop: 26 * u,\n            display: \"flex\",\n            flexDirection: \"column\",\n            gap: 26 * u,\n            width: \"100%\",\n          }}\n        >\n          {bullets.map((b, i) => (\n            <Bullet key={b + i} index={i} text={b} u={u} t={t} />\n          ))}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Bullet: React.FC<{\n  index: number;\n  text: string;\n  u: number;\n  t: (typeof BRANDS)[string];\n}> = ({ index, text, u, t }) => {\n  const st = useStaggeredStage(BUDGET, index + 2, 10);\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"flex-start\",\n        gap: 18 * u,\n        opacity: st.enter,\n        transform: `translateX(${(1 - st.enter) * 16 * u}px)`,\n      }}\n    >\n      <span\n        style={{\n          width: 13 * u,\n          height: 13 * u,\n          backgroundColor: t.accent,\n          flexShrink: 0,\n          marginTop: 12 * u,\n        }}\n      />\n      <span\n        style={{\n          fontFamily: t.fontBody,\n          fontWeight: 400,\n          fontSize: 31 * u,\n          lineHeight: 1.28,\n          color: t.ink,\n        }}\n      >\n        {text}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"stat-side-panel\",\n  name: \"Stat Side Panel\",\n  category: \"data\",\n  summary:\n    \"A ring gauge, a heading and a list that builds one point at a time, sized as a hard-edged column beside a talking head\",\n  tags: [\"split screen\", \"panel\", \"ring\", \"gauge\", \"bullets\", \"explainer\", \"alpha\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: statSidePanelSchema,\n  defaults: {\n    percent: 78,\n    title: \"Costs Increase\",\n    bullets: [\n      \"Hiring salespeople\",\n      \"Building a go-to-market engine\",\n      \"Spending money to land customers\",\n      \"Legal costs…\",\n    ],\n    side: \"right\",\n    widthFraction: 0.36,\n    fill: true,\n    brand: \"ledger\",\n  },\n  component: StatSidePanel,\n  file: \"src/registry/items/stat-side-panel.tsx\",\n  origin: {\n    prompt:\n      \"A hard-edged panel occupying one third of the frame beside a presenter: a thick ring gauge sweeping to a percentage with the number counting up inside it, a large heading, a soft glowing chevron divider, and a list of points that arrive one at a time with small square accent bullets. The panel wipes in from its own edge rather than fading. The ring keeps sweeping after the bullets have landed so something is still moving through the hold, and the ground can be switched off entirely to render the panel with an alpha channel.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A hard-edged panel occupying one third of the frame beside a presenter: a thick ring gauge sweeping to a percentage with the number counting up inside it, a large heading, a soft glowing chevron divider, and a list of points that arrive one at a time with small square accent bullets. The panel wipes in from its own edge rather than fading. The ring keeps sweeping after the bullets have landed so something is still moving through the hold, and the ground can be switched off entirely to render the panel with an alpha channel.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2879,
      "props": [
        {
          "name": "percent",
          "type": "number",
          "description": "Ring value. The number in the middle counts up to it as the arc sweeps.",
          "required": true,
          "defaultValue": "78",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "The heading under the ring. Two or three words, set large.",
          "required": true,
          "defaultValue": "Costs Increase",
          "options": null
        },
        {
          "name": "bullets",
          "type": "array",
          "description": "Points that appear one at a time under the heading. Keep each to a line or two — this column is narrow on purpose.",
          "required": true,
          "defaultValue": "4 items",
          "options": null
        },
        {
          "name": "side",
          "type": "\"right\" | \"left\"",
          "description": "Which edge the panel occupies. The presenter takes the other one.",
          "required": false,
          "defaultValue": "right",
          "options": [
            "right",
            "left"
          ]
        },
        {
          "name": "widthFraction",
          "type": "number",
          "description": "Panel width as a fraction of the frame. Below about a third the bullets start wrapping badly.",
          "required": false,
          "defaultValue": "0.36",
          "options": null
        },
        {
          "name": "fill",
          "type": "boolean",
          "description": "Paint the panel's ground. Turn it off to render with an alpha channel and drop the panel straight over your own footage in any editor.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "ledger",
          "options": null
        }
      ]
    },
    {
      "id": "sticker-hub-diagram",
      "name": "Sticker Hub Diagram",
      "category": "diagram",
      "categoryLabel": "Diagrams",
      "summary": "A hub-and-spoke diagram on graph paper where each edge wires out from the centre and lands its node as it arrives",
      "tags": [
        "diagram",
        "light",
        "paper",
        "hub and spoke",
        "sticker",
        "grid",
        "explainer"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/sticker-hub-diagram.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const stickerHubDiagramSchema = z.object({\n  hub: z\n    .string()\n    .describe(\"The centre node. A product or system name — one or two words.\"),\n  nodes: z\n    .array(z.string())\n    .min(3)\n    .max(8)\n    .describe(\"Satellite labels, placed clockwise from the top. Three to eight; past eight the ring stops reading and the connectors cross.\"),\n  topLabel: z\n    .string()\n    .optional()\n    .describe(\"Solid label bar above the diagram, set in inverted type.\"),\n  bottomLabel: z\n    .string()\n    .optional()\n    .describe(\"Label bar below the diagram, in the accent colour. Use it for the conclusion the diagram is arguing for.\"),\n  brand: z\n    .string()\n    .default(\"field\")\n    .describe(\"Brand token id — one of the keys of BRANDS. Built for the light brands (field, press, paper).\"),\n});\n\ntype Props = z.infer<typeof stickerHubDiagramSchema>;\n\nconst BUDGET = BUDGETS.scene;\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"multiply\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence\n        type=\"fractalNoise\"\n        baseFrequency=\"0.95\"\n        numOctaves={4}\n        seed={Math.floor(frame / 3) % 48}\n        stitchTiles=\"stitch\"\n      />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\n/**\n * A sticker.\n *\n * Hard offset shadow, no blur — the thing that makes these read as cut-out\n * paper stuck to a sheet rather than as UI cards. A soft shadow would say\n * \"elevation\" and put them in a browser; a hard one says \"physical object on a\n * surface\", which is the whole visual argument of this layout.\n */\nconst shadow = (ink: string, d: number) => `${d}px ${d}px 0 0 ${alpha(ink, 0.9)}`;\n\nconst StickerHubDiagram: React.FC<Props> = ({ hub, nodes, topLabel, bottomLabel, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n\n  const cx = width / 2;\n  const cy = height * 0.5;\n  const rx = width * 0.29;\n  // Tightened so the lowest node clears the bottom label bar.\n  const ry = height * 0.255;\n\n  const cell = width * 0.0125;\n  const rule = Math.max(1, width * 0.0013);\n  const gridIn = interpolate(frame, [0, 22], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const hubIn = interpolate(frame, [6, 26], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const topIn = interpolate(frame, [2, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const botIn = interpolate(frame, [58, 78], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1.02, 1]);\n\n  const placed = nodes.map((label, i) => {\n    // Clockwise from the top, so reading order matches the order given.\n    const a = -Math.PI / 2 + (i / nodes.length) * Math.PI * 2;\n    return { label, x: cx + Math.cos(a) * rx, y: cy + Math.sin(a) * ry, i };\n  });\n\n  const nodeW = width * 0.125;\n  const nodeH = height * 0.115;\n  const hubW = width * 0.115;\n  const hubH = height * 0.085;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"50% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            opacity: gridIn * 0.85,\n            /* Weight derived from the frame — a 1px rule vanishes entirely once\n               the preview is downscaled, taking the graph paper with it. */\n            backgroundImage: `\n              linear-gradient(${alpha(t.ink, 0.26)} ${rule}px, transparent ${rule}px),\n              linear-gradient(90deg, ${alpha(t.ink, 0.26)} ${rule}px, transparent ${rule}px),\n              linear-gradient(${alpha(t.ink, 0.13)} ${rule}px, transparent ${rule}px),\n              linear-gradient(90deg, ${alpha(t.ink, 0.13)} ${rule}px, transparent ${rule}px)`,\n            backgroundSize: `${cell * 5}px ${cell * 5}px, ${cell * 5}px ${cell * 5}px, ${cell}px ${cell}px, ${cell}px ${cell}px`,\n          }}\n        />\n        {/* The print light is deliberately weak. At full strength it washed the\n            rule off the paper entirely, which defeated the point of drawing on\n            graph paper at all. */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: `radial-gradient(100% 78% at 26% 6%, ${alpha(\"#fff\", 0.22)} 0%, ${alpha(\n              \"#fff\",\n              0.06,\n            )} 36%, ${alpha(t.ink, 0.02)} 70%, ${alpha(t.ink, 0.08)} 100%)`,\n          }}\n        />\n\n        {/* ---------------------------------------------------- the connectors */}\n        <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }} aria-hidden>\n          {placed.map((p) => {\n            /**\n             * Each edge draws out from the hub toward its node, and the node it\n             * feeds only lands once the line reaches it. The diagram therefore\n             * builds as a circuit being wired rather than as eight cards fading\n             * up — the order carries the meaning.\n             */\n            const start = 20 + p.i * 4;\n            const grow = interpolate(frame, [start, start + 14], [0, 1], {\n              extrapolateLeft: \"clamp\",\n              extrapolateRight: \"clamp\",\n              easing: EASING.travel,\n            });\n            const ex = cx + (p.x - cx) * grow;\n            const ey = cy + (p.y - cy) * grow;\n            return (\n              <line\n                key={p.label}\n                x1={cx}\n                y1={cy}\n                x2={ex}\n                y2={ey}\n                stroke={t.accent}\n                strokeWidth={Math.max(2, width * 0.0016)}\n                strokeLinecap=\"round\"\n                opacity={0.85}\n              />\n            );\n          })}\n        </svg>\n\n        {/* --------------------------------------------------------- the nodes */}\n        {placed.map((p) => {\n          const start = 20 + p.i * 4 + 11;\n          const on = interpolate(frame, [start, start + 12], [0, 1], {\n            extrapolateLeft: \"clamp\",\n            extrapolateRight: \"clamp\",\n            easing: EASING.enter,\n          });\n          const tilt = ((p.i % 3) - 1) * 1.1;\n          return (\n            <div\n              key={p.label}\n              style={{\n                position: \"absolute\",\n                left: p.x - nodeW / 2,\n                top: p.y - nodeH / 2,\n                width: nodeW,\n                height: nodeH,\n                backgroundColor: t.surface,\n                border: `${Math.max(2, width * 0.0016)}px solid ${t.ink}`,\n                boxShadow: shadow(t.ink, Math.max(3, width * 0.0026)),\n                padding: nodeW * 0.075,\n                opacity: on,\n                transform: `rotate(${tilt}deg) scale(${interpolate(on, [0, 1], [0.9, 1])})`,\n              }}\n            >\n              <div\n                style={{\n                  fontFamily: t.fontMono,\n                  fontSize: nodeW * 0.085,\n                  fontWeight: 600,\n                  letterSpacing: nodeW * 0.006,\n                  textTransform: \"uppercase\",\n                  color: t.ink,\n                }}\n              >\n                {p.label}\n              </div>\n              <div style={{ height: 1, backgroundColor: alpha(t.ink, 0.75), margin: `${nodeH * 0.07}px 0` }} />\n              {[0.86, 0.66, 0.44].map((w, k) => (\n                <div\n                  key={k}\n                  style={{\n                    height: Math.max(1, nodeH * 0.035),\n                    width: `${w * 100 * on}%`,\n                    marginBottom: nodeH * 0.055,\n                    backgroundColor: alpha(t.ink, 0.28),\n                  }}\n                />\n              ))}\n            </div>\n          );\n        })}\n\n        {/* ----------------------------------------------------------- the hub */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: cx - hubW / 2,\n            top: cy - hubH / 2,\n            width: hubW,\n            height: hubH,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            backgroundColor: t.surface,\n            border: `${Math.max(3, width * 0.0021)}px solid ${t.ink}`,\n            boxShadow: shadow(t.ink, Math.max(4, width * 0.0034)),\n            opacity: hubIn,\n            transform: `scale(${interpolate(hubIn, [0, 1], [0.86, 1])})`,\n          }}\n        >\n          <span\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: hubW * 0.19,\n              fontWeight: 700,\n              letterSpacing: hubW * -0.004,\n              color: t.ink,\n            }}\n          >\n            {hub}\n          </span>\n        </div>\n\n        {/* -------------------------------------------------------- the labels */}\n        {topLabel ? (\n          <LabelBar\n            text={topLabel}\n            bg={t.ink}\n            fg={t.bg}\n            font={t.fontDisplay}\n            width={width}\n            top={height * 0.075}\n            reveal={topIn}\n            ink={t.ink}\n          />\n        ) : null}\n        {bottomLabel ? (\n          <LabelBar\n            text={bottomLabel}\n            bg={t.accent}\n            fg={t.accentInk}\n            font={t.fontDisplay}\n            width={width}\n            top={height * 0.855}\n            reveal={botIn}\n            ink={t.ink}\n          />\n        ) : null}\n      </div>\n\n      <Grain id=\"sticker-hub-grain\" frame={frame} opacity={0.15} />\n    </AbsoluteFill>\n  );\n};\n\n/** A solid label bar, wiped on from the left like a printed block. */\nconst LabelBar: React.FC<{\n  text: string;\n  bg: string;\n  fg: string;\n  font: string;\n  width: number;\n  top: number;\n  reveal: number;\n  ink: string;\n}> = ({ text, bg, fg, font, width, top, reveal, ink }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      left: 0,\n      right: 0,\n      top,\n      display: \"flex\",\n      justifyContent: \"center\",\n    }}\n  >\n    <div\n      style={{\n        overflow: \"hidden\",\n        backgroundColor: bg,\n        boxShadow: `${Math.max(3, width * 0.0026)}px ${Math.max(3, width * 0.0026)}px 0 0 ${alpha(ink, 0.9)}`,\n        padding: `${width * 0.008}px ${width * 0.022}px`,\n        transform: `scaleX(${reveal})`,\n        transformOrigin: \"left center\",\n      }}\n    >\n      <span\n        style={{\n          display: \"block\",\n          fontFamily: font,\n          fontSize: width * 0.024,\n          fontWeight: 700,\n          letterSpacing: width * 0.0028,\n          textTransform: \"uppercase\",\n          color: fg,\n          whiteSpace: \"nowrap\",\n          // Counter-scale so the type is not stretched by the bar's wipe.\n          transform: `scaleX(${1 / Math.max(0.02, reveal)})`,\n          transformOrigin: \"left center\",\n          opacity: reveal > 0.55 ? 1 : 0,\n        }}\n      >\n        {text}\n      </span>\n    </div>\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"sticker-hub-diagram\",\n  name: \"Sticker Hub Diagram\",\n  category: \"diagram\",\n  summary:\n    \"A hub-and-spoke diagram on graph paper where each edge wires out from the centre and lands its node as it arrives\",\n  tags: [\"diagram\", \"light\", \"paper\", \"hub and spoke\", \"sticker\", \"grid\", \"explainer\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: stickerHubDiagramSchema,\n  defaults: {\n    hub: \"OpenAI\",\n    nodes: [\"Data\", \"Models\", \"Chat\", \"Vision\", \"Agents\", \"Tools\", \"Memory\", \"Reasoning\"],\n    topLabel: \"Unified superapp\",\n    bottomLabel: \"Agent-first ecosystem\",\n    brand: \"field\",\n  },\n  component: StickerHubDiagram,\n  file: \"src/registry/items/sticker-hub-diagram.tsx\",\n  origin: {\n    prompt:\n      \"A hub-and-spoke explainer drawn on cream graph paper. The boxes should look like cut-out stickers pressed onto the sheet — white fill, heavy black rule, hard offset shadow with no blur. Wire the diagram rather than fading it in: each connector draws out from the centre and its box only lands when the line reaches it. Solid label bars top and bottom, wiping on from the left.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A hub-and-spoke explainer drawn on cream graph paper. The boxes should look like cut-out stickers pressed onto the sheet — white fill, heavy black rule, hard offset shadow with no blur. Wire the diagram rather than fading it in: each connector draws out from the centre and its box only lands when the line reaches it. Solid label bars top and bottom, wiping on from the left.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.728,
      "props": [
        {
          "name": "hub",
          "type": "string",
          "description": "The centre node. A product or system name — one or two words.",
          "required": true,
          "defaultValue": "OpenAI",
          "options": null
        },
        {
          "name": "nodes",
          "type": "array",
          "description": "Satellite labels, placed clockwise from the top. Three to eight; past eight the ring stops reading and the connectors cross.",
          "required": true,
          "defaultValue": "8 items",
          "options": null
        },
        {
          "name": "topLabel",
          "type": "string",
          "description": "Solid label bar above the diagram, set in inverted type.",
          "required": false,
          "defaultValue": "Unified superapp",
          "options": null
        },
        {
          "name": "bottomLabel",
          "type": "string",
          "description": "Label bar below the diagram, in the accent colour. Use it for the conclusion the diagram is arguing for.",
          "required": false,
          "defaultValue": "Agent-first ecosystem",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS. Built for the light brands (field, press, paper).",
          "required": false,
          "defaultValue": "field",
          "options": null
        }
      ]
    },
    {
      "id": "telemetry-ascent",
      "name": "Telemetry Ascent",
      "category": "data",
      "categoryLabel": "Data",
      "summary": "A flight-telemetry gauge where the altitude scale scrolls past a fixed readout while the vehicle holds frame",
      "tags": [
        "telemetry",
        "technical",
        "vertical",
        "gauge",
        "axis",
        "hud",
        "counter"
      ],
      "aspect": "9:16",
      "width": 1080,
      "height": 1920,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/telemetry-ascent.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\n\nconst rgb = (hex: string) => {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = Number.parseInt(full, 16);\n  return `${(n >> 16) & 255},${(n >> 8) & 255},${n & 255}`;\n};\nconst alpha = (hex: string, a: number) => `rgba(${rgb(hex)},${a})`;\n\nexport const telemetryAscentSchema = z.object({\n  from: z\n    .number()\n    .default(37500)\n    .describe(\"Altitude at the start of the shot, in the unit named by axisUnit.\"),\n  to: z\n    .number()\n    .default(41000)\n    .describe(\"Altitude at the end. The axis scrolls to cover exactly this range, so the span sets the tick density.\"),\n  step: z\n    .number()\n    .default(500)\n    .describe(\"Distance between axis ticks. Aim for six to nine ticks across the range.\"),\n  axisLabel: z\n    .string()\n    .default(\"ALT\")\n    .describe(\"Short axis name, printed in the accent colour beside the live tick.\"),\n  axisUnit: z\n    .string()\n    .default(\"FT\")\n    .describe(\"Unit printed under the axis label.\"),\n  caption: z\n    .string()\n    .optional()\n    .describe(\"Line under the readout. Keep it to a few words — this frame is a gauge, not a slide.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof telemetryAscentSchema>;\n\nconst BUDGET = BUDGETS.scene;\nconst SEED = \"telemetry-ascent\";\n\nconst Grain: React.FC<{ id: string; frame: number; opacity: number }> = ({ id, frame, opacity }) => (\n  <svg\n    width=\"100%\"\n    height=\"100%\"\n    style={{ position: \"absolute\", inset: 0, opacity, mixBlendMode: \"overlay\", pointerEvents: \"none\" }}\n    aria-hidden\n  >\n    <filter id={id} x=\"0%\" y=\"0%\" width=\"100%\" height=\"100%\">\n      <feTurbulence type=\"fractalNoise\" baseFrequency=\"0.88\" numOctaves={3} seed={Math.floor(frame / 2) % 64} stitchTiles=\"stitch\" />\n      <feColorMatrix type=\"saturate\" values=\"0\" />\n    </filter>\n    <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n  </svg>\n);\n\nconst TelemetryAscent: React.FC<Props> = ({ from, to, step, axisLabel, axisUnit, caption, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height, fps } = useVideoConfig();\n  const frame = useCurrentFrame();\n  const stage = useStage(BUDGET);\n  const time = frame / fps;\n\n  /**\n   * The climb.\n   *\n   * The vehicle stays put and the WORLD moves — the axis scrolls down past a\n   * fixed readout line. That is how flight telemetry is actually shot and it is\n   * the only way this works vertically: an object that physically rises leaves\n   * the frame in a second, whereas a scrolling scale can climb for as long as\n   * the shot needs.\n   */\n  const climb = interpolate(frame, [10, BUDGET.enter + BUDGET.hold], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const altitude = from + (to - from) * climb;\n\n  const readY = height * 0.52;\n  const pxPerUnit = height / ((to - from) * 1.9);\n\n  const firstTick = Math.floor(from / step) * step;\n  const lastTick = Math.ceil(to / step) * step + step * 2;\n  const ticks: number[] = [];\n  for (let v = firstTick - step * 2; v <= lastTick; v += step) ticks.push(v);\n\n  const axisIn = interpolate(frame, [0, 20], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const craftIn = interpolate(frame, [6, 30], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const capIn = interpolate(frame, [40, 60], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const push = interpolate(stage.progress, [0, 1], [1.03, 1]);\n  const bodyW = width * 0.075;\n  const bodyH = height * 0.26;\n  const cx = width * 0.62;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          background: `radial-gradient(80% 46% at 56% 44%, ${alpha(t.surface, 0.85)} 0%, ${alpha(\n            t.surface,\n            0.25,\n          )} 36%, ${alpha(t.bg, 0.92)} 70%, ${alpha(t.bg, 1)} 100%)`,\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `scale(${push})`,\n          transformOrigin: \"56% 50%\",\n          opacity: stage.presence,\n        }}\n      >\n        {/* the reference grid, scrolling with the climb */}\n        <div\n          style={{\n            position: \"absolute\",\n            inset: `-${height * 0.2}px`,\n            opacity: 0.5 * axisIn,\n            backgroundImage: `\n              linear-gradient(${alpha(t.accentAlt, 0.09)} 1px, transparent 1px),\n              linear-gradient(90deg, ${alpha(t.accentAlt, 0.09)} 1px, transparent 1px)`,\n            backgroundSize: `${width * 0.06}px ${width * 0.06}px`,\n            backgroundPosition: `0 ${((altitude - from) * pxPerUnit) % (width * 0.06)}px`,\n          }}\n        />\n\n        {/* trajectory arcs */}\n        <svg width={width} height={height} style={{ position: \"absolute\", inset: 0 }} aria-hidden>\n          {[-1, 1].map((dir) => (\n            <path\n              key={dir}\n              /* Offsets are fractions of the SHORT edge, so both arcs stay in\n                 frame at 9:16 — at 0.62 the right-hand one was drawn entirely\n                 outside the composition and only ever rendered as one arc. */\n              d={`M ${cx + dir * width * 0.4} ${-height * 0.05}\n                  Q ${cx + dir * width * 0.13} ${height * 0.5}\n                    ${cx + dir * width * 0.43} ${height * 1.05}`}\n              fill=\"none\"\n              stroke={alpha(t.accentAlt, 0.5)}\n              strokeWidth={Math.max(2, width * 0.0035)}\n              opacity={axisIn}\n            />\n          ))}\n        </svg>\n\n        {/* ------------------------------------------------------- the axis */}\n        {/* The scale is clipped above the readout. Run full-height it scrolls its\n            own tick labels straight into the caption, and a gauge whose numbers\n            collide with its own readout is worse than no gauge. */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: width * 0.11,\n            top: 0,\n            height: height * 0.74,\n            width: width * 0.3,\n            overflow: \"hidden\",\n          }}\n        >\n          {ticks.map((v) => {\n            const y = readY + (altitude - v) * pxPerUnit;\n            if (y < -height * 0.1 || y > height * 1.1) return null;\n            const live = Math.abs(v - altitude) < step * 0.55;\n            return (\n              <div key={v} style={{ position: \"absolute\", left: 0, top: y, display: \"flex\", alignItems: \"center\", gap: width * 0.022, opacity: axisIn }}>\n                <div\n                  style={{\n                    width: live ? width * 0.055 : width * 0.03,\n                    height: Math.max(2, width * 0.004),\n                    backgroundColor: live ? t.accentAlt : alpha(t.ink, 0.32),\n                  }}\n                />\n                <span\n                  style={{\n                    fontFamily: t.fontMono,\n                    fontSize: width * 0.032,\n                    fontWeight: 500,\n                    letterSpacing: width * 0.001,\n                    color: live ? t.ink : alpha(t.ink, 0.42),\n                  }}\n                >\n                  {Math.round(v).toLocaleString(\"en-US\")}\n                </span>\n              </div>\n            );\n          })}\n\n          {/* Column header rather than a floating label beside the live tick,\n              where it collided with whichever number happened to be live. */}\n          <div style={{ position: \"absolute\", left: 0, top: height * 0.1, opacity: axisIn }}>\n            <div style={{ fontFamily: t.fontMono, fontSize: width * 0.028, fontWeight: 700, letterSpacing: width * 0.004, color: t.accentAlt }}>\n              {axisLabel} ({axisUnit})\n            </div>\n            <div style={{ marginTop: width * 0.012, width: width * 0.09, height: Math.max(2, width * 0.004), backgroundColor: alpha(t.accentAlt, 0.5) }} />\n          </div>\n        </div>\n\n        {/* ------------------------------------------------------ the vehicle */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: cx - bodyW / 2,\n            top: readY - bodyH * 0.62,\n            width: bodyW,\n            opacity: craftIn,\n            // Only a breath of vertical float. The scale is doing the climbing.\n            transform: `translateY(${Math.sin(time * 1.1) * height * 0.004 + interpolate(craftIn, [0, 1], [height * 0.06, 0])}px)`,\n          }}\n        >\n          <svg width={bodyW} height={bodyH} viewBox=\"0 0 40 240\" style={{ display: \"block\", overflow: \"visible\" }} aria-hidden>\n            <path d=\"M20 0 C31 34 34 62 34 96 L34 210 L6 210 L6 96 C6 62 9 34 20 0 Z\" fill={t.ink} opacity={0.96} />\n            <path d=\"M20 0 C26 34 27 62 27 96 L27 210 L20 210 Z\" fill={alpha(\"#fff\", 0.35)} />\n            <path d=\"M6 210 L34 210 L30 232 L10 232 Z\" fill={alpha(t.ink, 0.55)} />\n          </svg>\n\n          {/* Plume. Flicker driven by a seeded hash of the frame, so it is\n              lively but identical on every render. */}\n          <div\n            style={{\n              position: \"absolute\",\n              left: \"50%\",\n              top: bodyH * 0.97,\n              width: bodyW * 0.7,\n              height: bodyH * (0.34 + random(`${SEED}-${Math.floor(frame / 2)}`) * 0.22),\n              marginLeft: -bodyW * 0.35,\n              borderRadius: \"50% 50% 46% 46%\",\n              background: `linear-gradient(180deg, ${alpha(\"#fff\", 0.95)} 0%, ${alpha(\n                t.accentAlt,\n                0.85,\n              )} 24%, ${alpha(t.accent, 0.55)} 58%, ${alpha(t.accent, 0)} 100%)`,\n              filter: `blur(${width * 0.008}px)`,\n              opacity: craftIn,\n            }}\n          />\n        </div>\n\n        {/* ---------------------------------------------------- the readout\n         * Moved to the foot of the frame. Sat beside the vehicle it printed\n         * straight through it — the one collision a vertical layout guarantees\n         * if the number and the subject share a band. */}\n        <div style={{ position: \"absolute\", left: 0, right: 0, top: height * 0.79, textAlign: \"center\", opacity: axisIn }}>\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: width * 0.135,\n              fontWeight: 700,\n              lineHeight: 1,\n              letterSpacing: width * -0.004,\n              color: t.ink,\n              fontVariantNumeric: \"tabular-nums\",\n            }}\n          >\n            {Math.round(altitude).toLocaleString(\"en-US\")}\n          </div>\n          <div style={{ marginTop: width * 0.012, fontFamily: t.fontMono, fontSize: width * 0.03, letterSpacing: width * 0.004, color: t.accentAlt }}>\n            {axisUnit}\n          </div>\n          {caption ? (\n            <div\n              style={{\n                marginTop: width * 0.03,\n                maxWidth: width * 0.62,\n                marginLeft: \"auto\",\n                marginRight: \"auto\",\n                fontFamily: t.fontBody,\n                fontSize: width * 0.031,\n                lineHeight: 1.4,\n                color: t.inkMuted,\n                opacity: capIn,\n              }}\n            >\n              {caption}\n            </div>\n          ) : null}\n        </div>\n      </div>\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          pointerEvents: \"none\",\n          background: `radial-gradient(112% 74% at 52% 46%, ${alpha(\"#000\", 0)} 32%, ${alpha(\"#000\", 0.16)} 62%, ${alpha(\n            \"#000\",\n            0.5,\n          )} 100%)`,\n        }}\n      />\n      <Grain id=\"telemetry-ascent-grain\" frame={frame} opacity={0.08} />\n    </AbsoluteFill>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"telemetry-ascent\",\n  name: \"Telemetry Ascent\",\n  category: \"data\",\n  summary:\n    \"A flight-telemetry gauge where the altitude scale scrolls past a fixed readout while the vehicle holds frame\",\n  tags: [\"telemetry\", \"technical\", \"vertical\", \"gauge\", \"axis\", \"hud\", \"counter\"],\n  budget: BUDGETS.scene,\n  aspect: \"9:16\",\n  schema: telemetryAscentSchema,\n  defaults: {\n    from: 37500,\n    to: 41000,\n    step: 500,\n    axisLabel: \"ALT\",\n    axisUnit: \"FT\",\n    caption: \"Nominal. Second stage on profile.\",\n    brand: \"circuit\",\n  },\n  component: TelemetryAscent,\n  file: \"src/registry/items/telemetry-ascent.tsx\",\n  origin: {\n    prompt:\n      \"A vertical telemetry frame in the style of a launch broadcast: a navy grid, an altitude scale down the left with tick marks and monospace labels, and a vehicle silhouette with an exhaust plume. Keep the vehicle fixed and scroll the scale past it so it can climb for the whole shot, mark the live tick in the accent colour, and count a large readout up alongside. Two trajectory arcs sweeping through the frame.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A vertical telemetry frame in the style of a launch broadcast: a navy grid, an altitude scale down the left with tick marks and monospace labels, and a vehicle silhouette with an exhaust plume. Keep the vehicle fixed and scroll the scale past it so it can climb for the whole shot, mark the live tick in the accent colour, and count a large readout up alongside. Two trajectory arcs sweeping through the frame.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.7662,
      "props": [
        {
          "name": "from",
          "type": "number",
          "description": "Altitude at the start of the shot, in the unit named by axisUnit.",
          "required": false,
          "defaultValue": "37500",
          "options": null
        },
        {
          "name": "to",
          "type": "number",
          "description": "Altitude at the end. The axis scrolls to cover exactly this range, so the span sets the tick density.",
          "required": false,
          "defaultValue": "41000",
          "options": null
        },
        {
          "name": "step",
          "type": "number",
          "description": "Distance between axis ticks. Aim for six to nine ticks across the range.",
          "required": false,
          "defaultValue": "500",
          "options": null
        },
        {
          "name": "axisLabel",
          "type": "string",
          "description": "Short axis name, printed in the accent colour beside the live tick.",
          "required": false,
          "defaultValue": "ALT",
          "options": null
        },
        {
          "name": "axisUnit",
          "type": "string",
          "description": "Unit printed under the axis label.",
          "required": false,
          "defaultValue": "FT",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Line under the readout. Keep it to a few words — this frame is a gauge, not a slide.",
          "required": false,
          "defaultValue": "Nominal. Second stage on profile.",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "terminal-window",
      "name": "Terminal Window",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A terminal runs a command: it types in, then the log streams line by line with a cycling spinner and an accent success line",
      "tags": [
        "terminal",
        "cli",
        "shell",
        "typing",
        "spinner",
        "product",
        "developer tooling"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/terminal-window.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nconst BUDGET = BUDGETS.feature;\n\n/* -------------------------------------------------------------------- timing */\n\n/**\n * Typing starts while the window is still arriving. The session was already open\n * when we cut to it, which is why the prompt glyph and the caret are on screen\n * from frame 0 rather than being drawn in.\n */\nconst TYPE_START = Math.round(BUDGET.enter * 0.7);\n/** Base frames per keystroke before jitter: ~17 characters a second. */\nconst KEY_BASE = 1.7;\n/** The return key. Also the beat between the last log line and the prompt coming back. */\nconst RUN_GAP = 9;\n/**\n * How long each kind of line owns the bottom of the log before the next one lands.\n * Dim detail lines flush fast because they are consequences, not events; a spinner\n * has to spin long enough to read as work.\n */\nconst GAP = { out: 8, dim: 6, ok: 10, spinner: 26 } as const;\n/** Frames per spinner step. 4 → 7.5 steps a second: lively without strobing. */\nconst SPINNER_TICK = 4;\n/**\n * The classic four-frame line spinner, advanced by frame index — no timers, no\n * randomness, the same take on every render. Deliberately ASCII: `../fonts`\n * loads Geist Mono's `latin` subset, so braille and dingbat glyphs would fall\n * back to whatever mono the render host happens to have, or to nothing at all.\n */\nconst SPINNER_FRAMES = [\"|\", \"/\", \"-\", \"\\\\\"];\n/**\n * Command, log and returning prompt must all have landed by here, so the tail of\n * `hold` is a finished still with nothing moving but the caret.\n */\nconst DEADLINE = BUDGET.enter + Math.round(BUDGET.hold * 0.82);\n\n/* ------------------------------------------------------------------- metrics */\n\n/** Geist Mono's advance width in em, with a little safety. Drives the column fit. */\nconst CHAR_EM = 0.62;\n/** Terminal line height. Tight — a log is a block of text, not a paragraph. */\nconst ROW_EM = 1.5;\n/** Glyph + space: every line, glyph or not, starts on the same column. */\nconst GUTTER_COLS = 2;\nconst PAD_X_EM = 2.1;\nconst PAD_Y_EM = 1.7;\nconst TITLE_EM = 3;\n\n/* -------------------------------------------------------------------- schema */\n\nexport const terminalOutputLineSchema = z.object({\n  text: z\n    .string()\n    .describe(\n      \"The line exactly as the shell would print it. Leading spaces are preserved, so you can indent by hand if you want to.\",\n    ),\n  tone: z\n    .enum([\"out\", \"dim\", \"ok\", \"spinner\"])\n    .default(\"out\")\n    .describe(\n      \"'out' is normal output. 'dim' is a secondary consequence line, set in muted ink — use it for most detail. 'spinner' holds the line for ~0.9s with a line spinner cycling in its gutter, then resolves to a check once the next line lands. 'ok' is the one accent-coloured success line — use it once, on the last line.\",\n    ),\n});\n\nexport const terminalWindowSchema = z.object({\n  command: z\n    .string()\n    .default(\"npx remotion render hero out/hero.mp4\")\n    .describe(\n      \"The command that types in after the prompt glyph. Keep it to one line — 30–45 characters reads best at 16:9; longer shrinks the type for every line in the window.\",\n    ),\n  output: z\n    .array(terminalOutputLineSchema)\n    .min(1)\n    .max(9)\n    .default([\n      { text: \"bundling the composition\", tone: \"spinner\" },\n      { text: \"webpack compiled in 1.9s\", tone: \"dim\" },\n      { text: \"rendering 240 frames at 1920×1080\", tone: \"spinner\" },\n      { text: \"concurrency 8 · h264 · crf 18\", tone: \"dim\" },\n      { text: \"frames 240/240 · 34 fps\", tone: \"dim\" },\n      { text: \"out/hero.mp4 — 8.0s · 4.2 MB\", tone: \"ok\" },\n    ])\n    .describe(\n      \"The log, in order. One to nine lines; the window reserves all of their space from frame 0, so it never reflows as they land. Mix tones for hierarchy — spinners for the two or three real steps, dim for detail, one 'ok' at the end.\",\n    ),\n  title: z\n    .string()\n    .default(\"~/motionstash — zsh\")\n    .describe(\"Mono title centred in the window chrome. A path and a shell name is the convention. Empty string for bare chrome.\"),\n  prompt: z\n    .string()\n    .default(\"$\")\n    .describe(\"Prompt glyph in the gutter of the command line and of the prompt left behind at the end. '$', '%' and '›' all work.\"),\n  eyebrow: z\n    .string()\n    .default(\"Motionstash\")\n    .describe(\"Small uppercase mono label above the window, preceded by an accent dash. Empty string for none.\"),\n  caption: z\n    .string()\n    .default(\"Deterministic render — the same frames on every machine\")\n    .describe(\"Dim mono note under the window, aligned to its right edge, fading in only once the prompt returns. Empty string for none.\"),\n  caretBlink: z\n    .number()\n    .int()\n    .min(8)\n    .max(60)\n    .default(20)\n    .describe(\n      \"Frames in one full caret blink. 20 is ~1.5 blinks a second at 30fps. The caret holds solid while keys are landing and only blinks when the shell is waiting.\",\n    ),\n  seed: z\n    .string()\n    .default(\"terminal-01\")\n    .describe(\n      \"Seeds the per-keystroke jitter that keeps the typing off the metronome. Change it for a differently-fumbled take of identical duration; the same seed always renders identical frames.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof terminalWindowSchema>;\ntype OutputLine = z.infer<typeof terminalOutputLineSchema>;\ntype Tone = OutputLine[\"tone\"];\n\n/* ------------------------------------------------------------------ schedule */\n\ntype Keystroke = { char: string; at: number };\n/** A log line with the absolute frame it lands on, and the frame it stops being the newest line. */\ntype LoggedLine = { text: string; tone: Tone; at: number; until: number };\n\ntype Schedule = {\n  keys: Keystroke[];\n  firstKeyAt: number;\n  lines: LoggedLine[];\n  promptAt: number;\n};\n\n/**\n * The whole session, resolved to absolute frames before anything renders.\n *\n * Two things matter here. First, the keystroke interval jitters — a constant\n * interval reads as a machine typing, and this is meant to read as someone\n * running a command. Second, the schedule is auto-fitted: props are arbitrary but\n * the budget is not, so if a long command and nine lines would overrun the\n * deadline, the entire session is compressed uniformly. It keeps its rhythm and\n * just runs faster, rather than being truncated mid-render.\n */\nconst buildSchedule = (command: string, output: OutputLine[], seed: string): Schedule => {\n  let cursor = TYPE_START;\n  const rawKeys = Array.from(command).map((char, i) => {\n    const jitter = 0.6 + random(`${seed}/key/${i}`) * 0.8;\n    // A space is a word boundary, and hands pause on word boundaries.\n    cursor += KEY_BASE * jitter * (char === \" \" ? 1.6 : 1);\n    return { char, at: cursor };\n  });\n  const rawCommandEnd = rawKeys.length > 0 ? rawKeys[rawKeys.length - 1].at : TYPE_START;\n\n  let logCursor = rawCommandEnd + RUN_GAP;\n  const rawLines = output.map((line) => {\n    const at = logCursor;\n    logCursor += GAP[line.tone];\n    return { text: line.text, tone: line.tone, at };\n  });\n  const rawPromptAt = logCursor + RUN_GAP;\n\n  const span = rawPromptAt - TYPE_START;\n  const room = DEADLINE - TYPE_START;\n  const squeeze = span > room ? room / span : 1;\n  const fit = (at: number) => TYPE_START + Math.round((at - TYPE_START) * squeeze);\n\n  const keys = rawKeys.map((key) => ({ char: key.char, at: fit(key.at) }));\n  const promptAt = fit(rawPromptAt);\n  const lines = rawLines.map((line, i) => ({\n    ...line,\n    at: fit(line.at),\n    // A spinner spins until the next line supersedes it — then it resolves.\n    until: i + 1 < rawLines.length ? fit(rawLines[i + 1].at) : promptAt,\n  }));\n\n  return {\n    keys,\n    firstKeyAt: keys.length > 0 ? keys[0].at : TYPE_START,\n    lines,\n    promptAt,\n  };\n};\n\n/** One mono style, used everywhere, so the column grid actually holds. */\nconst mono = (tokens: BrandTokens, size: number): React.CSSProperties => ({\n  fontFamily: tokens.fontMono,\n  fontSize: size,\n  // 500 and 600 are the only Geist Mono weights `../fonts` loads; asking for 400\n  // would get a synthesised face and a different advance width, which would put\n  // the column grid out by a fraction of a character on every line.\n  fontWeight: 500,\n  lineHeight: 1,\n  letterSpacing: 0,\n  whiteSpace: \"pre\",\n});\n\n/** Fade in, fade out, hold — a caret that crossfades instead of hard-cutting. */\nconst blink = (frame: number, from: number, cycle: number) => {\n  if (frame < from) return 0;\n  const phase = (frame - from) % cycle;\n  return interpolate(\n    phase,\n    [0, cycle * 0.44, cycle * 0.52, cycle * 0.94, cycle],\n    [1, 1, 0.05, 0.05, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n};\n\n/* ----------------------------------------------------------------- component */\n\nconst TerminalWindow: React.FC<Props> = ({\n  command,\n  output,\n  title,\n  prompt,\n  eyebrow,\n  caption,\n  caretBlink,\n  seed,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const frame = stage.frame;\n  const schedule = buildSchedule(command, output, seed);\n\n  const portrait = height > width;\n  const winW = width * (portrait ? 0.9 : 0.68);\n\n  // The window is a character grid, so the type scale is a fit problem: the\n  // largest size at which the widest line still clears the padding AND the whole\n  // window still clears the frame, capped so a three-word command doesn't render\n  // as a poster. Everything below is expressed in that one size.\n  const cols = Math.max(\n    Array.from(command).length + Math.max(prompt.length, 1) + 2,\n    ...output.map((line) => Array.from(line.text).length + GUTTER_COLS),\n    28,\n  );\n  const rows = output.length + 2; // the command, the log, the prompt left behind\n  const fitToWidth = winW / (2 * PAD_X_EM + cols * CHAR_EM);\n  const fitToHeight =\n    (height * (portrait ? 0.58 : 0.62)) / (TITLE_EM + 2 * PAD_Y_EM + rows * ROW_EM);\n  // The cap is per-orientation: a 9:16 cut has far fewer horizontal pixels to\n  // spend on the same number of columns, so it is allowed a bigger relative size.\n  const cap = width * (portrait ? 0.032 : 0.0205);\n  const size = Math.max(width * 0.0075, Math.min(fitToWidth, fitToHeight, cap));\n\n  const charW = size * CHAR_EM;\n  const rowH = size * ROW_EM;\n  const padX = size * PAD_X_EM;\n  const padY = size * PAD_Y_EM;\n  const gutter = charW * GUTTER_COLS;\n  /** ~4px at 1920×1080, and the same optical distance in every other aspect. */\n  const rise = Math.max(2, size * 0.11);\n  // Token radius is authored for UI at ~16px type. Scale it with the type so a\n  // 10px corner still looks like a 10px corner on a 1300px-wide window. 0 stays 0.\n  const radius = t.radius * (size / 16);\n\n  // The command line's caret: blinks while the shell waits, goes solid the moment\n  // keys start landing — the way a real one does — and is gone once the command\n  // has been sent and the log owns the window.\n  const handoff = schedule.lines.length > 0 ? schedule.lines[0].at : schedule.promptAt;\n  const commandCaret =\n    frame >= handoff ? 0 : frame < schedule.firstKeyAt ? blink(frame, 0, caretBlink) : 1;\n\n  const captionIn = interpolate(frame, [schedule.promptAt, schedule.promptAt + 14], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"flex-start\",\n        paddingLeft: width * (portrait ? 0.05 : 0.085),\n      }}\n    >\n      {/* One presence container: the window, its label and its caption arrive and\n          leave as a single object, and every internal animation inherits this\n          opacity rather than fighting it. */}\n      <div\n        style={{\n          width: winW,\n          opacity: stage.presence,\n          transform: `translateY(${(1 - stage.presence) * size * 0.5}px) scale(${0.992 + 0.008 * stage.presence})`,\n          transformOrigin: \"left center\",\n        }}\n      >\n        {eyebrow ? <Eyebrow label={eyebrow} tokens={t} size={size * 0.62} draw={stage.enter} /> : null}\n\n        <div\n          style={{\n            borderRadius: radius,\n            border: `1px solid ${t.rule}`,\n            backgroundColor: t.surface,\n            overflow: \"hidden\",\n            boxShadow: `0 ${size * 0.7}px ${size * 2.2}px rgba(0,0,0,0.22)`,\n          }}\n        >\n          <Chrome title={title} tokens={t} size={size} height={size * TITLE_EM} padX={padX} />\n\n          <div style={{ padding: `${padY}px ${padX}px` }}>\n            <div style={{ height: rowH, display: \"flex\", alignItems: \"center\" }}>\n              <Gutter tokens={t} size={size} width={gutter} color={t.accent}>\n                {prompt}\n              </Gutter>\n              <span style={{ ...mono(t, size), color: t.ink }}>\n                {schedule.keys\n                  .filter((key) => frame >= key.at)\n                  .map((key) => key.char)\n                  .join(\"\")}\n              </span>\n              <Caret charW={charW} size={size} color={t.accent} opacity={commandCaret} />\n            </div>\n\n            {schedule.lines.map((line, i) => (\n              <LogLine\n                key={`${line.text}-${i}`}\n                line={line}\n                frame={frame}\n                tokens={t}\n                size={size}\n                rowH={rowH}\n                gutter={gutter}\n                rise={rise}\n              />\n            ))}\n\n            <PromptLine\n              at={schedule.promptAt}\n              frame={frame}\n              prompt={prompt}\n              tokens={t}\n              size={size}\n              rowH={rowH}\n              gutter={gutter}\n              charW={charW}\n              rise={rise}\n              cycle={caretBlink}\n            />\n          </div>\n        </div>\n\n        {caption ? (\n          <div\n            style={{\n              ...mono(t, size * 0.62),\n              marginTop: size * 1.3,\n              textAlign: \"right\",\n              color: t.inkMuted,\n              opacity: captionIn,\n              whiteSpace: \"normal\",\n            }}\n          >\n            {caption}\n          </div>\n        ) : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------------- pieces */\n\n/** Window chrome: three dots, a centred mono title, and a hairline under it. */\nconst Chrome: React.FC<{\n  title: string;\n  tokens: BrandTokens;\n  size: number;\n  height: number;\n  padX: number;\n}> = ({ title, tokens, size, height, padX }) => (\n  <div\n    style={{\n      position: \"relative\",\n      height,\n      display: \"flex\",\n      alignItems: \"center\",\n      paddingLeft: padX,\n      backgroundColor: tokens.bg,\n      borderBottom: `1px solid ${tokens.rule}`,\n    }}\n  >\n    <div style={{ display: \"flex\", gap: size * 0.5 }}>\n      {[0, 1, 2].map((i) => (\n        <div\n          key={i}\n          style={{\n            width: size * 0.58,\n            height: size * 0.58,\n            borderRadius: \"50%\",\n            backgroundColor: tokens.inkMuted,\n            opacity: 0.38,\n          }}\n        />\n      ))}\n    </div>\n    <div\n      style={{\n        ...mono(tokens, size * 0.72),\n        position: \"absolute\",\n        left: 0,\n        right: 0,\n        padding: `0 ${padX * 3}px`,\n        textAlign: \"center\",\n        color: tokens.inkMuted,\n        overflow: \"hidden\",\n        textOverflow: \"ellipsis\",\n      }}\n    >\n      {title}\n    </div>\n  </div>\n);\n\n/** The glyph column. Fixed width, so glyph and no-glyph lines share one left edge. */\nconst Gutter: React.FC<{\n  tokens: BrandTokens;\n  size: number;\n  width: number;\n  color: string;\n  opacity?: number;\n  children?: React.ReactNode;\n}> = ({ tokens, size, width, color, opacity = 1, children }) => (\n  <span\n    style={{\n      ...mono(tokens, size),\n      width,\n      flexShrink: 0,\n      color,\n      opacity,\n      display: \"inline-flex\",\n      alignItems: \"center\",\n    }}\n  >\n    {children}\n  </span>\n);\n\n/**\n * The completion mark, drawn rather than typed. `../fonts` loads the `latin`\n * subset, which has no U+2713, and a tick is the one glyph in this component that\n * must never come back as a fallback box. One cell wide, so the column holds, and\n * it inherits the gutter's colour so a resolved step and a success line differ by\n * exactly one token.\n */\nconst Check: React.FC<{ size: number }> = ({ size }) => (\n  <svg\n    width={size * CHAR_EM}\n    height={size * CHAR_EM}\n    viewBox=\"0 0 12 12\"\n    fill=\"none\"\n    style={{ display: \"block\" }}\n  >\n    <path\n      d=\"M1.7 6.4 L4.6 9.2 L10.3 3\"\n      stroke=\"currentColor\"\n      strokeWidth={1.8}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst Caret: React.FC<{ charW: number; size: number; color: string; opacity: number }> = ({\n  charW,\n  size,\n  color,\n  opacity,\n}) => (\n  <span\n    style={{\n      display: \"inline-block\",\n      width: charW,\n      height: size * 1.16,\n      backgroundColor: color,\n      opacity,\n      flexShrink: 0,\n    }}\n  />\n);\n\n/**\n * One line of output. It fades up and rises a hair as it lands — 4px, enough to\n * read as arriving from below the fold and not enough to look like a slide.\n * The row's height is reserved from frame 0, so the window never reflows.\n */\nconst LogLine: React.FC<{\n  line: LoggedLine;\n  frame: number;\n  tokens: BrandTokens;\n  size: number;\n  rowH: number;\n  gutter: number;\n  rise: number;\n}> = ({ line, frame, tokens, size, rowH, gutter, rise }) => {\n  const land = interpolate(frame, [line.at, line.at + 9], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const spinning = line.tone === \"spinner\" && frame < line.until;\n  const step = Math.floor(Math.max(0, frame - line.at) / SPINNER_TICK) % SPINNER_FRAMES.length;\n  const done = line.tone === \"ok\" || (line.tone === \"spinner\" && !spinning);\n\n  return (\n    <div\n      style={{\n        height: rowH,\n        display: \"flex\",\n        alignItems: \"center\",\n        opacity: land,\n        transform: `translateY(${(1 - land) * rise}px)`,\n      }}\n    >\n      <Gutter\n        tokens={tokens}\n        size={size}\n        width={gutter}\n        color={line.tone === \"ok\" ? tokens.accent : tokens.inkMuted}\n        opacity={line.tone === \"ok\" ? 1 : 0.85}\n      >\n        {done ? <Check size={size} /> : spinning ? SPINNER_FRAMES[step] : \"\"}\n      </Gutter>\n      <span\n        style={{\n          ...mono(tokens, size),\n          color:\n            line.tone === \"ok\" ? tokens.accent : line.tone === \"dim\" ? tokens.inkMuted : tokens.ink,\n          fontWeight: line.tone === \"ok\" ? 600 : 500,\n        }}\n      >\n        {line.text}\n      </span>\n    </div>\n  );\n};\n\n/** The prompt the shell leaves behind. The only thing still moving at rest. */\nconst PromptLine: React.FC<{\n  at: number;\n  frame: number;\n  prompt: string;\n  tokens: BrandTokens;\n  size: number;\n  rowH: number;\n  gutter: number;\n  charW: number;\n  rise: number;\n  cycle: number;\n}> = ({ at, frame, prompt, tokens, size, rowH, gutter, charW, rise, cycle }) => {\n  const land = interpolate(frame, [at, at + 9], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  return (\n    <div\n      style={{\n        height: rowH,\n        display: \"flex\",\n        alignItems: \"center\",\n        opacity: land,\n        transform: `translateY(${(1 - land) * rise}px)`,\n      }}\n    >\n      <Gutter tokens={tokens} size={size} width={gutter} color={tokens.accent}>\n        {prompt}\n      </Gutter>\n      <Caret charW={charW} size={size} color={tokens.accent} opacity={blink(frame, at + 4, cycle)} />\n    </div>\n  );\n};\n\n/** Accent dash, then the label. The dash draws out from the window's left edge. */\nconst Eyebrow: React.FC<{ label: string; tokens: BrandTokens; size: number; draw: number }> = ({\n  label,\n  tokens,\n  size,\n  draw,\n}) => (\n  <div style={{ display: \"flex\", alignItems: \"center\", gap: size * 0.7, marginBottom: size * 1.5 }}>\n    <div\n      style={{\n        width: size * 1.6,\n        height: Math.max(2, size * 0.11),\n        backgroundColor: tokens.accent,\n        transform: `scaleX(${draw})`,\n        transformOrigin: \"left\",\n      }}\n    />\n    <div\n      style={{\n        ...mono(tokens, size),\n        fontWeight: 600,\n        letterSpacing: size * 0.18,\n        textTransform: \"uppercase\",\n        color: tokens.inkMuted,\n      }}\n    >\n      {label}\n    </div>\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"terminal-window\",\n  name: \"Terminal Window\",\n  category: \"product\",\n  summary:\n    \"A terminal runs a command: it types in, then the log streams line by line with a cycling spinner and an accent success line\",\n  tags: [\"terminal\", \"cli\", \"shell\", \"typing\", \"spinner\", \"product\", \"developer tooling\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: terminalWindowSchema,\n  defaults: {\n    command: \"npx remotion render hero out/hero.mp4\",\n    output: [\n      { text: \"bundling the composition\", tone: \"spinner\" },\n      { text: \"webpack compiled in 1.9s\", tone: \"dim\" },\n      { text: \"rendering 240 frames at 1920×1080\", tone: \"spinner\" },\n      { text: \"concurrency 8 · h264 · crf 18\", tone: \"dim\" },\n      { text: \"frames 240/240 · 34 fps\", tone: \"dim\" },\n      { text: \"out/hero.mp4 — 8.0s · 4.2 MB\", tone: \"ok\" },\n    ],\n    title: \"~/motionstash — zsh\",\n    prompt: \"$\",\n    eyebrow: \"Motionstash\",\n    caption: \"Deterministic render — the same frames on every machine\",\n    caretBlink: 20,\n    seed: \"terminal-01\",\n    brand: \"slate\",\n  },\n  component: TerminalWindow,\n  file: \"src/registry/items/terminal-window.tsx\",\n  origin: {\n    prompt:\n      \"A terminal window running a command. Window chrome with traffic lights and a mono title, a prompt glyph, then the command types itself in and the output streams out line by line — a spinner cycling while a step runs, dim detail lines, one accent-coloured success line at the end, and the prompt coming back with a blinking caret. Tight line heights, generous window padding, true monospace throughout, and each output line should fade up and rise a few pixels as it lands.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A terminal window running a command. Window chrome with traffic lights and a mono title, a prompt glyph, then the command types itself in and the output streams out line by line — a spinner cycling while a step runs, dim detail lines, one accent-coloured success line at the end, and the prompt coming back with a blinking caret. Tight line heights, generous window padding, true monospace throughout, and each output line should fade up and rise a few pixels as it lands.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2518,
      "props": [
        {
          "name": "command",
          "type": "string",
          "description": "The command that types in after the prompt glyph. Keep it to one line — 30–45 characters reads best at 16:9; longer shrinks the type for every line in the window.",
          "required": false,
          "defaultValue": "npx remotion render hero out/hero.mp4",
          "options": null
        },
        {
          "name": "output",
          "type": "array",
          "description": "The log, in order. One to nine lines; the window reserves all of their space from frame 0, so it never reflows as they land. Mix tones for hierarchy — spinners for the two or three real steps, dim for detail, one 'ok' at the end.",
          "required": false,
          "defaultValue": "6 items",
          "options": null
        },
        {
          "name": "title",
          "type": "string",
          "description": "Mono title centred in the window chrome. A path and a shell name is the convention. Empty string for bare chrome.",
          "required": false,
          "defaultValue": "~/motionstash — zsh",
          "options": null
        },
        {
          "name": "prompt",
          "type": "string",
          "description": "Prompt glyph in the gutter of the command line and of the prompt left behind at the end. '$', '%' and '›' all work.",
          "required": false,
          "defaultValue": "$",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label above the window, preceded by an accent dash. Empty string for none.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Dim mono note under the window, aligned to its right edge, fading in only once the prompt returns. Empty string for none.",
          "required": false,
          "defaultValue": "Deterministic render — the same frames on every machine",
          "options": null
        },
        {
          "name": "caretBlink",
          "type": "number",
          "description": "Frames in one full caret blink. 20 is ~1.5 blinks a second at 30fps. The caret holds solid while keys are landing and only blinks when the shell is waiting.",
          "required": false,
          "defaultValue": "20",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seeds the per-keystroke jitter that keeps the typing off the metronome. Change it for a differently-fumbled take of identical duration; the same seed always renders identical frames.",
          "required": false,
          "defaultValue": "terminal-01",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "text-highlight-sweep",
      "name": "Text Highlight Sweep",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "A sentence fades up, then a marker sweeps in behind one phrase and flips its ink",
      "tags": [
        "highlight",
        "marker",
        "emphasis",
        "sentence",
        "sweep",
        "annotation"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 90,
      "durationSeconds": 3,
      "budget": {
        "enter": 15,
        "hold": 60,
        "exit": 15
      },
      "file": "src/registry/items/text-highlight-sweep.tsx",
      "source": "/**\n * Text Highlight Sweep — a sentence where one phrase gets marked.\n *\n * The sentence fades up as a single unit (one presence curve, no per-word\n * stagger: the point of the shot is the mark, not the arrival), and then one\n * marker wipes in behind the emphasised phrase left-to-right.\n *\n * The interesting problem here is coordinate alignment. The marker has to sit\n * behind the phrase, bleed slightly past it the way a real marker does, and —\n * for the solid block — flip the ink under itself as its leading edge travels.\n * That only works if the block's `scaleX` and the inverted ink's `clipPath`\n * measure from exactly the same box, so the phrase wrapper is padded by the\n * bleed and then given a matching negative margin: the marker layer, the ink\n * overlay and the wrapper's padding box are all one geometry, while the inline\n * flow around it is untouched.\n */\nimport { evolvePath } from \"@remotion/paths\";\nimport React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const textHighlightSweepSchema = z.object({\n  sentence: z\n    .string()\n    .describe(\n      \"The full sentence, punctuation included. Eight to sixteen words reads best; longer drops the type size to keep it inside three lines.\",\n    ),\n  emphasis: z\n    .string()\n    .describe(\n      \"The phrase inside `sentence` to mark. Must appear in it (matched case-insensitively, first occurrence); if it does not, the sentence renders unmarked. Keep it to two to four words — it is held on one line as an unbreakable unit so the marker stays a single clean shape.\",\n    ),\n  marker: z\n    .enum([\"block\", \"underline\", \"box\"])\n    .default(\"block\")\n    .describe(\n      \"How the phrase is marked. 'block' is a solid accent slab behind the words and inverts their ink as it passes — the strongest and the default. 'underline' is a thick highlighter bar under the baseline, quieter and safe over long phrases. 'box' is a hand-drawn rounded outline drawn on with an SVG path, for an annotated, marked-up-by-a-human feel.\",\n    ),\n  eyebrow: z\n    .string()\n    .optional()\n    .describe(\"Small uppercase mono label above the sentence. Omit for a barer frame.\"),\n  footnote: z\n    .string()\n    .optional()\n    .describe(\n      \"One supporting line under a hairline rule — a source, a caveat, a second thought. Omit to let the sentence stand alone.\",\n    ),\n  align: z\n    .enum([\"left\", \"center\"])\n    .default(\"left\")\n    .describe(\n      \"Text alignment. 'left' composes against a margin rule and is the editorial default; 'center' drops the rule and suits 1:1 and title-card use.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof textHighlightSweepSchema>;\ntype MarkerStyle = Props[\"marker\"];\n\n/** Leading of the sentence. Loose enough that two stacked markers never touch. */\nconst SENTENCE_LINE_HEIGHT = 1.3;\n/** Tracking of the sentence, in em. Shared so the box marker can allow for it. */\nconst SENTENCE_TRACKING = -0.018;\n\n/** The sweep starts just before the fade-up finishes, so it reads as one event. */\nconst SWEEP_START = 12;\nconst SWEEP_FRAMES: Record<MarkerStyle, number> = { block: 16, underline: 14, box: 24 };\n\n/* ------------------------------------------------------------------ helpers */\n\n/** Split a sentence into the run before the phrase, the phrase, and the run after. */\nconst splitOnPhrase = (sentence: string, phrase: string) => {\n  const needle = phrase.trim();\n  const at = needle ? sentence.toLowerCase().indexOf(needle.toLowerCase()) : -1;\n  if (at < 0) return { before: sentence, phrase: \"\", after: \"\" };\n  return {\n    before: sentence.slice(0, at),\n    phrase: sentence.slice(at, at + needle.length),\n    after: sentence.slice(at + needle.length),\n  };\n};\n\nconst NARROW_GLYPHS = \"iíìjlItf.,;:!|'\\\"()[]{}-/\\\\\";\nconst WIDE_GLYPHS = \"mwMWQ@%&\";\n\n/**\n * Approximate advance width of a string, in px.\n *\n * The `box` marker needs a number at render time to build its SVG viewBox, and\n * measuring the real span would mean a ref plus an effect — which the\n * determinism rule rules out. These ratios are averaged from Archivo and Inter\n * at weight 600. The path is then stretched to the true span width, so a few\n * percent of error shows up only as a few percent of stroke-weight variation\n * between the horizontal and vertical strokes — which, on a mark that is\n * supposed to look drawn by hand, is not a defect.\n */\nconst estimateWidth = (text: string, size: number) => {\n  let units = 0;\n  for (const ch of text) {\n    if (ch === \" \") units += 0.26;\n    else if (NARROW_GLYPHS.includes(ch)) units += 0.32;\n    else if (WIDE_GLYPHS.includes(ch)) units += 0.86;\n    else if (ch >= \"A\" && ch <= \"Z\") units += 0.66;\n    else if (ch >= \"0\" && ch <= \"9\") units += 0.58;\n    else units += 0.54;\n  }\n  return units * size;\n};\n\n/**\n * A rounded box that looks drawn rather than drafted: every edge is a cubic with\n * jittered control points, every corner a quadratic, and the stroke closes past\n * where it started and runs on along the top edge — the overshoot is the tell.\n * Jitter comes from `random(seed)`, so the wobble is identical on every frame\n * and on every machine.\n */\nconst handDrawnBoxPath = (w: number, h: number, seed: string): string => {\n  const f = (v: number) => v.toFixed(1);\n  const amp = h * 0.055;\n  const wob = (k: number, scale = 1) => (random(`${seed}:${k}`) - 0.5) * 2 * amp * scale;\n\n  const r = Math.min(h * 0.44, w * 0.2);\n  const ty = amp + wob(1, 0.6);\n  const by = h - amp + wob(2, 0.6);\n  const lx = amp + wob(3, 0.6);\n  const rx = w - amp + wob(4, 0.6);\n  const spanX = rx - lx;\n  const spanY = by - ty;\n\n  return [\n    `M ${f(lx + r)} ${f(ty)}`,\n    `C ${f(lx + spanX * 0.32)} ${f(ty + wob(5, 1.5))} ${f(lx + spanX * 0.68)} ${f(ty + wob(6, 1.5))} ${f(rx - r)} ${f(ty)}`,\n    `Q ${f(rx)} ${f(ty)} ${f(rx)} ${f(ty + r)}`,\n    `C ${f(rx + wob(7, 1.2))} ${f(ty + spanY * 0.45)} ${f(rx + wob(8, 1.2))} ${f(ty + spanY * 0.72)} ${f(rx)} ${f(by - r)}`,\n    `Q ${f(rx)} ${f(by)} ${f(rx - r)} ${f(by)}`,\n    `C ${f(lx + spanX * 0.68)} ${f(by + wob(9, 1.5))} ${f(lx + spanX * 0.32)} ${f(by + wob(10, 1.5))} ${f(lx + r)} ${f(by)}`,\n    `Q ${f(lx)} ${f(by)} ${f(lx)} ${f(by - r)}`,\n    `C ${f(lx + wob(11, 1.2))} ${f(ty + spanY * 0.68)} ${f(lx + wob(12, 1.2))} ${f(ty + spanY * 0.38)} ${f(lx)} ${f(ty + r)}`,\n    `Q ${f(lx)} ${f(ty - amp * 0.3)} ${f(lx + r * 0.9)} ${f(ty + wob(13, 0.5))}`,\n    `C ${f(lx + spanX * 0.16)} ${f(ty - amp * 1.3)} ${f(lx + spanX * 0.28)} ${f(ty + amp * 0.9)} ${f(lx + spanX * 0.42)} ${f(ty + amp * 0.1)}`,\n  ].join(\" \");\n};\n\n/* ---------------------------------------------------------------- component */\n\nconst TextHighlightSweep: React.FC<Props> = ({\n  sentence,\n  emphasis,\n  marker,\n  eyebrow,\n  footnote,\n  align,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGETS.statement);\n\n  const parts = splitOnPhrase(sentence, emphasis);\n  const chars = sentence.trim().length;\n\n  // Type scale is derived from frame width and stepped by copy length, so one\n  // component serves 16:9, 9:16, 1:1 and 4:5 and always lands in two or three lines.\n  const size = width * (chars > 96 ? 0.04 : chars > 68 ? 0.048 : chars > 44 ? 0.058 : 0.07);\n  const column = width * (align === \"center\" ? 0.78 : 0.74);\n  const hairline = Math.max(1, width * 0.0009);\n\n  const sweep = interpolate(\n    stage.frame,\n    [SWEEP_START, SWEEP_START + SWEEP_FRAMES[marker]],\n    [0, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.travel },\n  );\n\n  // The whole block arrives as one unit: a single opacity and a single lift.\n  const lift = interpolate(stage.enter, [0, 1], [size * 0.16, 0]);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: align === \"center\" ? \"center\" : \"flex-start\",\n        padding: `0 ${width * 0.085}px`,\n        // Optical centre: the footnote hangs weight below, so sit a little high.\n        paddingBottom: height * 0.04,\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"stretch\",\n          gap: width * 0.028,\n          opacity: stage.presence,\n          transform: `translateY(${lift}px)`,\n          // The markers sit at z-index -1, behind the glyphs. Isolating here\n          // guarantees they stay above the canvas rather than falling behind it.\n          isolation: \"isolate\",\n        }}\n      >\n        {align === \"left\" ? <MarginRule width={hairline} tokens={t} /> : null}\n\n        <div style={{ maxWidth: column, textAlign: align }}>\n          {eyebrow ? (\n            <div\n              style={{\n                fontFamily: t.fontMono,\n                fontSize: size * 0.155,\n                fontWeight: 600,\n                letterSpacing: size * 0.026,\n                textTransform: \"uppercase\",\n                color: t.accent,\n                marginBottom: size * 0.42,\n              }}\n            >\n              {eyebrow}\n            </div>\n          ) : null}\n\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontSize: size,\n              fontWeight: 600,\n              lineHeight: SENTENCE_LINE_HEIGHT,\n              letterSpacing: size * SENTENCE_TRACKING,\n              color: t.ink,\n            }}\n          >\n            {/* Every text run is positioned, so it paints above the marker layer\n                even where the marker's bleed reaches under adjacent punctuation. */}\n            {parts.before ? <span style={{ position: \"relative\" }}>{parts.before}</span> : null}\n            {parts.phrase ? (\n              <MarkedPhrase text={parts.phrase} marker={marker} sweep={sweep} size={size} tokens={t} />\n            ) : null}\n            {parts.after ? <span style={{ position: \"relative\" }}>{parts.after}</span> : null}\n          </div>\n\n          {footnote ? (\n            <>\n              <div\n                style={{\n                  height: hairline,\n                  width: Math.min(column, size * 3.4),\n                  backgroundColor: t.rule,\n                  marginTop: size * 0.46,\n                  marginBottom: size * 0.26,\n                  marginLeft: align === \"center\" ? \"auto\" : 0,\n                  marginRight: align === \"center\" ? \"auto\" : 0,\n                }}\n              />\n              <div\n                style={{\n                  fontFamily: t.fontBody,\n                  fontSize: size * 0.175,\n                  fontWeight: 400,\n                  lineHeight: 1.5,\n                  color: t.inkMuted,\n                  maxWidth: column * 0.66,\n                  marginLeft: align === \"center\" ? \"auto\" : 0,\n                  marginRight: align === \"center\" ? \"auto\" : 0,\n                }}\n              >\n                {footnote}\n              </div>\n            </>\n          ) : null}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/**\n * The margin rule the left-aligned composition hangs off. Hairline in `rule`,\n * with its top fifth in `accent` — a static editorial tick, not a gesture.\n */\nconst MarginRule: React.FC<{ width: number; tokens: BrandTokens }> = ({ width, tokens }) => (\n  <div style={{ width, flexShrink: 0, backgroundColor: tokens.rule }}>\n    <div style={{ width: \"100%\", height: \"22%\", backgroundColor: tokens.accent }} />\n  </div>\n);\n\n/**\n * The marked phrase. `display: inline-block` keeps it on one line so the marker\n * is always a single shape, and the padding/negative-margin pair gives the\n * marker its bleed without disturbing the words on either side.\n */\nconst MarkedPhrase: React.FC<{\n  text: string;\n  marker: MarkerStyle;\n  sweep: number;\n  size: number;\n  tokens: BrandTokens;\n}> = ({ text, marker, sweep, size, tokens: t }) => {\n  const padX = size * (marker === \"box\" ? 0.19 : 0.14);\n  // The block bleeds fully into the word spaces; the drawn box keeps a little\n  // more air, so it only gives back half of its padding to the inline flow.\n  const margin = marker === \"box\" ? -padX * 0.5 : -padX;\n\n  return (\n    <span\n      style={{\n        position: \"relative\",\n        display: \"inline-block\",\n        whiteSpace: \"nowrap\",\n        padding: `0 ${padX}px`,\n        margin: `0 ${margin}px`,\n      }}\n    >\n      {marker === \"block\" ? <BlockMarker sweep={sweep} size={size} tokens={t} /> : null}\n      {marker === \"underline\" ? <UnderlineMarker sweep={sweep} size={size} tokens={t} /> : null}\n      {marker === \"box\" ? <BoxMarker text={text} sweep={sweep} size={size} tokens={t} /> : null}\n\n      <span style={{ position: \"relative\", color: t.ink }}>{text}</span>\n\n      {/* Inverted ink, clipped to exactly the swept region. Same box as the\n          block's scaleX, so the flip happens on the block's leading edge\n          rather than a frame either side of it. */}\n      {marker === \"block\" ? (\n        <span\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            top: 0,\n            bottom: 0,\n            padding: `0 ${padX}px`,\n            whiteSpace: \"nowrap\",\n            color: t.accentInk,\n            clipPath: `inset(0 ${(1 - sweep) * 100}% 0 0)`,\n            zIndex: 1,\n          }}\n        >\n          {text}\n        </span>\n      ) : null}\n    </span>\n  );\n};\n\n/** Solid slab, tall enough to clear both cap height and descenders. */\nconst BlockMarker: React.FC<{ sweep: number; size: number; tokens: BrandTokens }> = ({\n  sweep,\n  size,\n  tokens: t,\n}) => (\n  <span\n    style={{\n      position: \"absolute\",\n      left: 0,\n      right: 0,\n      top: size * 0.12,\n      bottom: size * 0.06,\n      backgroundColor: t.accent,\n      borderRadius: Math.min(t.radius * 0.35, size * 0.05),\n      transform: `scaleX(${sweep})`,\n      transformOrigin: \"0% 50%\",\n      zIndex: -1,\n    }}\n  />\n);\n\n/** Highlighter bar, sitting just clear of the descenders. */\nconst UnderlineMarker: React.FC<{ sweep: number; size: number; tokens: BrandTokens }> = ({\n  sweep,\n  size,\n  tokens: t,\n}) => {\n  const bar = Math.max(2, size * 0.115);\n  // Pulls in slightly from the block marker's full bleed into the word spaces.\n  const inset = size * 0.05;\n  return (\n    <span\n      style={{\n        position: \"absolute\",\n        left: inset,\n        right: inset,\n        bottom: size * 0.02,\n        height: bar,\n        backgroundColor: t.accent,\n        borderRadius: Math.min(t.radius * 0.3, bar * 0.35),\n        transform: `scaleX(${sweep})`,\n        transformOrigin: \"0% 50%\",\n        zIndex: -1,\n      }}\n    />\n  );\n};\n\n/**\n * Hand-drawn outline, drawn on with `evolvePath`. The viewBox is built from the\n * estimated phrase width and stretched to the real one with\n * `preserveAspectRatio=\"none\"`; the overshoot tail needs `overflow: visible`.\n */\nconst BoxMarker: React.FC<{\n  text: string;\n  sweep: number;\n  size: number;\n  tokens: BrandTokens;\n}> = ({ text, sweep, size, tokens: t }) => {\n  const padX = size * 0.19;\n  const bleedY = size * 0.03;\n  const boxWidth = estimateWidth(text, size) + size * SENTENCE_TRACKING * text.length + padX * 2;\n  const boxHeight = size * SENTENCE_LINE_HEIGHT + bleedY * 2;\n  const path = handDrawnBoxPath(boxWidth, boxHeight, `highlight-box:${text}`);\n  const evolution = evolvePath(sweep, path);\n\n  return (\n    <span\n      style={{\n        position: \"absolute\",\n        left: 0,\n        right: 0,\n        top: -bleedY,\n        bottom: -bleedY,\n        zIndex: -1,\n        transform: \"rotate(-0.5deg)\",\n      }}\n    >\n      <svg\n        viewBox={`0 0 ${boxWidth.toFixed(1)} ${boxHeight.toFixed(1)}`}\n        preserveAspectRatio=\"none\"\n        style={{ position: \"absolute\", inset: 0, width: \"100%\", height: \"100%\", overflow: \"visible\" }}\n      >\n        <path\n          d={path}\n          fill=\"none\"\n          stroke={t.accent}\n          strokeWidth={Math.max(2, size * 0.055)}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          strokeDasharray={evolution.strokeDasharray}\n          strokeDashoffset={evolution.strokeDashoffset}\n        />\n      </svg>\n    </span>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"text-highlight-sweep\",\n  name: \"Text Highlight Sweep\",\n  category: \"typography\",\n  summary:\n    \"A sentence fades up, then a marker sweeps in behind one phrase and flips its ink\",\n  tags: [\"highlight\", \"marker\", \"emphasis\", \"sentence\", \"sweep\", \"annotation\"],\n  budget: BUDGETS.statement,\n  aspect: \"16:9\",\n  schema: textHighlightSweepSchema,\n  defaults: {\n    sentence: \"Motion that ships as code you own.\",\n    emphasis: \"code you own\",\n    marker: \"block\",\n    eyebrow: \"Motionstash\",\n    footnote: \"Every component is one portable file — brand tokens in, video out.\",\n    align: \"left\",\n    brand: \"slate\",\n  },\n  component: TextHighlightSweep,\n  file: \"src/registry/items/text-highlight-sweep.tsx\",\n  origin: {\n    prompt:\n      \"A sentence that fades up as one unit, then a highlighter sweeps left to right behind one phrase in it — with the option of a solid block that inverts the text it covers, a thick underline, or a rounded box that looks hand-drawn.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A sentence that fades up as one unit, then a highlighter sweeps left to right behind one phrase in it — with the option of a solid block that inverts the text it covers, a thick underline, or a rounded box that looks hand-drawn.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.321,
      "props": [
        {
          "name": "sentence",
          "type": "string",
          "description": "The full sentence, punctuation included. Eight to sixteen words reads best; longer drops the type size to keep it inside three lines.",
          "required": true,
          "defaultValue": "Motion that ships as code you own.",
          "options": null
        },
        {
          "name": "emphasis",
          "type": "string",
          "description": "The phrase inside `sentence` to mark. Must appear in it (matched case-insensitively, first occurrence); if it does not, the sentence renders unmarked. Keep it to two to four words — it is held on one line as an unbreakable unit so the marker stays a single clean shape.",
          "required": true,
          "defaultValue": "code you own",
          "options": null
        },
        {
          "name": "marker",
          "type": "\"block\" | \"underline\" | \"box\"",
          "description": "How the phrase is marked. 'block' is a solid accent slab behind the words and inverts their ink as it passes — the strongest and the default. 'underline' is a thick highlighter bar under the baseline, quieter and safe over long phrases. 'box' is a hand-drawn rounded outline drawn on with an SVG path, for an annotated, marked-up-by-a-human feel.",
          "required": false,
          "defaultValue": "block",
          "options": [
            "block",
            "underline",
            "box"
          ]
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small uppercase mono label above the sentence. Omit for a barer frame.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "footnote",
          "type": "string",
          "description": "One supporting line under a hairline rule — a source, a caveat, a second thought. Omit to let the sentence stand alone.",
          "required": false,
          "defaultValue": "Every component is one portable file — brand tokens in, v…",
          "options": null
        },
        {
          "name": "align",
          "type": "\"left\" | \"center\"",
          "description": "Text alignment. 'left' composes against a margin rule and is the editorial default; 'center' drops the rule and suits 1:1 and title-card use.",
          "required": false,
          "defaultValue": "left",
          "options": [
            "left",
            "center"
          ]
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "typewriter-caret",
      "name": "Typewriter Caret",
      "category": "typography",
      "categoryLabel": "Kinetic type",
      "summary": "Lines typed out character by character on seeded irregular timing, led by a block caret that blinks on a fixed frame cycle",
      "tags": [
        "typewriter",
        "caret",
        "cursor",
        "terminal",
        "kinetic type",
        "deterministic"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/typewriter-caret.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nconst BUDGET = BUDGETS.scene;\n\n/** The prompt glyph lands first, so the caret gets a beat to blink before anything is typed. */\nconst PROMPT_AT = 4;\n/** Typing owns the middle of the budget and never runs into the exit window. */\nconst TYPE_START = Math.round(BUDGET.enter * 0.78);\nconst TYPE_END = Math.round(BUDGET.enter + BUDGET.hold * 0.72);\n/** Hairline + caption draw once the last key has landed, confirming the frame is finished. */\nconst RULE_IN: [number, number] = [TYPE_END + 3, TYPE_END + 17];\n\nexport const typewriterCaretSchema = z.object({\n  lines: z\n    .array(z.string())\n    .min(1)\n    .max(4)\n    .default([\"Type the scene, not the tween.\", \"Then render it — frame by frame.\"])\n    .describe(\n      \"Lines typed one after another, in order, with a beat between them. One to four. The type scale shrinks to fit the longest line, so keep each one to a single measure — 25–40 characters reads best at 16:9.\",\n    ),\n  prefix: z\n    .string()\n    .default(\"›\")\n    .describe(\n      \"Mono prompt glyph set in front of the first line, terminal style. '›', '$' and '/' all work. Empty string for none; later lines indent to stay aligned either way.\",\n    ),\n  eyebrow: z\n    .string()\n    .default(\"Motionstash\")\n    .describe(\"Small mono label anchored top-left, preceded by an accent dash. Empty string for none.\"),\n  caption: z\n    .string()\n    .default(\"Seeded per-character jitter — the same take on every render\")\n    .describe(\"Footer note under a hairline, drawn in only after typing finishes. Empty string for none.\"),\n  typeface: z\n    .enum([\"display\", \"mono\"])\n    .default(\"display\")\n    .describe(\"'display' reads as an editorial statement being written; 'mono' as a terminal session.\"),\n  caretBlink: z\n    .number()\n    .int()\n    .min(8)\n    .max(60)\n    .default(20)\n    .describe(\n      \"Frames in one full blink cycle. 20 is ~1.5 blinks per second at 30fps. The caret crossfades rather than hard-cutting, and holds solid while keys are landing.\",\n    ),\n  seed: z\n    .string()\n    .default(\"caret-01\")\n    .describe(\n      \"Seeds the per-keystroke jitter. Change it for a differently-fumbled take of identical duration; the same seed always renders the same frames.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof typewriterCaretSchema>;\n\ntype Keystroke = {\n  char: string;\n  /** Index of the line this glyph belongs to. */\n  line: number;\n  /** Absolute frame at which the key lands. */\n  at: number;\n};\n\n/**\n * The keystroke schedule.\n *\n * A constant interval per character reads as a machine, so each interval is\n * weighted: seeded jitter, a short skip through spaces, a real pause after\n * punctuation and a longer beat between lines. The weights are then normalised\n * across the typing window, which means any length of text — one word or four\n * long lines — still finishes exactly on TYPE_END and leaves the held frame\n * fully typed. `random()` from Remotion keeps the whole thing deterministic:\n * same seed, same take, every render.\n */\nconst buildKeystrokes = (lines: string[], seed: string, start: number, end: number): Keystroke[] => {\n  const flat: { char: string; line: number }[] = [];\n  lines.forEach((line, lineIndex) => {\n    for (const char of Array.from(line)) flat.push({ char, line: lineIndex });\n  });\n  if (flat.length === 0) return [];\n\n  const weights = flat.map((glyph, i) => {\n    let w = 0.6 + random(seed + i) * 0.9;\n    const prev = i > 0 ? flat[i - 1] : null;\n    if (glyph.char === \" \") w *= 0.6;\n    if (prev && prev.line !== glyph.line) w += 7;\n    else if (prev && /[.!?…—]/.test(prev.char)) w += 2.6;\n    else if (prev && /[,;:)]/.test(prev.char)) w += 1.2;\n    return w;\n  });\n\n  const totalWeight = weights.reduce((a, b) => a + b, 0);\n  const span = Math.max(1, end - start);\n  let acc = 0;\n  return flat.map((glyph, i) => {\n    acc += weights[i];\n    return { char: glyph.char, line: glyph.line, at: start + (acc / totalWeight) * span };\n  });\n};\n\n/**\n * Blink on a fixed frame cycle, with eased edges so it reads as a phosphor\n * cursor rather than a strobe — and solid while keys are actually landing,\n * the way a real caret suppresses its own blink as you type.\n */\nconst caretOpacityAt = (frame: number, period: number) => {\n  const phase = ((((frame % period) + period) % period) / period);\n  const blink = interpolate(phase, [0, 0.34, 0.5, 0.84, 1], [1, 1, 0.04, 0.04, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const typing = interpolate(\n    frame,\n    [TYPE_START - 4, TYPE_START, TYPE_END, TYPE_END + 8],\n    [0, 1, 1, 0],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\", easing: EASING.travel },\n  );\n  return 1 - (1 - blink) * (1 - typing);\n};\n\nconst TypewriterCaret: React.FC<Props> = ({\n  lines,\n  prefix,\n  eyebrow,\n  caption,\n  typeface,\n  caretBlink,\n  seed,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n\n  // Asymmetric margins: generous on the left, wider still on the right, so the\n  // measure sits in the left two thirds instead of floating in the middle.\n  const padLeft = width * 0.1;\n  const padRight = width * 0.14;\n  const measure = width - padLeft - padRight;\n\n  const mono = typeface === \"mono\";\n  // Average glyph advance in em, used to fit the longest line to the measure so\n  // the type is as large as the copy allows. Caps and figures are considerably\n  // wider than lowercase, so the estimate leans on how shouty the copy is.\n  // Everything below is derived from frame width — no px assumes a 1920 frame.\n  const set = lines.join(\"\").replace(/\\s/g, \"\");\n  const caps = set.replace(/[^A-Z0-9]/g, \"\").length / Math.max(1, set.length);\n  const advance = mono ? 0.62 : 0.54 + caps * 0.14;\n  const longest = lines.reduce((n, line) => Math.max(n, Array.from(line).length), 1);\n  const fitted = measure / (longest * advance + (prefix ? 0.95 : 0));\n  const size = Math.max(width * 0.016, Math.min(width * 0.055, fitted));\n  const metaSize = width * 0.0112;\n\n  const keys = buildKeystrokes(lines, seed, TYPE_START, TYPE_END);\n  const perLine = lines.map((_, lineIndex) => keys.filter((k) => k.line === lineIndex));\n\n  // The caret lives on the first line that is still being typed, and comes to\n  // rest after the final character of the last one.\n  const pending = perLine.findIndex((ks) => ks.length > 0 && stage.frame < ks[ks.length - 1].at);\n  const caretLine = pending === -1 ? lines.length - 1 : pending;\n  const caretOpacity = caretOpacityAt(stage.frame, caretBlink);\n\n  const lift = interpolate(stage.enter, [0, 1], [size * 0.3, 0]);\n  const ruleIn = interpolate(stage.frame, RULE_IN, [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      {eyebrow ? (\n        <Eyebrow label={eyebrow} tokens={t} size={metaSize} left={padLeft} top={height * 0.13} />\n      ) : null}\n\n      {/* The measure. Optically centred vertically, hard left horizontally. */}\n      <AbsoluteFill\n        style={{\n          justifyContent: \"center\",\n          alignItems: \"flex-start\",\n          padding: `0 ${padRight}px ${height * 0.06}px ${padLeft}px`,\n        }}\n      >\n        <div style={{ opacity: stage.presence, transform: `translateY(${lift}px)` }}>\n          {lines.map((line, lineIndex) => (\n            <TypedLine\n              key={`${lineIndex}-${line}`}\n              keys={perLine[lineIndex]}\n              frame={stage.frame}\n              tokens={t}\n              size={size}\n              mono={mono}\n              prefix={lineIndex === 0 ? prefix : \"\"}\n              prefixWidth={prefix ? size * 0.95 : 0}\n              caret={lineIndex === caretLine ? caretOpacity : 0}\n            />\n          ))}\n        </div>\n      </AbsoluteFill>\n\n      {caption ? (\n        <Footer\n          label={caption}\n          tokens={t}\n          size={metaSize}\n          left={padLeft}\n          bottom={height * 0.12}\n          width={measure * 0.64}\n          progress={ruleIn * stage.presence}\n        />\n      ) : null}\n    </AbsoluteFill>\n  );\n};\n\n/**\n * One line, laid out once and revealed a character at a time.\n *\n * Every glyph is rendered from frame 0 — untyped ones simply at zero opacity —\n * so the line's layout is fixed and no already-typed character ever shifts as\n * the line grows. The caret is a real inline box spliced into the flow at the\n * cursor position, which is what lets it sit correctly against a proportional\n * typeface without any measurement.\n */\nconst TypedLine: React.FC<{\n  keys: Keystroke[];\n  frame: number;\n  tokens: BrandTokens;\n  size: number;\n  mono: boolean;\n  prefix: string;\n  prefixWidth: number;\n  /** Caret opacity, or 0 when the caret is on another line. */\n  caret: number;\n}> = ({ keys, frame, tokens, size, mono, prefix, prefixWidth, caret }) => {\n  const typed = keys.filter((k) => k.at <= frame).length;\n  const promptIn = interpolate(frame, [PROMPT_AT, PROMPT_AT + 6], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const cursor =\n    caret > 0 ? (\n      <span\n        style={{\n          display: \"inline-block\",\n          width: size * 0.48,\n          height: size * 0.78,\n          marginLeft: size * 0.05,\n          marginRight: size * -0.02,\n          verticalAlign: \"baseline\",\n          transform: `translateY(${size * 0.04}px)`,\n          borderRadius: Math.max(1, size * 0.02),\n          backgroundColor: tokens.accent,\n          opacity: caret,\n        }}\n      />\n    ) : null;\n\n  return (\n    <div\n      style={{\n        whiteSpace: \"pre\",\n        fontFamily: mono ? tokens.fontMono : tokens.fontDisplay,\n        fontSize: size,\n        fontWeight: mono ? 500 : 600,\n        lineHeight: 1.3,\n        letterSpacing: mono ? 0 : size * -0.018,\n        color: tokens.ink,\n      }}\n    >\n      {/* Fixed-width prompt gutter: holds the glyph on line one, holds the\n          indent on every line after it. */}\n      {prefixWidth > 0 ? (\n        <span style={{ display: \"inline-block\", width: prefixWidth }}>\n          {prefix ? (\n            <span\n              style={{\n                fontFamily: tokens.fontMono,\n                fontSize: size * 0.72,\n                fontWeight: 500,\n                color: tokens.accent,\n                opacity: promptIn * 0.65,\n              }}\n            >\n              {prefix}\n            </span>\n          ) : null}\n        </span>\n      ) : null}\n\n      {keys.map((k, i) => (\n        <React.Fragment key={i}>\n          {i === typed ? cursor : null}\n          <span\n            style={{\n              opacity: interpolate(frame, [k.at, k.at + 2.4], [0, 1], {\n                extrapolateLeft: \"clamp\",\n                extrapolateRight: \"clamp\",\n              }),\n            }}\n          >\n            {k.char}\n          </span>\n        </React.Fragment>\n      ))}\n      {typed >= keys.length ? cursor : null}\n    </div>\n  );\n};\n\nconst Eyebrow: React.FC<{\n  label: string;\n  tokens: BrandTokens;\n  size: number;\n  left: number;\n  top: number;\n}> = ({ label, tokens, size, left, top }) => {\n  const stage = useStage(BUDGET);\n  // Anchored on the left margin, so it arrives from the left.\n  const x = interpolate(stage.enter, [0, 1], [-size, 0]);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left,\n        top,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: size * 0.9,\n        opacity: stage.presence,\n        transform: `translateX(${x}px)`,\n      }}\n    >\n      <div\n        style={{\n          width: size * 1.9,\n          height: Math.max(2, size * 0.11),\n          backgroundColor: tokens.accent,\n          transform: `scaleX(${stage.enter})`,\n          transformOrigin: \"left\",\n        }}\n      />\n      <div\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: size,\n          fontWeight: 500,\n          letterSpacing: size * 0.16,\n          textTransform: \"uppercase\",\n          color: tokens.inkMuted,\n        }}\n      >\n        {label}\n      </div>\n    </div>\n  );\n};\n\nconst Footer: React.FC<{\n  label: string;\n  tokens: BrandTokens;\n  size: number;\n  left: number;\n  bottom: number;\n  width: number;\n  progress: number;\n}> = ({ label, tokens, size, left, bottom, width, progress }) => (\n  <div style={{ position: \"absolute\", left, bottom, width }}>\n    <div\n      style={{\n        height: Math.max(1, size * 0.06),\n        backgroundColor: tokens.rule,\n        transform: `scaleX(${progress})`,\n        transformOrigin: \"left\",\n      }}\n    />\n    <div\n      style={{\n        marginTop: size * 0.85,\n        fontFamily: tokens.fontMono,\n        fontSize: size * 0.86,\n        fontWeight: 500,\n        letterSpacing: size * 0.02,\n        color: tokens.inkMuted,\n        opacity: progress,\n      }}\n    >\n      {label}\n    </div>\n  </div>\n);\n\nexport default defineMotionItem({\n  id: \"typewriter-caret\",\n  name: \"Typewriter Caret\",\n  category: \"typography\",\n  summary:\n    \"Lines typed out character by character on seeded irregular timing, led by a block caret that blinks on a fixed frame cycle\",\n  tags: [\"typewriter\", \"caret\", \"cursor\", \"terminal\", \"kinetic type\", \"deterministic\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: typewriterCaretSchema,\n  defaults: {\n    lines: [\"Type the scene, not the tween.\", \"Then render it — frame by frame.\"],\n    prefix: \"›\",\n    eyebrow: \"Motionstash\",\n    caption: \"Seeded per-character jitter — the same take on every render\",\n    typeface: \"display\",\n    caretBlink: 20,\n    seed: \"caret-01\",\n    brand: \"slate\",\n  },\n  component: TypewriterCaret,\n  file: \"src/registry/items/typewriter-caret.tsx\",\n  origin: {\n    prompt:\n      \"Text typed out character by character with a blinking block caret, left-aligned on a generous margin with a mono prompt glyph in front of the first line. Make the per-character timing slightly irregular so it reads as a person typing rather than a metronome, pause between lines, and have the caret fade in and out on a fixed cycle instead of hard-cutting.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Text typed out character by character with a blinking block caret, left-aligned on a generous margin with a mono prompt glyph in front of the first line. Make the per-character timing slightly irregular so it reads as a person typing rather than a metronome, pause between lines, and have the caret fade in and out on a fixed cycle instead of hard-cutting.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.652,
      "props": [
        {
          "name": "lines",
          "type": "array",
          "description": "Lines typed one after another, in order, with a beat between them. One to four. The type scale shrinks to fit the longest line, so keep each one to a single measure — 25–40 characters reads best at 16:9.",
          "required": false,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "prefix",
          "type": "string",
          "description": "Mono prompt glyph set in front of the first line, terminal style. '›', '$' and '/' all work. Empty string for none; later lines indent to stay aligned either way.",
          "required": false,
          "defaultValue": "›",
          "options": null
        },
        {
          "name": "eyebrow",
          "type": "string",
          "description": "Small mono label anchored top-left, preceded by an accent dash. Empty string for none.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Footer note under a hairline, drawn in only after typing finishes. Empty string for none.",
          "required": false,
          "defaultValue": "Seeded per-character jitter — the same take on every render",
          "options": null
        },
        {
          "name": "typeface",
          "type": "\"display\" | \"mono\"",
          "description": "'display' reads as an editorial statement being written; 'mono' as a terminal session.",
          "required": false,
          "defaultValue": "display",
          "options": [
            "display",
            "mono"
          ]
        },
        {
          "name": "caretBlink",
          "type": "number",
          "description": "Frames in one full blink cycle. 20 is ~1.5 blinks per second at 30fps. The caret crossfades rather than hard-cutting, and holds solid while keys are landing.",
          "required": false,
          "defaultValue": "20",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seeds the per-keystroke jitter. Change it for a differently-fumbled take of identical duration; the same seed always renders the same frames.",
          "required": false,
          "defaultValue": "caret-01",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "x-post-card",
      "name": "Post Card",
      "category": "social",
      "categoryLabel": "Social formats",
      "summary": "A generic social post card with a drawn check and icons, scaling in on the surface while its metrics count up",
      "tags": [
        "social",
        "card",
        "post",
        "metrics",
        "square"
      ],
      "aspect": "1:1",
      "width": 1080,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/x-post-card.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, SPRINGS, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const xPostCardSchema = z.object({\n  name: z.string().default(\"Motionstash\").describe(\"Display name on the card.\"),\n  handle: z.string().default(\"@motionregistry\").describe(\"Handle, including the @.\"),\n  body: z\n    .string()\n    .default(\"Every component is one file you own. No config, no runtime, no lock-in.\")\n    .describe(\"Post body. Two or three lines reads best.\"),\n  mention: z\n    .string()\n    .optional()\n    .describe(\"A word or handle inside the body to render in the accent colour.\"),\n  verified: z.boolean().default(true).describe(\"Show a drawn verification check beside the name.\"),\n  metrics: z\n    .array(z.number())\n    .length(3)\n    .describe(\"Replies, reposts and likes. Each counts up.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof xPostCardSchema>;\n\n/**\n * A generic social post card — deliberately not any real platform.\n *\n * No third-party wordmark, glyph or brand colour appears here: the check, the\n * icons and the avatar are all drawn, so the component is safe to ship in a\n * commercial video and safe to restyle to any brand. The metrics count rather\n * than appearing, because a static number under an animated card reads as a\n * screenshot pasted into the frame.\n */\nconst XPostCard: React.FC<Props> = ({ name, handle, body, mention, verified, metrics, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, fps } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n\n  const pop = spring({ frame: stage.frame, fps, config: SPRINGS.settle, durationInFrames: 22 });\n  const cardW = width * 0.78;\n  const pad = cardW * 0.062;\n  const avatar = cardW * 0.11;\n  const initials = name\n    .split(/\\s+/)\n    .slice(0, 2)\n    .map((w) => w[0])\n    .join(\"\")\n    .toUpperCase();\n\n  const parts = mention && body.includes(mention) ? body.split(mention) : null;\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        justifyContent: \"center\",\n        alignItems: \"center\",\n        opacity: stage.presence,\n      }}\n    >\n      <div\n        style={{\n          width: cardW,\n          padding: pad,\n          borderRadius: t.radius * 1.6,\n          backgroundColor: t.surface,\n          border: `1px solid ${t.rule}`,\n          boxShadow: `0 ${cardW * 0.03}px ${cardW * 0.08}px rgba(0,0,0,0.18)`,\n          transform: `scale(${0.96 + pop * 0.04})`,\n        }}\n      >\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: pad * 0.55 }}>\n          <div\n            style={{\n              width: avatar,\n              height: avatar,\n              borderRadius: \"50%\",\n              backgroundColor: t.accent,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              flexShrink: 0,\n            }}\n          >\n            <span\n              style={{\n                fontFamily: t.fontDisplay,\n                fontSize: avatar * 0.4,\n                fontWeight: 700,\n                color: t.accentInk,\n              }}\n            >\n              {initials}\n            </span>\n          </div>\n\n          <div style={{ minWidth: 0 }}>\n            <div style={{ display: \"flex\", alignItems: \"center\", gap: pad * 0.22 }}>\n              <span\n                style={{\n                  fontFamily: t.fontDisplay,\n                  fontSize: cardW * 0.045,\n                  fontWeight: 600,\n                  color: t.ink,\n                }}\n              >\n                {name}\n              </span>\n              {verified ? <Check size={cardW * 0.042} tokens={t} /> : null}\n            </div>\n            <div\n              style={{\n                fontFamily: t.fontBody,\n                fontSize: cardW * 0.036,\n                color: t.inkMuted,\n                marginTop: pad * 0.08,\n              }}\n            >\n              {handle}\n            </div>\n          </div>\n        </div>\n\n        <div\n          style={{\n            marginTop: pad * 0.85,\n            fontFamily: t.fontBody,\n            fontSize: cardW * 0.05,\n            lineHeight: 1.42,\n            color: t.ink,\n          }}\n        >\n          {parts ? (\n            <>\n              {parts[0]}\n              <span style={{ color: t.accent }}>{mention}</span>\n              {parts.slice(1).join(mention)}\n            </>\n          ) : (\n            body\n          )}\n        </div>\n\n        <div\n          style={{\n            marginTop: pad,\n            paddingTop: pad * 0.7,\n            borderTop: `1px solid ${t.rule}`,\n            display: \"flex\",\n            gap: pad * 1.6,\n          }}\n        >\n          {metrics.map((value, i) => (\n            <Metric key={i} index={i} value={value} size={cardW * 0.036} tokens={t} />\n          ))}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Check: React.FC<{ size: number; tokens: BrandTokens }> = ({ size, tokens }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\">\n    <circle cx={12} cy={12} r={11} fill={tokens.accent} />\n    <path\n      d=\"M6.5 12.5 L10.2 16 L17.5 8.5\"\n      fill=\"none\"\n      stroke={tokens.accentInk}\n      strokeWidth={2.6}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst ICONS = [\n  \"M4 5h16v10H9l-5 4z\",\n  \"M6 7h11l-3-3 M18 17H7l3 3 M17 7v10 M7 7v10\",\n  \"M12 20s-7-4.5-7-9a4 4 0 017-2.6A4 4 0 0119 11c0 4.5-7 9-7 9z\",\n];\n\nconst Metric: React.FC<{\n  index: number;\n  value: number;\n  size: number;\n  tokens: BrandTokens;\n}> = ({ index, value, size, tokens }) => {\n  const stage = useStaggeredStage(BUDGETS.scene, index, 4);\n  const count = Math.round(\n    interpolate(stage.frame, [16, 46], [0, value], {\n      extrapolateLeft: \"clamp\",\n      extrapolateRight: \"clamp\",\n      easing: EASING.travel,\n    }),\n  );\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: size * 0.5, opacity: stage.enter }}>\n      <svg width={size * 1.25} height={size * 1.25} viewBox=\"0 0 24 24\">\n        <path\n          d={ICONS[index]}\n          fill=\"none\"\n          stroke={tokens.inkMuted}\n          strokeWidth={1.9}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n      <span\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: size,\n          color: tokens.inkMuted,\n          fontVariantNumeric: \"tabular-nums\",\n        }}\n      >\n        {count.toLocaleString(\"en-US\")}\n      </span>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"x-post-card\",\n  name: \"Post Card\",\n  category: \"social\",\n  summary:\n    \"A generic social post card with a drawn check and icons, scaling in on the surface while its metrics count up\",\n  tags: [\"social\", \"card\", \"post\", \"metrics\", \"square\"],\n  budget: BUDGETS.scene,\n  aspect: \"1:1\",\n  schema: xPostCardSchema,\n  defaults: {\n    name: \"Motionstash\",\n    handle: \"@motionregistry\",\n    body: \"Every component is one file you own. No config, no runtime, no lock-in.\",\n    mention: \"one file you own\",\n    verified: true,\n    metrics: [128, 542, 3894],\n    brand: \"slate\",\n  },\n  component: XPostCard,\n  file: \"src/registry/items/x-post-card.tsx\",\n  origin: {\n    prompt:\n      \"A social post card — avatar with initials, display name with a drawn verified check, handle, body copy with one accent-coloured phrase, and a metrics row whose numbers count up.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A social post card — avatar with initials, display name with a drawn verified check, handle, body copy with one accent-coloured phrase, and a metrics row whose numbers count up.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2721,
      "props": [
        {
          "name": "name",
          "type": "string",
          "description": "Display name on the card.",
          "required": false,
          "defaultValue": "Motionstash",
          "options": null
        },
        {
          "name": "handle",
          "type": "string",
          "description": "Handle, including the @.",
          "required": false,
          "defaultValue": "@motionregistry",
          "options": null
        },
        {
          "name": "body",
          "type": "string",
          "description": "Post body. Two or three lines reads best.",
          "required": false,
          "defaultValue": "Every component is one file you own. No config, no runtim…",
          "options": null
        },
        {
          "name": "mention",
          "type": "string",
          "description": "A word or handle inside the body to render in the accent colour.",
          "required": false,
          "defaultValue": "one file you own",
          "options": null
        },
        {
          "name": "verified",
          "type": "boolean",
          "description": "Show a drawn verification check beside the name.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "metrics",
          "type": "array",
          "description": "Replies, reposts and likes. Each counts up.",
          "required": true,
          "defaultValue": "3 items",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "youtube-comment-highlight",
      "name": "YouTube Comment Highlight",
      "category": "youtube",
      "categoryLabel": "YouTube",
      "summary": "A viewer's comment featured as the platform renders it, with the like row and the creator's heart landing after the words",
      "tags": [
        "youtube",
        "comment",
        "social proof",
        "testimonial",
        "card",
        "ui"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/youtube-comment-highlight.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\nimport { PLATFORM } from \"../platform\";\n\n/**\n * A viewer's comment, featured.\n *\n * The comment arrives as a unit and then its furniture arrives after it: the\n * handle and the text land together, and only then do the like count, the heart\n * and the reply line fade up. That order is the whole trick — reading someone\n * else's words is the point, and a like count that animates at the same time as\n * the sentence competes with it for the first second of attention.\n *\n * The heart is the creator's, not the commenter's, and it lands last with a\n * small overshoot. On the platform that mark means the channel replied; here it\n * is the beat that closes the card.\n */\n\nconst CARD_W = 1180;\n\nexport const youtubeCommentHighlightSchema = z.object({\n  handle: z\n    .string()\n    .describe(\"The commenter's @handle. Include the @ — without it the card reads as a generic quote rather than as a comment.\"),\n  timeAgo: z\n    .string()\n    .default(\"2 days ago\")\n    .describe(\"Relative timestamp beside the handle, exactly as the platform words it.\"),\n  comment: z\n    .string()\n    .describe(\n      \"The comment itself. Two or three lines is the sweet spot: long enough to feel real, short enough to read before the shot cuts.\",\n    ),\n  likes: z\n    .string()\n    .default(\"1.2K\")\n    .describe(\"Like count as written text, so 1.2K and 24 both set correctly without number formatting.\"),\n  replies: z\n    .string()\n    .default(\"\")\n    .describe(\"Reply count line under the comment — '18 replies'. Empty omits the line.\"),\n  initials: z\n    .string()\n    .default(\"\")\n    .describe(\"One or two characters for the drawn avatar. Empty uses the handle's first letter after the @.\"),\n  avatarColor: z\n    .string()\n    .default(\"\")\n    .describe(\"Avatar fill. Empty uses the brand accent, which keeps the card inside the video's palette.\"),\n  hearted: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the creator's heart on the like row, landing as the last beat. The reason to feature a comment is usually that you replied to it.\"),\n  brand: z\n    .string()\n    .default(\"paper\")\n    .describe(\"Brand preset. Light presets read as the platform's light mode, dark ones as its dark mode.\"),\n});\n\ntype Props = z.infer<typeof youtubeCommentHighlightSchema>;\n\nexport const YoutubeCommentHighlight: React.FC<Props> = ({\n  handle,\n  timeAgo,\n  comment,\n  likes,\n  replies,\n  initials,\n  avatarColor,\n  hearted,\n  brand,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n  const t = BRANDS[brand] ?? BRANDS.paper;\n\n  // Card first, then furniture. Three staggered steps, not one.\n  const s0 = useStaggeredStage(BUDGETS.scene, 0, 6);\n  const s1 = useStaggeredStage(BUDGETS.scene, 1, 6);\n  const s2 = useStaggeredStage(BUDGETS.scene, 2, 6);\n  const steps = [s0.enter, s1.enter, s2.enter];\n\n  const scale = Math.min(width / (CARD_W + 140), height / 620, 1.25);\n  const rise = interpolate(stage.enter, [0, 1], [40, 0]);\n\n  const heart = spring({ frame: frame - 34, fps, config: { damping: 10, mass: 0.5 } });\n\n  /**\n   * A slow float, and a heart that keeps beating after it lands.\n   *\n   * A card that rises in and then holds perfectly still reads as a screenshot\n   * rather than as a shot, and the craft audit scores it accordingly — this one\n   * sat near the bottom of the library until it was given something that carries\n   * on moving through the hold.\n   */\n  const float = Math.sin((frame / fps) * 0.5) * 5;\n  const beat = 1 + 0.06 * Math.sin((frame / fps) * 2.1);\n  const letter = (initials || handle.replace(\"@\", \"\").charAt(0) || \"?\").slice(0, 2).toUpperCase();\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, alignItems: \"center\", justifyContent: \"center\" }}>\n      <div\n        style={{\n          width: CARD_W,\n          transform: `scale(${scale}) translateY(${rise + float}px)`,\n          opacity: stage.presence,\n          backgroundColor: t.surface,\n          border: `2px solid ${t.rule}`,\n          borderRadius: 26,\n          padding: \"42px 48px\",\n          display: \"flex\",\n          gap: 30,\n        }}\n      >\n        <div\n          style={{\n            width: 104,\n            height: 104,\n            borderRadius: \"50%\",\n            backgroundColor: avatarColor || t.accent,\n            flexShrink: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#FFFFFF\",\n            fontFamily: t.fontDisplay,\n            fontWeight: 700,\n            fontSize: 42,\n          }}\n        >\n          {letter}\n        </div>\n\n        <div style={{ flex: 1, minWidth: 0 }}>\n          {/* handle + timestamp */}\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"baseline\",\n              gap: 14,\n              opacity: steps[0],\n              transform: `translateY(${(1 - steps[0]) * 10}px)`,\n            }}\n          >\n            <span\n              style={{\n                fontFamily: t.fontDisplay,\n                fontWeight: 600,\n                fontSize: 30,\n                color: t.ink,\n              }}\n            >\n              {handle}\n            </span>\n            <span style={{ fontFamily: t.fontBody, fontSize: 24, color: t.inkMuted }}>\n              {timeAgo}\n            </span>\n          </div>\n\n          {/* the comment */}\n          <p\n            style={{\n              margin: \"16px 0 0\",\n              fontFamily: t.fontBody,\n              fontWeight: 400,\n              fontSize: 34,\n              lineHeight: 1.42,\n              color: t.ink,\n              opacity: steps[1],\n              transform: `translateY(${(1 - steps[1]) * 12}px)`,\n            }}\n          >\n            {comment}\n          </p>\n\n          {/* like row */}\n          <div\n            style={{\n              marginTop: 26,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 22,\n              opacity: steps[2],\n            }}\n          >\n            <ThumbUp color={t.inkMuted} />\n            <span style={{ fontFamily: t.fontBody, fontSize: 25, color: t.inkMuted }}>{likes}</span>\n            <ThumbDown color={t.inkMuted} />\n            <span\n              style={{\n                fontFamily: t.fontDisplay,\n                fontWeight: 600,\n                fontSize: 25,\n                color: t.inkMuted,\n                marginLeft: 8,\n              }}\n            >\n              Reply\n            </span>\n\n            {hearted ? (\n              <span\n                style={{\n                  marginLeft: 6,\n                  display: \"inline-flex\",\n                  transform: `scale(${Math.max(0, heart) * beat})`,\n                }}\n              >\n                <Heart color={PLATFORM.youtubeRed} ring={t.surface} />\n              </span>\n            ) : null}\n          </div>\n\n          {replies ? (\n            <div\n              style={{\n                marginTop: 20,\n                fontFamily: t.fontDisplay,\n                fontWeight: 600,\n                fontSize: 25,\n                color: t.accent,\n                opacity: steps[2],\n              }}\n            >\n              {replies}\n            </div>\n          ) : null}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst ThumbUp: React.FC<{ color: string }> = ({ color }) => (\n  <svg width=\"30\" height=\"30\" viewBox=\"0 0 24 24\" fill=\"none\">\n    <path\n      d=\"M7 10.5v9H4.5v-9H7Zm2 0 3.4-6.6a1.6 1.6 0 0 1 3 .8V9h4.1a1.6 1.6 0 0 1 1.55 2l-1.8 6.6a1.6 1.6 0 0 1-1.55 1.2H9v-8.3Z\"\n      stroke={color}\n      strokeWidth=\"1.6\"\n      strokeLinejoin=\"round\"\n    />\n  </svg>\n);\n\nconst ThumbDown: React.FC<{ color: string }> = ({ color }) => (\n  <svg width=\"30\" height=\"30\" viewBox=\"0 0 24 24\" fill=\"none\">\n    <g transform=\"rotate(180 12 12)\">\n      <path\n        d=\"M7 10.5v9H4.5v-9H7Zm2 0 3.4-6.6a1.6 1.6 0 0 1 3 .8V9h4.1a1.6 1.6 0 0 1 1.55 2l-1.8 6.6a1.6 1.6 0 0 1-1.55 1.2H9v-8.3Z\"\n        stroke={color}\n        strokeWidth=\"1.6\"\n        strokeLinejoin=\"round\"\n      />\n    </g>\n  </svg>\n);\n\n/** The creator's heart: a small avatar ring with a filled heart sitting on it. */\nconst Heart: React.FC<{ color: string; ring: string }> = ({ color, ring }) => (\n  <svg width=\"34\" height=\"34\" viewBox=\"0 0 24 24\">\n    <circle cx=\"12\" cy=\"12\" r=\"11\" fill={ring} stroke={color} strokeWidth=\"1.2\" opacity=\"0.35\" />\n    <path\n      d=\"M12 19s-6.2-3.8-6.2-8A3.6 3.6 0 0 1 12 8.6 3.6 3.6 0 0 1 18.2 11c0 4.2-6.2 8-6.2 8Z\"\n      fill={color}\n    />\n  </svg>\n);\n\nexport default defineMotionItem({\n  id: \"youtube-comment-highlight\",\n  name: \"YouTube Comment Highlight\",\n  category: \"youtube\",\n  summary:\n    \"A viewer's comment featured as the platform renders it, with the like row and the creator's heart landing after the words\",\n  tags: [\"youtube\", \"comment\", \"social proof\", \"testimonial\", \"card\", \"ui\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: youtubeCommentHighlightSchema,\n  defaults: {\n    handle: \"@marlowe.builds\",\n    timeAgo: \"2 days ago\",\n    comment:\n      \"Watched this twice. The bit at 4:12 finally made the whole thing click — I had been doing it backwards for about a year.\",\n    likes: \"1.2K\",\n    replies: \"18 replies\",\n    initials: \"MB\",\n    avatarColor: \"\",\n    hearted: true,\n    brand: \"paper\",\n  },\n  component: YoutubeCommentHighlight,\n  file: \"src/registry/items/youtube-comment-highlight.tsx\",\n  origin: {\n    prompt:\n      \"A YouTube-style comment card for featuring a viewer's reply: drawn avatar, @handle and relative timestamp, the comment set large, then a like row with thumb icons, a count and a Reply link. The card rises in as a unit; the handle and comment land first and the like row fades up after them, so the words are read before the furniture arrives. The creator's heart springs on last with a small overshoot.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A YouTube-style comment card for featuring a viewer's reply: drawn avatar, @handle and relative timestamp, the comment set large, then a like row with thumb icons, a count and a Reply link. The card rises in as a unit; the handle and comment land first and the like row fades up after them, so the words are read before the furniture arrives. The creator's heart springs on last with a small overshoot.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "light",
      "score": 0.2867,
      "props": [
        {
          "name": "handle",
          "type": "string",
          "description": "The commenter's @handle. Include the @ — without it the card reads as a generic quote rather than as a comment.",
          "required": true,
          "defaultValue": "@marlowe.builds",
          "options": null
        },
        {
          "name": "timeAgo",
          "type": "string",
          "description": "Relative timestamp beside the handle, exactly as the platform words it.",
          "required": false,
          "defaultValue": "2 days ago",
          "options": null
        },
        {
          "name": "comment",
          "type": "string",
          "description": "The comment itself. Two or three lines is the sweet spot: long enough to feel real, short enough to read before the shot cuts.",
          "required": true,
          "defaultValue": "Watched this twice. The bit at 4:12 finally made the whol…",
          "options": null
        },
        {
          "name": "likes",
          "type": "string",
          "description": "Like count as written text, so 1.2K and 24 both set correctly without number formatting.",
          "required": false,
          "defaultValue": "1.2K",
          "options": null
        },
        {
          "name": "replies",
          "type": "string",
          "description": "Reply count line under the comment — '18 replies'. Empty omits the line.",
          "required": false,
          "defaultValue": "18 replies",
          "options": null
        },
        {
          "name": "initials",
          "type": "string",
          "description": "One or two characters for the drawn avatar. Empty uses the handle's first letter after the @.",
          "required": false,
          "defaultValue": "MB",
          "options": null
        },
        {
          "name": "avatarColor",
          "type": "string",
          "description": "Avatar fill. Empty uses the brand accent, which keeps the card inside the video's palette.",
          "required": false,
          "defaultValue": "",
          "options": null
        },
        {
          "name": "hearted",
          "type": "boolean",
          "description": "Draw the creator's heart on the like row, landing as the last beat. The reason to feature a comment is usually that you replied to it.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand preset. Light presets read as the platform's light mode, dark ones as its dark mode.",
          "required": false,
          "defaultValue": "paper",
          "options": null
        }
      ]
    },
    {
      "id": "youtube-end-card",
      "name": "YouTube End Card",
      "category": "youtube",
      "categoryLabel": "YouTube",
      "summary": "An end screen with two recommended slots and a channel ring, laid out clear of the player's own controls",
      "tags": [
        "youtube",
        "end card",
        "end screen",
        "call to action",
        "safe area",
        "subscribe"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/youtube-end-card.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\nimport { PLATFORM } from \"../platform\";\n\n/**\n * The last twenty seconds, laid out properly.\n *\n * An end screen is the one part of a video with a job that is not storytelling:\n * it has to hold two clickable slots and a channel ring in positions the viewer\n * already knows, without any of them landing under the platform's own progress\n * bar and controls.\n *\n * So the safe area is declared as a fraction of the frame rather than in pixels,\n * and the bottom inset is deliberately the largest — that is where the scrubber\n * and the timestamp sit, and a recommended-video slot placed there is a slot\n * nobody can click. The slots themselves are drawn placeholders rather than\n * images: this registry ships no asset files, and an end screen's geometry is\n * the part worth getting right anyway.\n */\n\nexport const youtubeEndCardSchema = z.object({\n  headline: z\n    .string()\n    .describe(\"The line across the top. Short and imperative reads best — it competes with two thumbnails for attention.\"),\n  channel: z.string().describe(\"Channel name, set under the ring.\"),\n  handle: z.string().default(\"\").describe(\"The @handle under the channel name. Empty omits it.\"),\n  initials: z\n    .string()\n    .default(\"\")\n    .describe(\"One or two characters for the drawn channel ring. Empty uses the channel's first letter.\"),\n  avatarColor: z.string().default(PLATFORM.youtubeRed).describe(\"Fill inside the channel ring.\"),\n  subscribeLabel: z\n    .string()\n    .default(\"Subscribe\")\n    .describe(\"Button under the channel ring. Empty draws no button.\"),\n  slotLabels: z\n    .array(z.string())\n    .min(1)\n    .max(2)\n    .default([\"Watch next\", \"Most popular\"])\n    .describe(\n      \"Caption under each video slot. Two slots is what the platform actually offers at this size; one is a legitimate choice and gets a wider slot.\",\n    ),\n  slotTitles: z\n    .array(z.string())\n    .default([])\n    .describe(\"Optional title drawn inside each slot, so the end screen reads as designed rather than as empty boxes.\"),\n  safeBottom: z\n    .number()\n    .min(0)\n    .max(0.3)\n    .default(0.14)\n    .describe(\n      \"Fraction of the frame height kept clear at the bottom for the platform's scrubber and controls. Lower it only if you know the player chrome is hidden.\",\n    ),\n  brand: z\n    .string()\n    .default(\"slate\")\n    .describe(\"Brand preset. This is the last frame of the video, so it is usually the one that should look most like the channel.\"),\n});\n\ntype Props = z.infer<typeof youtubeEndCardSchema>;\n\nexport const YoutubeEndCard: React.FC<Props> = ({\n  headline,\n  channel,\n  handle,\n  initials,\n  avatarColor,\n  subscribeLabel,\n  slotLabels,\n  slotTitles,\n  safeBottom,\n  brand,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n  const t = BRANDS[brand] ?? BRANDS.slate;\n\n  const head = useStaggeredStage(BUDGETS.feature, 0, 6);\n  const slotA = useStaggeredStage(BUDGETS.feature, 1, 6);\n  const slotB = useStaggeredStage(BUDGETS.feature, 2, 6);\n  const ring = useStaggeredStage(BUDGETS.feature, 3, 6);\n\n  const u = Math.min(width, height) / 1080;\n  const portrait = height > width;\n\n  // Safe area. The bottom inset is the one that matters and the one people forget.\n  const insetX = width * 0.075;\n  const insetTop = height * 0.11;\n  const insetBottom = height * safeBottom;\n\n  const bodyTop = insetTop + 96 * u;\n  const bodyBottom = height - insetBottom;\n  const bodyH = bodyBottom - bodyTop;\n\n  const pop = spring({ frame: frame - 18, fps, config: { damping: 13, mass: 0.6 } });\n\n  /**\n   * The countdown ring around the channel avatar.\n   *\n   * Not decoration: a real end screen counts down, and that ring is the only\n   * thing on it that moves for its whole duration. Without it this component sat\n   * still through its own hold, which is the one thing the house rules forbid —\n   * and the measurement agreed, scoring it near the bottom of the library.\n   */\n  const countdown = interpolate(\n    frame,\n    [BUDGETS.feature.enter, BUDGETS.feature.enter + BUDGETS.feature.hold],\n    [0, 1],\n    { extrapolateLeft: \"clamp\", extrapolateRight: \"clamp\" },\n  );\n\n  /** A slow drift on everything, so the frame is never locked off. */\n  const drift = Math.sin((frame / fps) * 0.42) * 4 * u;\n  const letter = (initials || channel.trim().charAt(0) || \"?\").slice(0, 2).toUpperCase();\n\n  const slots = slotLabels.slice(0, 2);\n  const stages = [slotA, slotB];\n\n  /**\n   * Portrait stacks; landscape sits the slots beside the channel ring. Same\n   * component, two arrangements — an end screen that keeps a 16:9 row in a\n   * vertical frame ends up with slots too small to hit.\n   */\n  const ringSize = (portrait ? 200 : 230) * u;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg }}>\n      <AbsoluteFill style={{ opacity: stage.presence }}>\n        {/* headline */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: insetX,\n            top: insetTop,\n            right: insetX,\n            fontFamily: t.fontDisplay,\n            fontWeight: 700,\n            fontSize: 58 * u,\n            letterSpacing: \"-0.025em\",\n            color: t.ink,\n            opacity: head.enter,\n            transform: `translateY(${(1 - head.enter) * 18 + drift * 0.5}px)`,\n          }}\n        >\n          {headline}\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: insetX,\n            right: insetX,\n            top: bodyTop,\n            height: bodyH,\n            transform: `translateY(${drift}px)`,\n            display: \"flex\",\n            flexDirection: portrait ? \"column\" : \"row\",\n            alignItems: \"center\",\n            gap: 40 * u,\n          }}\n        >\n          {/* recommended slots */}\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: portrait ? \"column\" : \"row\",\n              gap: 34 * u,\n              flex: 1,\n              width: \"100%\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n            }}\n          >\n            {slots.map((label, i) => {\n              const st = stages[i]?.enter ?? 1;\n              return (\n                <div\n                  key={label + i}\n                  style={{\n                    flex: 1,\n                    maxWidth: portrait ? \"100%\" : undefined,\n                    width: \"100%\",\n                    opacity: st,\n                    transform: `translateY(${(1 - st) * 26}px)`,\n                  }}\n                >\n                  <div\n                    style={{\n                      width: \"100%\",\n                      aspectRatio: \"16 / 9\",\n                      borderRadius: 18 * u,\n                      backgroundColor: t.surface,\n                      border: `${Math.max(2, 3 * u)}px solid ${t.rule}`,\n                      display: \"flex\",\n                      alignItems: \"center\",\n                      justifyContent: \"center\",\n                      padding: 28 * u,\n                      position: \"relative\",\n                      overflow: \"hidden\",\n                    }}\n                  >\n                    <PlayGlyph size={70 * u} color={t.inkMuted} />\n                    {slotTitles[i] ? (\n                      <div\n                        style={{\n                          position: \"absolute\",\n                          left: 26 * u,\n                          right: 26 * u,\n                          bottom: 22 * u,\n                          fontFamily: t.fontDisplay,\n                          fontWeight: 600,\n                          fontSize: 27 * u,\n                          lineHeight: 1.25,\n                          color: t.ink,\n                        }}\n                      >\n                        {slotTitles[i]}\n                      </div>\n                    ) : null}\n                  </div>\n                  <div\n                    style={{\n                      marginTop: 14 * u,\n                      fontFamily: t.fontBody,\n                      fontWeight: 500,\n                      fontSize: 25 * u,\n                      color: t.inkMuted,\n                    }}\n                  >\n                    {label}\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n\n          {/* channel ring */}\n          <div\n            style={{\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"center\",\n              gap: 16 * u,\n              opacity: ring.enter,\n              transform: `scale(${interpolate(Math.max(0, pop), [0, 1], [0.9, 1])})`,\n              flexShrink: 0,\n            }}\n          >\n            <div style={{ position: \"relative\", width: ringSize, height: ringSize }}>\n              <svg\n                width={ringSize}\n                height={ringSize}\n                viewBox=\"0 0 100 100\"\n                style={{ position: \"absolute\", inset: 0, transform: \"rotate(-90deg)\" }}\n              >\n                <circle cx=\"50\" cy=\"50\" r=\"47\" fill=\"none\" stroke={t.rule} strokeWidth=\"3.2\" />\n                <circle\n                  cx=\"50\"\n                  cy=\"50\"\n                  r=\"47\"\n                  fill=\"none\"\n                  stroke={avatarColor}\n                  strokeWidth=\"3.2\"\n                  strokeLinecap=\"round\"\n                  strokeDasharray={`${2 * Math.PI * 47}`}\n                  strokeDashoffset={`${2 * Math.PI * 47 * countdown}`}\n                />\n              </svg>\n            <div\n              style={{\n                position: \"absolute\",\n                inset: ringSize * 0.075,\n                borderRadius: \"50%\",\n                backgroundColor: avatarColor,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                color: \"#FFFFFF\",\n                fontFamily: t.fontDisplay,\n                fontWeight: 700,\n                fontSize: ringSize * 0.33,\n              }}\n            >\n              {letter}\n            </div>\n            </div>\n            <div\n              style={{\n                fontFamily: t.fontDisplay,\n                fontWeight: 600,\n                fontSize: 32 * u,\n                color: t.ink,\n                textAlign: \"center\",\n              }}\n            >\n              {channel}\n            </div>\n            {handle ? (\n              <div style={{ fontFamily: t.fontBody, fontSize: 24 * u, color: t.inkMuted }}>\n                {handle}\n              </div>\n            ) : null}\n            {subscribeLabel ? (\n              <div\n                style={{\n                  marginTop: 6 * u,\n                  padding: `${13 * u}px ${34 * u}px`,\n                  borderRadius: 40 * u,\n                  backgroundColor: PLATFORM.youtubeRed,\n                  color: \"#FFFFFF\",\n                  fontFamily: t.fontDisplay,\n                  fontWeight: 700,\n                  fontSize: 27 * u,\n                }}\n              >\n                {subscribeLabel}\n              </div>\n            ) : null}\n          </div>\n        </div>\n\n        {/* the reserved strip, drawn faintly so the safe area is visible in preview */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            right: 0,\n            bottom: 0,\n            height: insetBottom,\n            background: `linear-gradient(to top, ${t.bg}, transparent)`,\n            opacity: 0.9,\n          }}\n        />\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\nconst PlayGlyph: React.FC<{ size: number; color: string }> = ({ size, color }) => (\n  <svg width={size} height={size} viewBox=\"0 0 24 24\" fill=\"none\" opacity={0.55}>\n    <circle cx=\"12\" cy=\"12\" r=\"10.2\" stroke={color} strokeWidth=\"1.4\" />\n    <path d=\"M10 8.2l6 3.8-6 3.8V8.2Z\" fill={color} />\n  </svg>\n);\n\nexport default defineMotionItem({\n  id: \"youtube-end-card\",\n  name: \"YouTube End Card\",\n  category: \"youtube\",\n  summary:\n    \"An end screen with two recommended slots and a channel ring, laid out clear of the player's own controls\",\n  tags: [\"youtube\", \"end card\", \"end screen\", \"call to action\", \"safe area\", \"subscribe\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: youtubeEndCardSchema,\n  defaults: {\n    headline: \"Keep going — two more on this\",\n    channel: \"Northlane Works\",\n    handle: \"@northlaneworks\",\n    initials: \"NW\",\n    avatarColor: PLATFORM.youtubeRed,\n    subscribeLabel: \"Subscribe\",\n    slotLabels: [\"Watch next\", \"Most popular\"],\n    slotTitles: [\"The setup that finally stuck\", \"Everything I got wrong first\"],\n    safeBottom: 0.14,\n    brand: \"slate\",\n  },\n  component: YoutubeEndCard,\n  file: \"src/registry/items/youtube-end-card.tsx\",\n  origin: {\n    prompt:\n      \"A YouTube end screen: a headline across the top, two 16:9 recommended-video slots with captions, and a circular channel ring with name, handle and a red subscribe button. Everything sits inside a declared safe area whose bottom inset is the largest, because that is where the scrubber and controls sit. The headline lands first, then each slot in turn, then the ring springs in. In a vertical frame the slots stack above the ring instead of sitting beside it.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A YouTube end screen: a headline across the top, two 16:9 recommended-video slots with captions, and a circular channel ring with name, handle and a red subscribe button. Everything sits inside a declared safe area whose bottom inset is the largest, because that is where the scrubber and controls sit. The headline lands first, then each slot in turn, then the ring springs in. In a vertical frame the slots stack above the ring instead of sitting beside it.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.4496,
      "props": [
        {
          "name": "headline",
          "type": "string",
          "description": "The line across the top. Short and imperative reads best — it competes with two thumbnails for attention.",
          "required": true,
          "defaultValue": "Keep going — two more on this",
          "options": null
        },
        {
          "name": "channel",
          "type": "string",
          "description": "Channel name, set under the ring.",
          "required": true,
          "defaultValue": "Northlane Works",
          "options": null
        },
        {
          "name": "handle",
          "type": "string",
          "description": "The @handle under the channel name. Empty omits it.",
          "required": false,
          "defaultValue": "@northlaneworks",
          "options": null
        },
        {
          "name": "initials",
          "type": "string",
          "description": "One or two characters for the drawn channel ring. Empty uses the channel's first letter.",
          "required": false,
          "defaultValue": "NW",
          "options": null
        },
        {
          "name": "avatarColor",
          "type": "string",
          "description": "Fill inside the channel ring.",
          "required": false,
          "defaultValue": "#FF0033",
          "options": null
        },
        {
          "name": "subscribeLabel",
          "type": "string",
          "description": "Button under the channel ring. Empty draws no button.",
          "required": false,
          "defaultValue": "Subscribe",
          "options": null
        },
        {
          "name": "slotLabels",
          "type": "array",
          "description": "Caption under each video slot. Two slots is what the platform actually offers at this size; one is a legitimate choice and gets a wider slot.",
          "required": false,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "slotTitles",
          "type": "array",
          "description": "Optional title drawn inside each slot, so the end screen reads as designed rather than as empty boxes.",
          "required": false,
          "defaultValue": "2 items",
          "options": null
        },
        {
          "name": "safeBottom",
          "type": "number",
          "description": "Fraction of the frame height kept clear at the bottom for the platform's scrubber and controls. Lower it only if you know the player chrome is hidden.",
          "required": false,
          "defaultValue": "0.14",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand preset. This is the last frame of the video, so it is usually the one that should look most like the channel.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "youtube-subscribe-nudge",
      "name": "YouTube Subscribe Nudge",
      "category": "youtube",
      "categoryLabel": "YouTube",
      "summary": "A channel card that presses its own subscribe button and then rings its own bell, with a cursor that travels to each in turn",
      "tags": [
        "youtube",
        "subscribe",
        "call to action",
        "cursor",
        "state change",
        "bell"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/youtube-subscribe-nudge.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, spring, useCurrentFrame, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS } from \"../tokens\";\nimport { PLATFORM } from \"../platform\";\n\n/**\n * The ask, animated: a channel card that presses its own subscribe button and\n * then rings its own bell, with a cursor that goes to each in turn.\n *\n * The whole point is the state change. A static \"please subscribe\" graphic asks\n * for something; this one shows the thing happening, which is why the button\n * flip and the bell ring are separate beats with a gap between them rather than\n * one combined move — the viewer needs to read the first before the second\n * starts, or neither registers.\n *\n * The card is drawn at a fixed design width and then scaled to whatever frame it\n * is given, so the layout never reflows. That is deliberate: this is a piece of\n * chrome quoting a real interface, and interfaces do not re-wrap when you put\n * them in a vertical video — they get smaller.\n */\n\nconst CARD_W = 1120;\nconst CARD_H = 300;\n\nexport const youtubeSubscribeNudgeSchema = z.object({\n  channel: z.string().describe(\"Channel name, set large. Two or three words reads best at this size.\"),\n  handle: z\n    .string()\n    .describe(\"The @handle under the name. Include the @ — it is what makes the card read as the platform rather than as a generic card.\"),\n  initials: z\n    .string()\n    .default(\"\")\n    .describe(\n      \"One or two characters drawn in the avatar ring. Empty falls back to the channel's first letter. This registry ships no image files, so the avatar is drawn rather than loaded.\",\n    ),\n  avatarColor: z\n    .string()\n    .default(PLATFORM.youtubeRed)\n    .describe(\"Fill behind the initials. The channel's own colour is the point of it.\"),\n  subscribeLabel: z.string().default(\"Subscribe\").describe(\"Button text before the press.\"),\n  subscribedLabel: z.string().default(\"Subscribed\").describe(\"Button text after it, shown with a tick.\"),\n  buttonColor: z\n    .string()\n    .default(PLATFORM.youtubeRed)\n    .describe(\"The unpressed button fill. Turns to a muted surface once subscribed, the way the real control does.\"),\n  showCursor: z\n    .boolean()\n    .default(true)\n    .describe(\"Draw the pointer that travels to the button and then to the bell. Turn it off for a calmer card that still changes state.\"),\n  brand: z\n    .string()\n    .default(\"circuit\")\n    .describe(\"Brand preset. Dark presets read as the platform's dark mode; light ones as its light mode.\"),\n});\n\ntype Props = z.infer<typeof youtubeSubscribeNudgeSchema>;\n\nexport const YoutubeSubscribeNudge: React.FC<Props> = ({\n  channel,\n  handle,\n  initials,\n  avatarColor,\n  subscribeLabel,\n  subscribedLabel,\n  buttonColor,\n  showCursor,\n  brand,\n}) => {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const stage = useStage(BUDGETS.scene);\n  const t = BRANDS[brand] ?? BRANDS.circuit;\n\n  // One scale for the whole card: quote the interface, never re-wrap it.\n  const scale = Math.min(width / (CARD_W + 160), height / (CARD_H + 160), 1.35);\n\n  /* ---------------------------------------------------------------- beats */\n  const PRESS = 46; // the cursor arrives and the button is clicked\n  const FLIP = PRESS + 6; // subscribe -> subscribed\n  const BELL = 84; // the bell is rung\n\n  const press = spring({ frame: frame - PRESS, fps, config: { damping: 12, mass: 0.5 } });\n  // Down then back: a press is a dip, not a grow.\n  const pressScale = 1 - 0.06 * Math.sin(Math.PI * Math.min(1, Math.max(0, press)));\n\n  const flip = interpolate(frame, [FLIP, FLIP + 7], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const ring = spring({ frame: frame - BELL, fps, config: { damping: 9, mass: 0.4 } });\n  // Three decaying oscillations, which is what a bell shake looks like.\n  const bellTilt =\n    frame < BELL ? 0 : Math.sin((frame - BELL) * 0.75) * 16 * Math.exp(-(frame - BELL) / 14);\n  const bellPulse = 1 + 0.16 * Math.max(0, ring) * Math.exp(-(frame - BELL) / 12);\n\n  /* --------------------------------------------------------------- cursor */\n  const cursorFrom = { x: CARD_W * 0.98, y: CARD_H * 1.02 };\n  const cursorBtn = { x: CARD_W * 0.665, y: CARD_H * 0.56 };\n  const cursorBell = { x: CARD_W * 0.876, y: CARD_H * 0.53 };\n\n  const toBtn = interpolate(frame, [16, PRESS], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const toBell = interpolate(frame, [PRESS + 14, BELL], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n  const cx =\n    cursorFrom.x + (cursorBtn.x - cursorFrom.x) * toBtn + (cursorBell.x - cursorBtn.x) * toBell;\n  const cy =\n    cursorFrom.y + (cursorBtn.y - cursorFrom.y) * toBtn + (cursorBell.y - cursorBtn.y) * toBell;\n\n  /* ---------------------------------------------------------------- card */\n  const rise = interpolate(stage.enter, [0, 1], [34, 0]);\n  const cardScale = interpolate(stage.enter, [0, 1], [0.975, 1]);\n  const letter = (initials || channel.trim().charAt(0) || \"?\").slice(0, 2).toUpperCase();\n\n  // Derived from the brand's ink rather than picked per theme: one expression\n  // that lands correctly on a pale ground and a dark one.\n  const subscribedFill = alpha(t.ink, 0.12);\n  const bellWell = alpha(t.ink, 0.08);\n\n  return (\n    <AbsoluteFill\n      style={{\n        backgroundColor: t.bg,\n        alignItems: \"center\",\n        justifyContent: \"center\",\n      }}\n    >\n      <div\n        style={{\n          width: CARD_W,\n          height: CARD_H,\n          transform: `scale(${scale * cardScale}) translateY(${rise}px)`,\n          opacity: stage.presence,\n          backgroundColor: t.surface,\n          border: `2px solid ${t.rule}`,\n          borderRadius: 28,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: 34,\n          padding: \"0 44px\",\n          position: \"relative\",\n          boxShadow: `0 40px 90px ${brandIsDark(t.bg) ? \"rgba(0,0,0,0.55)\" : \"rgba(15,20,30,0.14)\"}`,\n        }}\n      >\n        {/* avatar */}\n        <div\n          style={{\n            width: 132,\n            height: 132,\n            borderRadius: \"50%\",\n            backgroundColor: avatarColor,\n            border: `4px solid ${t.surface}`,\n            flexShrink: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            color: \"#FFFFFF\",\n            fontFamily: t.fontDisplay,\n            fontWeight: 700,\n            fontSize: 52,\n            letterSpacing: \"-0.02em\",\n          }}\n        >\n          {letter}\n        </div>\n\n        {/* name + handle */}\n        <div style={{ flex: 1, minWidth: 0 }}>\n          <div\n            style={{\n              fontFamily: t.fontDisplay,\n              fontWeight: 700,\n              fontSize: 46,\n              color: t.ink,\n              letterSpacing: \"-0.02em\",\n              whiteSpace: \"nowrap\",\n              overflow: \"hidden\",\n              textOverflow: \"ellipsis\",\n            }}\n          >\n            {channel}\n          </div>\n          <div\n            style={{\n              marginTop: 6,\n              fontFamily: t.fontBody,\n              fontWeight: 500,\n              fontSize: 28,\n              color: t.inkMuted,\n            }}\n          >\n            {handle}\n          </div>\n        </div>\n\n        {/* the button, mid-flip */}\n        <div\n          style={{\n            position: \"relative\",\n            width: 280,\n            height: 84,\n            flexShrink: 0,\n            transform: `scale(${pressScale})`,\n          }}\n        >\n          <Pill\n            fill={buttonColor}\n            color=\"#FFFFFF\"\n            font={t.fontDisplay}\n            label={subscribeLabel}\n            opacity={1 - flip}\n          />\n          <Pill\n            fill={subscribedFill}\n            color={t.ink}\n            font={t.fontDisplay}\n            label={subscribedLabel}\n            opacity={flip}\n            tick\n          />\n        </div>\n\n        {/* bell */}\n        <div\n          style={{\n            width: 92,\n            height: 92,\n            borderRadius: \"50%\",\n            backgroundColor: bellWell,\n            flexShrink: 0,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"center\",\n            transform: `scale(${bellPulse}) rotate(${bellTilt}deg)`,\n          }}\n        >\n          <Bell color={t.ink} ringing={frame > BELL} />\n        </div>\n\n        {showCursor ? <Cursor x={cx} y={cy} pressed={frame >= PRESS && frame < PRESS + 8} /> : null}\n      </div>\n    </AbsoluteFill>\n  );\n};\n\nconst Pill: React.FC<{\n  fill: string;\n  color: string;\n  font: string;\n  label: string;\n  opacity: number;\n  tick?: boolean;\n}> = ({ fill, color, font, label, opacity, tick }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      inset: 0,\n      borderRadius: 42,\n      backgroundColor: fill,\n      opacity,\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      gap: 12,\n      fontFamily: font,\n      fontWeight: 700,\n      fontSize: 30,\n      color,\n      letterSpacing: \"-0.01em\",\n    }}\n  >\n    {tick ? (\n      <svg width=\"26\" height=\"26\" viewBox=\"0 0 24 24\" fill=\"none\">\n        <path\n          d=\"M4 12.5l5.2 5.2L20 7\"\n          stroke={color}\n          strokeWidth=\"2.6\"\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n        />\n      </svg>\n    ) : null}\n    {label}\n  </div>\n);\n\n/** Outline before the ring, filled with a notification wave after it. */\nconst Bell: React.FC<{ color: string; ringing: boolean }> = ({ color, ringing }) => (\n  <svg width=\"44\" height=\"44\" viewBox=\"0 0 24 24\" fill=\"none\">\n    <path\n      d=\"M12 3.5a5.5 5.5 0 0 0-5.5 5.5v3.2L5 15.2h14l-1.5-3V9A5.5 5.5 0 0 0 12 3.5Z\"\n      stroke={color}\n      strokeWidth=\"1.7\"\n      strokeLinejoin=\"round\"\n      fill={ringing ? color : \"none\"}\n      fillOpacity={ringing ? 0.9 : 0}\n    />\n    <path d=\"M10 18a2 2 0 0 0 4 0\" stroke={color} strokeWidth=\"1.7\" strokeLinecap=\"round\" />\n  </svg>\n);\n\nconst Cursor: React.FC<{ x: number; y: number; pressed: boolean }> = ({ x, y, pressed }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      left: x,\n      top: y,\n      transform: `scale(${pressed ? 0.86 : 1})`,\n      transformOrigin: \"top left\",\n      pointerEvents: \"none\",\n    }}\n  >\n    <svg width=\"38\" height=\"38\" viewBox=\"0 0 24 24\">\n      <path\n        d=\"M5 3l14 8.2-6.1 1.3L10 19.4 5 3z\"\n        fill=\"#FFFFFF\"\n        stroke=\"#000000\"\n        strokeWidth=\"1.4\"\n        strokeLinejoin=\"round\"\n      />\n    </svg>\n  </div>\n);\n\n/** A brand colour at partial strength, for neutrals that must work on both grounds. */\nfunction alpha(hex: string, a: number) {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h;\n  const n = parseInt(full, 16);\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;\n}\n\n/** Mean luminance of the brand ground, so the card's neutrals sit on the right side of it. */\nfunction brandIsDark(hex: string) {\n  const h = hex.replace(\"#\", \"\");\n  const n = parseInt(h.length === 3 ? h.split(\"\").map((c) => c + c).join(\"\") : h, 16);\n  const r = (n >> 16) & 255;\n  const g = (n >> 8) & 255;\n  const b = n & 255;\n  return 0.2126 * r + 0.7152 * g + 0.0722 * b < 128;\n}\n\nexport default defineMotionItem({\n  id: \"youtube-subscribe-nudge\",\n  name: \"YouTube Subscribe Nudge\",\n  category: \"youtube\",\n  summary:\n    \"A channel card that presses its own subscribe button and then rings its own bell, with a cursor that travels to each in turn\",\n  tags: [\"youtube\", \"subscribe\", \"call to action\", \"cursor\", \"state change\", \"bell\"],\n  budget: BUDGETS.scene,\n  aspect: \"16:9\",\n  schema: youtubeSubscribeNudgeSchema,\n  defaults: {\n    channel: \"Northlane Works\",\n    handle: \"@northlaneworks\",\n    initials: \"NW\",\n    avatarColor: PLATFORM.youtubeRed,\n    subscribeLabel: \"Subscribe\",\n    subscribedLabel: \"Subscribed\",\n    buttonColor: PLATFORM.youtubeRed,\n    showCursor: true,\n    brand: \"circuit\",\n  },\n  component: YoutubeSubscribeNudge,\n  file: \"src/registry/items/youtube-subscribe-nudge.tsx\",\n  origin: {\n    prompt:\n      \"A subscribe prompt that performs the subscribe: a channel card with a drawn avatar, name and handle, a red subscribe button and a bell. A pointer travels in from the corner, presses the button — which dips and springs back — and the label flips to a ticked 'Subscribed' on a muted fill. After a beat the pointer moves to the bell, which pulses and shakes through three decaying oscillations and fills in. Two separate beats with a gap, so each state change is read before the next begins.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A subscribe prompt that performs the subscribe: a channel card with a drawn avatar, name and handle, a red subscribe button and a bell. A pointer travels in from the corner, presses the button — which dips and springs back — and the label flips to a ticked 'Subscribed' on a muted fill. After a beat the pointer moves to the bell, which pulses and shakes through three decaying oscillations and fills in. Two separate beats with a gap, so each state change is read before the next begins.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.3716,
      "props": [
        {
          "name": "channel",
          "type": "string",
          "description": "Channel name, set large. Two or three words reads best at this size.",
          "required": true,
          "defaultValue": "Northlane Works",
          "options": null
        },
        {
          "name": "handle",
          "type": "string",
          "description": "The @handle under the name. Include the @ — it is what makes the card read as the platform rather than as a generic card.",
          "required": true,
          "defaultValue": "@northlaneworks",
          "options": null
        },
        {
          "name": "initials",
          "type": "string",
          "description": "One or two characters drawn in the avatar ring. Empty falls back to the channel's first letter. This registry ships no image files, so the avatar is drawn rather than loaded.",
          "required": false,
          "defaultValue": "NW",
          "options": null
        },
        {
          "name": "avatarColor",
          "type": "string",
          "description": "Fill behind the initials. The channel's own colour is the point of it.",
          "required": false,
          "defaultValue": "#FF0033",
          "options": null
        },
        {
          "name": "subscribeLabel",
          "type": "string",
          "description": "Button text before the press.",
          "required": false,
          "defaultValue": "Subscribe",
          "options": null
        },
        {
          "name": "subscribedLabel",
          "type": "string",
          "description": "Button text after it, shown with a tick.",
          "required": false,
          "defaultValue": "Subscribed",
          "options": null
        },
        {
          "name": "buttonColor",
          "type": "string",
          "description": "The unpressed button fill. Turns to a muted surface once subscribed, the way the real control does.",
          "required": false,
          "defaultValue": "#FF0033",
          "options": null
        },
        {
          "name": "showCursor",
          "type": "boolean",
          "description": "Draw the pointer that travels to the button and then to the bell. Turn it off for a calmer card that still changes state.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand preset. Dark presets read as the platform's dark mode; light ones as its light mode.",
          "required": false,
          "defaultValue": "circuit",
          "options": null
        }
      ]
    },
    {
      "id": "zoom-punch-cut",
      "name": "Zoom Punch Cut",
      "category": "transition",
      "categoryLabel": "Transitions",
      "summary": "A fast accelerating push-in carries a hard frame-exact cut, with the incoming scene decelerating out of an over-scaled state",
      "tags": [
        "transition",
        "cut",
        "beat",
        "punch",
        "blur"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 240,
      "durationSeconds": 8,
      "budget": {
        "enter": 20,
        "hold": 200,
        "exit": 20
      },
      "file": "src/registry/items/zoom-punch-cut.tsx",
      "source": "import React from \"react\";\nimport { AbsoluteFill, interpolate, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND, type BrandTokens } from \"../tokens\";\n\nexport const zoomPunchCutSchema = z.object({\n  first: z.string().default(\"Slow\").describe(\"Headline of the outgoing scene.\"),\n  second: z.string().default(\"Fast\").describe(\"Headline of the incoming scene.\"),\n  cutAt: z\n    .number()\n    .min(24)\n    .max(160)\n    .default(72)\n    .describe(\"Exact frame the cut lands on. Put it on the beat.\"),\n  punch: z\n    .number()\n    .min(1.1)\n    .max(2.4)\n    .default(1.55)\n    .describe(\"How far the outgoing scene pushes in before the cut.\"),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof zoomPunchCutSchema>;\n\n/**\n * A percussive cut. Two rules make it work and both are easy to get wrong.\n *\n * First, the cut is a HARD frame boundary — no cross-fade, no dissolve. Softening\n * it by even two frames turns a hit into a smear. Second, the outgoing push-in\n * accelerates while the incoming settle decelerates, so energy peaks exactly on\n * the cut instead of either side of it. The blur is derived from the scale rate,\n * not animated separately, which is why it disappears precisely as motion stops.\n */\nconst ZoomPunchCut: React.FC<Props> = ({ first, second, cutAt, punch, brand }) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width } = useVideoConfig();\n  const stage = useStage(BUDGETS.feature);\n  const f = stage.frame;\n\n  const runUp = 18;\n  const settle = 20;\n\n  const outScale = interpolate(f, [cutAt - runUp, cutAt], [1, punch], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n  const outBlur = interpolate(f, [cutAt - runUp, cutAt], [0, width * 0.006], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.accent,\n  });\n\n  const inScale = interpolate(f, [cutAt, cutAt + settle], [punch * 0.82, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const inBlur = interpolate(f, [cutAt, cutAt + settle * 0.6], [width * 0.004, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  const afterCut = f >= cutAt;\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, opacity: stage.presence, overflow: \"hidden\" }}>\n      {afterCut ? (\n        <AbsoluteFill\n          style={{ transform: `scale(${inScale})`, filter: `blur(${inBlur}px)` }}\n        >\n          <Panel headline={second} tokens={t} width={width} accent />\n        </AbsoluteFill>\n      ) : (\n        <AbsoluteFill\n          style={{ transform: `scale(${outScale})`, filter: `blur(${outBlur}px)` }}\n        >\n          <Panel headline={first} tokens={t} width={width} accent={false} />\n        </AbsoluteFill>\n      )}\n    </AbsoluteFill>\n  );\n};\n\nconst Panel: React.FC<{\n  headline: string;\n  tokens: BrandTokens;\n  width: number;\n  accent: boolean;\n}> = ({ headline, tokens, width, accent }) => (\n  <AbsoluteFill\n    style={{\n      backgroundColor: accent ? tokens.surface : tokens.bg,\n      justifyContent: \"center\",\n      alignItems: \"center\",\n    }}\n  >\n    <div\n      style={{\n        fontFamily: tokens.fontDisplay,\n        fontSize: width * 0.12,\n        fontWeight: 700,\n        lineHeight: 1,\n        letterSpacing: width * -0.004,\n        textTransform: \"uppercase\",\n        color: accent ? tokens.accent : tokens.ink,\n      }}\n    >\n      {headline}\n    </div>\n    <div\n      style={{\n        marginTop: width * 0.016,\n        fontFamily: tokens.fontMono,\n        fontSize: width * 0.013,\n        letterSpacing: width * 0.0024,\n        textTransform: \"uppercase\",\n        color: tokens.inkMuted,\n      }}\n    >\n      {accent ? \"after the cut\" : \"before the cut\"}\n    </div>\n  </AbsoluteFill>\n);\n\nexport default defineMotionItem({\n  id: \"zoom-punch-cut\",\n  name: \"Zoom Punch Cut\",\n  category: \"transition\",\n  summary:\n    \"A fast accelerating push-in carries a hard frame-exact cut, with the incoming scene decelerating out of an over-scaled state\",\n  tags: [\"transition\", \"cut\", \"beat\", \"punch\", \"blur\"],\n  budget: BUDGETS.feature,\n  aspect: \"16:9\",\n  schema: zoomPunchCutSchema,\n  defaults: { first: \"Slow\", second: \"Fast\", cutAt: 72, punch: 1.55, brand: \"slate\" },\n  component: ZoomPunchCut,\n  file: \"src/registry/items/zoom-punch-cut.tsx\",\n  origin: {\n    prompt:\n      \"A beat-driven transition: the first scene pushes in fast with increasing blur, hard cuts on the peak frame, and the second scene settles back from slightly over-scaled.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "A beat-driven transition: the first scene pushes in fast with increasing blur, hard cuts on the peak frame, and the second scene settles back from slightly over-scaled.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2335,
      "props": [
        {
          "name": "first",
          "type": "string",
          "description": "Headline of the outgoing scene.",
          "required": false,
          "defaultValue": "Slow",
          "options": null
        },
        {
          "name": "second",
          "type": "string",
          "description": "Headline of the incoming scene.",
          "required": false,
          "defaultValue": "Fast",
          "options": null
        },
        {
          "name": "cutAt",
          "type": "number",
          "description": "Exact frame the cut lands on. Put it on the beat.",
          "required": false,
          "defaultValue": "72",
          "options": null
        },
        {
          "name": "punch",
          "type": "number",
          "description": "How far the outgoing scene pushes in before the cut.",
          "required": false,
          "defaultValue": "1.55",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    },
    {
      "id": "zoom-to-region",
      "name": "Zoom To Region",
      "category": "product",
      "categoryLabel": "Product & UI",
      "summary": "A synthesised dashboard, then a keyframed camera push into one named region and back out, with an annotation ring on the detail",
      "tags": [
        "camera",
        "pan and zoom",
        "product shot",
        "dashboard",
        "ui mock",
        "callout",
        "annotation"
      ],
      "aspect": "16:9",
      "width": 1920,
      "height": 1080,
      "fps": 30,
      "durationInFrames": 150,
      "durationSeconds": 5,
      "budget": {
        "enter": 18,
        "hold": 114,
        "exit": 18
      },
      "file": "src/registry/items/zoom-to-region.tsx",
      "source": "import React from \"react\";\nimport { evolvePath } from \"@remotion/paths\";\nimport { makeCircle } from \"@remotion/shapes\";\nimport { AbsoluteFill, interpolate, random, useVideoConfig } from \"remotion\";\nimport { z } from \"zod\";\nimport { defineMotionItem } from \"../contract\";\nimport { BUDGETS, EASING, useStage, useStaggeredStage } from \"../timing\";\nimport { BRANDS, DEFAULT_BRAND } from \"../tokens\";\nimport type { BrandTokens } from \"../tokens\";\n\nconst BUDGET = BUDGETS.scene;\n\n/* ---------------------------------------------------------------- schedule */\n\n/**\n * Assembly order of the dashboard underneath the camera. Five groups on a\n * two-frame step, so the whole screen builds itself in one gesture and is\n * finished well before the camera starts to move.\n */\nconst PIECE = { header: 0, chart: 1, kpi: 2, tiles: 3, foot: 4 } as const;\nconst PIECE_STEP = 2;\n\n/** The frame by which nothing in the dashboard is moving any more. */\nconst ASSEMBLED = BUDGET.enter + PIECE.foot * PIECE_STEP;\n\n/** First frame of the exit window. The camera must be home before this. */\nconst EXIT_START = BUDGET.enter + BUDGET.hold;\n\n/*\n * The camera's four keyframes, all derived from the budget rather than typed as\n * loose numbers — change the budget and the move still lands where it should.\n *\n * Reticle up, push in, park at the detail long enough to read the annotation,\n * pull out. `PULL[1]` sits two frames short of `EXIT_START`, which is the whole\n * discipline of this item: the move lives entirely inside the hold, so the\n * component is at its wide rest position on the first frame of its exit and the\n * handoff contract sees an ordinary cross-fade rather than a moving camera.\n */\nconst RETICLE: [number, number] = [ASSEMBLED - 6, ASSEMBLED + 2];\nconst PUSH: [number, number] = [ASSEMBLED + 8, ASSEMBLED + 34];\nconst PULL: [number, number] = [EXIT_START - 20, EXIT_START - 2];\n\n/** Annotation beats, hung off the frame the push lands on. */\nconst RING: [number, number] = [PUSH[1] + 2, PUSH[1] + 16];\nconst LABEL: [number, number] = [PUSH[1] + 6, PUSH[1] + 18];\nconst FADE: [number, number] = [PULL[0] - 8, PULL[0]];\n\n/* ---------------------------------------------------------------- geometry */\n\ntype Rect = { x: number; y: number; w: number; h: number };\n\n/**\n * The dashboard, laid out in 0–1 FRAME units rather than in flexbox.\n *\n * This is the unusual decision in the file and it is deliberate: the camera\n * addresses regions of the frame, so the thing it flies into has to live in the\n * same coordinate space as the `target*` props. Anyone reading these numbers can\n * point the camera at a card by copying its rect straight out of this object.\n */\nconst GEO = {\n  header: { x: 0.045, y: 0.062, w: 0.91, h: 0.072 },\n  /** Hairline under the header. */\n  rule: 0.152,\n  chart: { x: 0.045, y: 0.192, w: 0.555, h: 0.398 },\n  kpi: { x: 0.616, y: 0.192, w: 0.339, h: 0.398 },\n  tiles: { x: 0.045, y: 0.618, w: 0.91, h: 0.256 },\n  foot: 0.914,\n} as const;\n\n/**\n * Plot area of the chart card, in the same frame units. Held out here because\n * two things depend on it: the chart draws itself into it, and the default\n * `focusX` / `focusY` are the position of the peak marker inside it —\n * PLOT.x + (8/12) * PLOT.w = 0.419, PLOT.y + (1 - 0.88) * PLOT.h = 0.325.\n */\nconst PLOT = { x: 0.093, y: 0.297, w: 0.489, h: 0.237 } as const;\n\n/** Gap between cards, as a fraction of width. ~30px at 1920. */\nconst CARD_GAP = 0.0156;\n\n/* -------------------------------------------------------------- furniture */\n\n/*\n * Two series and a peak. Authored rather than seeded: the camera flies to one\n * specific data point, so that point has to be in the same place every render\n * and has to be worth flying to — a rise, one spike, a settle.\n */\nconst SERIES = [0.26, 0.31, 0.29, 0.38, 0.44, 0.52, 0.61, 0.72, 0.88, 0.7, 0.64, 0.68, 0.62] as const;\nconst PRIOR = [0.2, 0.24, 0.23, 0.27, 0.31, 0.34, 0.37, 0.41, 0.44, 0.46, 0.49, 0.52, 0.54] as const;\nconst PEAK = 8;\n\nconst Y_LABELS = [\"4k\", \"3k\", \"2k\", \"1k\", \"0\"] as const;\nconst X_TICKS = [0, 3, 6, 9, 12] as const;\n\nconst RANGES = [\"7d\", \"30d\", \"90d\"] as const;\nconst ACTIVE_RANGE = 2;\n\n/*\n * Periphery. Only what the camera lands on is exposed as props: the chart card\n * and the annotation. The KPI stack and the tile row are furniture — read once\n * in the wide shot, never in the detail — and turning them into schema fields\n * would cost an agent eighteen decisions to change three words nobody reads.\n */\nconst KPIS = [\n  { label: \"Activated teams\", value: \"1,284\", delta: \"12.4%\", up: true },\n  { label: \"Time to first value\", value: \"3.1d\", delta: \"0.6d\", up: false },\n  { label: \"Seats per team\", value: \"8.7\", delta: \"0.9\", up: true },\n] as const;\n\nconst TILES = [\n  { label: \"Invites sent\", value: \"9,412\" },\n  { label: \"Workspaces created\", value: \"612\" },\n  { label: \"Templates used\", value: \"3,905\" },\n  { label: \"Docs exported\", value: \"18.2k\" },\n] as const;\n\nconst BARS = 7;\n\n/* ------------------------------------------------------------------ schema */\n\nexport const zoomToRegionSchema = z.object({\n  title: z\n    .string()\n    .default(\"Activation, week over week\")\n    .describe(\n      \"Display heading of the synthesised dashboard, top left. Three to six words; it is read in the wide shot only, so it can afford to be a full sentence fragment.\",\n    ),\n  meta: z\n    .string()\n    .default(\"Growth workspace · rolling 13 weeks\")\n    .describe(\n      \"Small mono line under the title. Use it for scope and range — the thing that tells a viewer what they are looking at before the camera moves.\",\n    ),\n  metricLabel: z\n    .string()\n    .default(\"Weekly activations\")\n    .describe(\n      \"Label of the big chart card. This one IS read at full zoom, so keep it two or three words and make it the name of the series.\",\n    ),\n  metricValue: z\n    .string()\n    .default(\"3,520\")\n    .describe(\n      \"Headline figure on the chart card, set in the display face. Pre-format it — thousands separators, units, currency — the component never touches your number. The default reads the peak off a 4k axis, which is where the camera is going.\",\n    ),\n  metricDelta: z\n    .string()\n    .default(\"22.2% w/w\")\n    .describe(\"Text of the accent-tinted chip beside the headline figure, led by an up caret. Empty string for no chip.\"),\n  focusLabel: z\n    .string()\n    .default(\"Week 8 spike\")\n    .describe(\n      \"Mono label on the annotation that appears once the camera has parked. Two or three words: this is the caption on the thing you zoomed in to see.\",\n    ),\n  focusValue: z\n    .string()\n    .default(\"2× the prior period\")\n    .describe(\n      \"Second line of the annotation, set larger — the fact the zoom exists to deliver, so make it something the wide shot could not say. The card sizes itself to whichever of the two lines is wider, and flips to the other side of the ring rather than run off the frame.\",\n    ),\n  caption: z\n    .string()\n    .default(\"One layer, two keyframes — the camera is a transform, not a cut\")\n    .describe(\"Muted footer line led by an accent dash, aligned to the dashboard's left edge. Empty string for none.\"),\n  targetX: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.2)\n    .describe(\"Left edge of the region the camera flies to, as a fraction of frame WIDTH. 0 is the left edge of the frame.\"),\n  targetY: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.24)\n    .describe(\"Top edge of the target region, as a fraction of frame HEIGHT. 0 is the top of the frame.\"),\n  targetW: z\n    .number()\n    .min(0.05)\n    .max(1)\n    .default(0.4)\n    .describe(\n      \"Width of the target region, as a fraction of frame width. Copy a rect out of the GEO object at the top of this file to frame a specific card: the chart is 0.045/0.192/0.555/0.398.\",\n    ),\n  targetH: z\n    .number()\n    .min(0.05)\n    .max(1)\n    .default(0.3)\n    .describe(\n      \"Height of the target region, as a fraction of frame height. The tighter of width and height decides the scale, so the region is always fully inside the held frame and usually a little more.\",\n    ),\n  zoom: z\n    .number()\n    .min(0.4)\n    .max(1.6)\n    .default(0.94)\n    .describe(\n      \"Camera push, as a multiple of the scale at which the target region exactly fits the frame. 1 puts the region edge to edge on its tighter axis, 0.94 leaves a hair of margin, above 1 crops inside it. The scale is clamped at 1 from below, so the camera can never pull back past the wide shot.\",\n    ),\n  focusX: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.419)\n    .describe(\n      \"Centre of the annotation ring, in the same frame units as the target region — put it on the thing you actually want circled, which is rarely the exact centre of the rect. The default sits on the chart's peak marker.\",\n    ),\n  focusY: z\n    .number()\n    .min(0)\n    .max(1)\n    .default(0.325)\n    .describe(\"Vertical centre of the annotation ring, as a fraction of frame height. The default sits on the chart's peak marker.\"),\n  reticle: z\n    .boolean()\n    .default(true)\n    .describe(\n      \"Bracket the target region with hairline corner marks and a mono scale readout before the push, and again on the way out. Turn it off for a straight cinematic move with no instrumentation.\",\n    ),\n  seed: z\n    .string()\n    .default(\"region-01\")\n    .describe(\n      \"Seeds the ragged heights of the micro histograms in the tile row along the bottom. Change it for a differently-shaped dashboard of identical duration; the same seed always renders the same frames.\",\n    ),\n  brand: z.string().default(\"slate\").describe(\"Brand token id — one of the keys of BRANDS.\"),\n});\n\ntype Props = z.infer<typeof zoomToRegionSchema>;\n\n/* -------------------------------------------------------------- type scale */\n\n/**\n * Every measurement in the file, derived once from the frame so one component\n * serves 16:9, 9:16, 1:1 and 4:5. Nothing below reads `useVideoConfig` again.\n */\ntype Metrics = {\n  /** 1–2px. A hairline that survives a downscaled preview but never thickens into a border. */\n  hair: number;\n  radius: number;\n  /** Mono micro labels: axis ticks, card labels, legend. */\n  tag: number;\n  /** Card values in the KPI stack and the tile row. */\n  value: number;\n  /** The chart card's headline figure. */\n  hero: number;\n  /** Dashboard title. */\n  title: number;\n  /** Card padding, and the unit the periphery spaces itself on. */\n  pad: number;\n  /** The annotation lives in screen space, so it carries its own two sizes. */\n  annTag: number;\n  annValue: number;\n  /** Radius of the annotation ring, in screen px. */\n  ring: number;\n};\n\nconst measure = (width: number, height: number, radius: number): Metrics => ({\n  hair: Math.max(1, Math.round(width * 0.0011)),\n  radius: Math.max(radius, 2),\n  tag: width * 0.0082,\n  value: width * 0.0225,\n  hero: width * 0.0275,\n  title: width * 0.0175,\n  pad: width * 0.017,\n  annTag: width * 0.0125,\n  annValue: width * 0.026,\n  ring: height * 0.072,\n});\n\n/* ----------------------------------------------------------------- helpers */\n\nconst clamp = (v: number, lo: number, hi: number) => Math.min(hi, Math.max(lo, v));\n\n/** Place a frame-unit rect into absolute pixels. The dashboard's only layout primitive. */\nconst box = (r: Rect, width: number, height: number): React.CSSProperties => ({\n  position: \"absolute\",\n  left: r.x * width,\n  top: r.y * height,\n  width: r.w * width,\n  height: r.h * height,\n});\n\nconst card = (t: BrandTokens, m: Metrics): React.CSSProperties => ({\n  boxSizing: \"border-box\",\n  backgroundColor: t.surface,\n  border: `${m.hair}px solid ${t.rule}`,\n  borderRadius: m.radius,\n});\n\n/** Rough advance width for a text run — enough to size a pill without measuring. */\nconst runWidth = (text: string, size: number, per: number) => text.length * size * per;\n\n/* ------------------------------------------------------------------ camera */\n\ntype Camera = {\n  /** 0 wide, 1 parked at the detail. */\n  k: number;\n  /** Current scale of the dashboard layer. */\n  scale: number;\n  /** Scale at the far end of the move. Reported by the reticle. */\n  full: number;\n  tx: number;\n  ty: number;\n};\n\n/**\n * The move, as four keyframes on one number.\n *\n * `EASING.travel` is the registry's only ease-in-out curve, and it is applied\n * per segment: the push leaves rest and arrives at rest, the park is literally\n * constant, the pull does the same in reverse. Nothing here is a spring — a\n * camera that overshoots its subject reads as a mistake, not as weight.\n */\nconst push = (frame: number) =>\n  interpolate(frame, [PUSH[0], PUSH[1], PULL[0], PULL[1]], [0, 1, 1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n\n/**\n * Scale and translate for the dashboard layer at `frame`, given the target rect.\n *\n * Two details earn their keep. Scale is interpolated in LOG space, so the\n * perceived rate of zoom is constant rather than crawling at the wide end and\n * lunging at the tight end — the difference between a camera and a CSS scale.\n * And the look-at point is clamped to half a viewport from each edge, so a\n * target rect near the frame border pans as far as it can and then stops\n * instead of dragging empty canvas into shot.\n */\nconst camera = (frame: number, width: number, height: number, target: Rect, zoom: number): Camera => {\n  const k = push(frame);\n  const fit = Math.min(1 / Math.max(target.w, 0.05), 1 / Math.max(target.h, 0.05));\n  const full = Math.max(1, fit * zoom);\n  const scale = Math.exp(Math.log(full) * k);\n\n  const half = 0.5 / scale;\n  const lookX = clamp(0.5 + (target.x + target.w / 2 - 0.5) * k, half, 1 - half);\n  const lookY = clamp(0.5 + (target.y + target.h / 2 - 0.5) * k, half, 1 - half);\n\n  return {\n    k,\n    scale,\n    full,\n    tx: width / 2 - lookX * width * scale,\n    ty: height / 2 - lookY * height * scale,\n  };\n};\n\n/** Where a frame-unit point currently sits on screen. Used by both overlays. */\nconst projX = (cam: Camera, fx: number, width: number) => cam.tx + fx * width * cam.scale;\nconst projY = (cam: Camera, fy: number, height: number) => cam.ty + fy * height * cam.scale;\n\n/* --------------------------------------------------------------- component */\n\nconst ZoomToRegion: React.FC<Props> = ({\n  title,\n  meta,\n  metricLabel,\n  metricValue,\n  metricDelta,\n  focusLabel,\n  focusValue,\n  caption,\n  targetX,\n  targetY,\n  targetW,\n  targetH,\n  zoom,\n  focusX,\n  focusY,\n  reticle,\n  seed,\n  brand,\n}) => {\n  const t = BRANDS[brand] ?? DEFAULT_BRAND;\n  const { width, height } = useVideoConfig();\n  const stage = useStage(BUDGET);\n  const m = measure(width, height, t.radius);\n\n  const target: Rect = { x: targetX, y: targetY, w: targetW, h: targetH };\n  const cam = camera(stage.frame, width, height, target, zoom);\n\n  /*\n   * Presence is a hair of extra push released as the scene arrives, and taken\n   * back as it leaves — so the exit unwinds the entrance rather than inventing\n   * a second curve. It sits on its own wrapper, outside the camera layer, which\n   * keeps the two transforms independent and the projection maths honest.\n   */\n  const release = interpolate(stage.presence, [0, 1], [1.03, 1]);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: t.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width,\n          height,\n          opacity: stage.presence,\n          transform: `scale(${release})`,\n        }}\n      >\n        {/* The camera layer. One transform carries the entire dashboard. */}\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            width,\n            height,\n            transformOrigin: \"0 0\",\n            transform: `translate(${cam.tx}px, ${cam.ty}px) scale(${cam.scale})`,\n          }}\n        >\n          <Wash tokens={t} width={width} height={height} amount={stage.enter} />\n          <Header tokens={t} width={width} height={height} m={m} title={title} meta={meta} />\n          <Rule tokens={t} width={width} height={height} m={m} />\n          <ChartCard\n            tokens={t}\n            width={width}\n            height={height}\n            m={m}\n            label={metricLabel}\n            value={metricValue}\n            delta={metricDelta}\n          />\n          <KpiStack tokens={t} width={width} height={height} m={m} />\n          <TileRow tokens={t} width={width} height={height} m={m} seed={seed} />\n          <Footer tokens={t} width={width} height={height} m={m} caption={caption} />\n        </div>\n\n        {/* Screen-space overlays. Sized in real pixels, so they stay crisp and\n            hairline-thin however far the camera has pushed in. */}\n        <Vignette k={cam.k} />\n        {reticle ? <Reticle tokens={t} m={m} cam={cam} target={target} width={width} height={height} /> : null}\n        <Annotation\n          tokens={t}\n          m={m}\n          cam={cam}\n          frame={stage.frame}\n          width={width}\n          height={height}\n          fx={focusX}\n          fy={focusY}\n          label={focusLabel}\n          value={focusValue}\n        />\n      </div>\n    </AbsoluteFill>\n  );\n};\n\n/* ------------------------------------------------------------- atmosphere */\n\n/** One soft accent light in the upper left of the dashboard, so the screen reads as lit. */\nconst Wash: React.FC<{ tokens: BrandTokens; width: number; height: number; amount: number }> = ({\n  tokens,\n  width,\n  height,\n  amount,\n}) => (\n  <div\n    style={{\n      position: \"absolute\",\n      left: -width * 0.1,\n      top: -height * 0.35,\n      width: width * 0.8,\n      height: height * 0.9,\n      borderRadius: \"50%\",\n      backgroundImage: `radial-gradient(closest-side, ${tokens.accent}, rgba(0,0,0,0))`,\n      opacity: 0.1 * amount,\n    }}\n  />\n);\n\n/**\n * A vignette that tightens as the camera pushes in. The one place this file\n * spends a literal colour: a black scrim, which reads correctly on the light\n * brands and the dark ones alike.\n */\nconst Vignette: React.FC<{ k: number }> = ({ k }) => (\n  <div\n    style={{\n      position: \"absolute\",\n      left: 0,\n      top: 0,\n      right: 0,\n      bottom: 0,\n      backgroundImage:\n        \"radial-gradient(closest-corner at 50% 48%, rgba(0,0,0,0) 42%, rgba(0,0,0,0.5) 100%)\",\n      opacity: 0.1 + 0.16 * k,\n    }}\n  />\n);\n\n/* ------------------------------------------------------------------ header */\n\nconst Header: React.FC<{\n  tokens: BrandTokens;\n  width: number;\n  height: number;\n  m: Metrics;\n  title: string;\n  meta: string;\n}> = ({ tokens, width, height, m, title, meta }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.header, PIECE_STEP);\n  const lift = (1 - s.enter) * height * 0.014;\n\n  return (\n    <div\n      style={{\n        ...box(GEO.header, width, height),\n        display: \"flex\",\n        alignItems: \"flex-start\",\n        justifyContent: \"space-between\",\n        opacity: s.enter,\n        transform: `translateY(${lift}px)`,\n      }}\n    >\n      <div>\n        <div\n          style={{\n            fontFamily: tokens.fontDisplay,\n            fontSize: m.title,\n            fontWeight: 700,\n            lineHeight: 1.04,\n            letterSpacing: m.title * -0.02,\n            color: tokens.ink,\n          }}\n        >\n          {title}\n        </div>\n        {meta ? (\n          <div\n            style={{\n              marginTop: m.title * 0.3,\n              fontFamily: tokens.fontMono,\n              fontSize: m.tag,\n              fontWeight: 500,\n              letterSpacing: m.tag * 0.1,\n              textTransform: \"uppercase\",\n              color: tokens.inkMuted,\n            }}\n          >\n            {meta}\n          </div>\n        ) : null}\n      </div>\n\n      <div style={{ display: \"flex\", alignItems: \"center\", gap: m.pad * 0.55 }}>\n        <Segmented tokens={tokens} m={m} />\n        <FilterPill tokens={tokens} m={m} />\n      </div>\n    </div>\n  );\n};\n\n/** Range switcher. Three cells, one filled — dashboard furniture, drawn exactly. */\nconst Segmented: React.FC<{ tokens: BrandTokens; m: Metrics }> = ({ tokens, m }) => (\n  <div\n    style={{\n      display: \"flex\",\n      padding: m.hair * 2,\n      gap: m.hair * 2,\n      borderRadius: m.radius,\n      border: `${m.hair}px solid ${tokens.rule}`,\n      backgroundColor: tokens.surface,\n    }}\n  >\n    {RANGES.map((r, i) => (\n      <div\n        key={r}\n        style={{\n          position: \"relative\",\n          overflow: \"hidden\",\n          padding: `${m.tag * 0.5}px ${m.tag * 0.95}px`,\n          borderRadius: Math.max(m.radius - m.hair * 2, 2),\n          fontFamily: tokens.fontMono,\n          fontSize: m.tag,\n          fontWeight: 600,\n          letterSpacing: m.tag * 0.04,\n          color: i === ACTIVE_RANGE ? tokens.accent : tokens.inkMuted,\n        }}\n      >\n        {i === ACTIVE_RANGE ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              right: 0,\n              bottom: 0,\n              backgroundColor: tokens.accent,\n              opacity: 0.14,\n            }}\n          />\n        ) : null}\n        <span style={{ position: \"relative\" }}>{r}</span>\n      </div>\n    ))}\n  </div>\n);\n\n/** Hairline pill with a three-bar filter glyph. */\nconst FilterPill: React.FC<{ tokens: BrandTokens; m: Metrics }> = ({ tokens, m }) => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      gap: m.tag * 0.6,\n      padding: `${m.tag * 0.66}px ${m.tag * 1.05}px`,\n      borderRadius: m.radius,\n      border: `${m.hair}px solid ${tokens.rule}`,\n      fontFamily: tokens.fontMono,\n      fontSize: m.tag,\n      fontWeight: 500,\n      letterSpacing: m.tag * 0.06,\n      color: tokens.inkMuted,\n    }}\n  >\n    <svg width={m.tag} height={m.tag} viewBox=\"0 0 10 10\" style={{ display: \"block\" }}>\n      <path\n        d=\"M1 2.5 H9 M2.4 5 H7.6 M4 7.5 H6\"\n        stroke={tokens.inkMuted}\n        strokeWidth={1.1}\n        strokeLinecap=\"round\"\n        fill=\"none\"\n      />\n    </svg>\n    Weekly\n  </div>\n);\n\nconst Rule: React.FC<{ tokens: BrandTokens; width: number; height: number; m: Metrics }> = ({\n  tokens,\n  width,\n  height,\n  m,\n}) => {\n  const s = useStaggeredStage(BUDGET, PIECE.header, PIECE_STEP);\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: GEO.header.x * width,\n        top: GEO.rule * height,\n        width: GEO.header.w * width,\n        height: m.hair,\n        backgroundColor: tokens.rule,\n        transform: `scaleX(${s.enter})`,\n        transformOrigin: \"left center\",\n      }}\n    />\n  );\n};\n\n/* -------------------------------------------------------------- chart card */\n\nconst ChartCard: React.FC<{\n  tokens: BrandTokens;\n  width: number;\n  height: number;\n  m: Metrics;\n  label: string;\n  value: string;\n  delta: string;\n}> = ({ tokens, width, height, m, label, value, delta }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.chart, PIECE_STEP);\n  const pw = PLOT.w * width;\n  const ph = PLOT.h * height;\n  const plotLeft = (PLOT.x - GEO.chart.x) * width;\n  const plotTop = (PLOT.y - GEO.chart.y) * height;\n\n  const xAt = (i: number) => (i / (SERIES.length - 1)) * pw;\n  const yAt = (v: number) => (1 - v) * ph;\n  const line = (vals: readonly number[]) =>\n    vals.map((v, i) => `${i === 0 ? \"M\" : \"L\"}${xAt(i).toFixed(2)},${yAt(v).toFixed(2)}`).join(\" \");\n\n  const series = line(SERIES);\n  const prior = line(PRIOR);\n  const area = `${series} L${pw.toFixed(2)},${ph.toFixed(2)} L0,${ph.toFixed(2)} Z`;\n\n  /* The series draws itself on during the card's own entrance, then holds. */\n  const drawn = evolvePath(s.enter, series);\n  const priorDrawn = evolvePath(clamp(s.enter * 1.2 - 0.2, 0, 1), prior);\n\n  /* The marker lands once the line has arrived under it. */\n  const marker = interpolate(s.frame, [BUDGET.enter - 4, BUDGET.enter + 6], [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n\n  /* Brand-scoped so two cards on one page never share a gradient definition. */\n  const fillId = `zoom-to-region-area-${tokens.id}`;\n\n  return (\n    <div\n      style={{\n        ...box(GEO.chart, width, height),\n        ...card(tokens, m),\n        opacity: s.enter,\n        overflow: \"hidden\",\n      }}\n    >\n      {/* Card head. Sized to end exactly where the plot begins. */}\n      <div\n        style={{\n          position: \"absolute\",\n          left: m.pad,\n          top: m.pad * 0.92,\n          right: m.pad,\n          height: plotTop - m.pad * 0.92,\n          overflow: \"hidden\",\n        }}\n      >\n        <div style={{ display: \"flex\", alignItems: \"center\", justifyContent: \"space-between\" }}>\n          <div\n            style={{\n              fontFamily: tokens.fontMono,\n              fontSize: m.tag,\n              fontWeight: 600,\n              letterSpacing: m.tag * 0.12,\n              textTransform: \"uppercase\",\n              color: tokens.inkMuted,\n            }}\n          >\n            {label}\n          </div>\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: m.tag * 1.3 }}>\n            <LegendKey tokens={tokens} m={m} color={tokens.accent} label=\"This period\" />\n            <LegendKey tokens={tokens} m={m} color={tokens.accentAlt} label=\"Prior\" />\n          </div>\n        </div>\n\n        <div style={{ display: \"flex\", alignItems: \"flex-end\", gap: m.pad * 0.6, marginTop: m.tag * 0.5 }}>\n          <div\n            style={{\n              fontFamily: tokens.fontDisplay,\n              fontSize: m.hero,\n              fontWeight: 700,\n              lineHeight: 1,\n              letterSpacing: m.hero * -0.024,\n              color: tokens.ink,\n            }}\n          >\n            {value}\n          </div>\n          {delta ? <TintPill tokens={tokens} m={m} label={delta} up /> : null}\n        </div>\n      </div>\n\n      {/* Plot. Its own coordinate space, in real pixels — no viewBox stretching,\n          so every stroke stays the width it was authored at. */}\n      <div style={{ position: \"absolute\", left: plotLeft, top: plotTop, width: pw, height: ph }}>\n        {Y_LABELS.map((y, i) => (\n          <div\n            key={y}\n            style={{\n              position: \"absolute\",\n              left: -plotLeft,\n              top: (i / (Y_LABELS.length - 1)) * ph - m.tag * 0.62,\n              width: plotLeft - m.tag * 0.9,\n              textAlign: \"right\",\n              fontFamily: tokens.fontMono,\n              fontSize: m.tag * 0.92,\n              color: tokens.inkMuted,\n              opacity: 0.85,\n            }}\n          >\n            {y}\n          </div>\n        ))}\n\n        <svg width={pw} height={ph} style={{ display: \"block\", overflow: \"visible\" }}>\n          <defs>\n            <linearGradient id={fillId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n              <stop offset=\"0%\" stopColor={tokens.accent} stopOpacity={0.26} />\n              <stop offset=\"100%\" stopColor={tokens.accent} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n\n          {Y_LABELS.map((y, i) => (\n            <line\n              key={y}\n              x1={0}\n              x2={pw}\n              y1={(i / (Y_LABELS.length - 1)) * ph}\n              y2={(i / (Y_LABELS.length - 1)) * ph}\n              stroke={tokens.rule}\n              strokeWidth={m.hair}\n            />\n          ))}\n\n          <path d={area} fill={`url(#${fillId})`} opacity={s.enter} />\n          <path\n            d={prior}\n            fill=\"none\"\n            stroke={tokens.accentAlt}\n            strokeWidth={m.hair * 1.1}\n            strokeLinecap=\"round\"\n            opacity={0.6}\n            {...priorDrawn}\n          />\n          <path\n            d={series}\n            fill=\"none\"\n            stroke={tokens.accent}\n            strokeWidth={m.hair * 1.8}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            {...drawn}\n          />\n\n          {/* Selected point: a dropline to the axis and a disc knocked out of the\n              surface, which is what keeps it legible on top of the area fill. */}\n          <line\n            x1={xAt(PEAK)}\n            x2={xAt(PEAK)}\n            y1={yAt(SERIES[PEAK])}\n            y2={ph}\n            stroke={tokens.accent}\n            strokeWidth={m.hair}\n            strokeDasharray={`${m.hair * 2} ${m.hair * 3}`}\n            opacity={0.55 * marker}\n          />\n          <circle\n            cx={xAt(PEAK)}\n            cy={yAt(SERIES[PEAK])}\n            r={m.hair * 3.4}\n            fill={tokens.surface}\n            stroke={tokens.accent}\n            strokeWidth={m.hair * 1.6}\n            opacity={marker}\n          />\n        </svg>\n\n        {X_TICKS.map((i) => (\n          <div\n            key={i}\n            style={{\n              position: \"absolute\",\n              left: xAt(i) - pw * 0.06,\n              top: ph + m.tag * 0.9,\n              width: pw * 0.12,\n              textAlign: \"center\",\n              fontFamily: tokens.fontMono,\n              fontSize: m.tag * 0.92,\n              color: tokens.inkMuted,\n              opacity: 0.85,\n            }}\n          >\n            {`W${i + 1}`}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n};\n\nconst LegendKey: React.FC<{ tokens: BrandTokens; m: Metrics; color: string; label: string }> = ({\n  tokens,\n  m,\n  color,\n  label,\n}) => (\n  <div style={{ display: \"flex\", alignItems: \"center\", gap: m.tag * 0.45 }}>\n    <div style={{ width: m.tag * 1.4, height: m.hair * 1.6, backgroundColor: color }} />\n    <span\n      style={{\n        fontFamily: tokens.fontMono,\n        fontSize: m.tag * 0.9,\n        letterSpacing: m.tag * 0.04,\n        color: tokens.inkMuted,\n      }}\n    >\n      {label}\n    </span>\n  </div>\n);\n\n/* ------------------------------------------------------------------- chips */\n\nconst Caret: React.FC<{ color: string; size: number; up: boolean }> = ({ color, size, up }) => (\n  <svg width={size} height={size} viewBox=\"0 0 10 10\" style={{ display: \"block\", position: \"relative\" }}>\n    <path d={up ? \"M5 1.6 L9 8.4 L1 8.4 Z\" : \"M5 8.4 L1 1.6 L9 1.6 Z\"} fill={color} />\n  </svg>\n);\n\n/**\n * An accent-tinted chip. The tint is a full-strength accent panel at low opacity\n * behind full-strength accent text, rather than a faded chip — a brand ships one\n * accent, not a ramp, and this is how you get a second step out of it without\n * dimming the label.\n */\nconst TintPill: React.FC<{ tokens: BrandTokens; m: Metrics; label: string; up?: boolean }> = ({\n  tokens,\n  m,\n  label,\n  up,\n}) => (\n  <span\n    style={{\n      position: \"relative\",\n      display: \"inline-flex\",\n      alignItems: \"center\",\n      gap: m.tag * 0.42,\n      padding: `${m.tag * 0.4}px ${m.tag * 0.7}px`,\n      borderRadius: m.radius * 0.7,\n      overflow: \"hidden\",\n      whiteSpace: \"nowrap\",\n    }}\n  >\n    <span\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        right: 0,\n        bottom: 0,\n        backgroundColor: tokens.accent,\n        opacity: 0.14,\n      }}\n    />\n    {up === undefined ? null : <Caret color={tokens.accent} size={m.tag * 0.72} up={up} />}\n    <span\n      style={{\n        position: \"relative\",\n        fontFamily: tokens.fontMono,\n        fontSize: m.tag,\n        fontWeight: 600,\n        letterSpacing: m.tag * 0.02,\n        color: tokens.accent,\n      }}\n    >\n      {label}\n    </span>\n  </span>\n);\n\n/* -------------------------------------------------------------- periphery */\n\nconst KpiStack: React.FC<{\n  tokens: BrandTokens;\n  width: number;\n  height: number;\n  m: Metrics;\n}> = ({ tokens, width, height, m }) => (\n  <div\n    style={{\n      ...box(GEO.kpi, width, height),\n      display: \"flex\",\n      flexDirection: \"column\",\n      gap: CARD_GAP * width * 0.62,\n    }}\n  >\n    {KPIS.map((k, i) => (\n      <KpiCard key={k.label} tokens={tokens} m={m} index={i} entry={k} />\n    ))}\n  </div>\n);\n\nconst KpiCard: React.FC<{\n  tokens: BrandTokens;\n  m: Metrics;\n  index: number;\n  entry: (typeof KPIS)[number];\n}> = ({ tokens, m, index, entry }) => {\n  /* Fractional index: the three cards land a frame apart inside the group's own\n     two-frame slot, so the column reads as one wipe rather than three events. */\n  const s = useStaggeredStage(BUDGET, PIECE.kpi + index * 0.5, PIECE_STEP);\n\n  return (\n    <div\n      style={{\n        ...card(tokens, m),\n        flex: 1,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"space-between\",\n        padding: m.pad * 0.74,\n        opacity: s.enter,\n        transform: `translateX(${(1 - s.enter) * m.pad}px)`,\n      }}\n    >\n      <div\n        style={{\n          fontFamily: tokens.fontMono,\n          fontSize: m.tag * 0.94,\n          fontWeight: 500,\n          letterSpacing: m.tag * 0.1,\n          textTransform: \"uppercase\",\n          color: tokens.inkMuted,\n        }}\n      >\n        {entry.label}\n      </div>\n\n      <div style={{ display: \"flex\", alignItems: \"flex-end\", justifyContent: \"space-between\", gap: m.pad * 0.5 }}>\n        <div\n          style={{\n            fontFamily: tokens.fontDisplay,\n            fontSize: m.value * 0.86,\n            fontWeight: 700,\n            lineHeight: 1,\n            letterSpacing: m.value * -0.02,\n            color: tokens.ink,\n          }}\n        >\n          {entry.value}\n        </div>\n        <TintPill tokens={tokens} m={m} label={entry.delta} up={entry.up} />\n      </div>\n    </div>\n  );\n};\n\nconst TileRow: React.FC<{\n  tokens: BrandTokens;\n  width: number;\n  height: number;\n  m: Metrics;\n  seed: string;\n}> = ({ tokens, width, height, m, seed }) => (\n  <div style={{ ...box(GEO.tiles, width, height), display: \"flex\", gap: CARD_GAP * width }}>\n    {TILES.map((tile, i) => (\n      <Tile key={tile.label} tokens={tokens} m={m} index={i} seed={seed} entry={tile} />\n    ))}\n  </div>\n);\n\nconst Tile: React.FC<{\n  tokens: BrandTokens;\n  m: Metrics;\n  index: number;\n  seed: string;\n  entry: (typeof TILES)[number];\n}> = ({ tokens, m, index, seed, entry }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.tiles + index * 0.34, PIECE_STEP);\n\n  return (\n    <div\n      style={{\n        ...card(tokens, m),\n        flex: 1,\n        display: \"flex\",\n        flexDirection: \"column\",\n        justifyContent: \"space-between\",\n        padding: m.pad * 0.82,\n        opacity: s.enter,\n        transform: `translateY(${(1 - s.enter) * m.pad * 0.8}px)`,\n      }}\n    >\n      <div>\n        <div\n          style={{\n            fontFamily: tokens.fontMono,\n            fontSize: m.tag * 0.94,\n            fontWeight: 500,\n            letterSpacing: m.tag * 0.1,\n            textTransform: \"uppercase\",\n            color: tokens.inkMuted,\n          }}\n        >\n          {entry.label}\n        </div>\n        <div\n          style={{\n            marginTop: m.tag * 0.7,\n            fontFamily: tokens.fontDisplay,\n            fontSize: m.value,\n            fontWeight: 700,\n            lineHeight: 1,\n            letterSpacing: m.value * -0.022,\n            color: tokens.ink,\n          }}\n        >\n          {entry.value}\n        </div>\n      </div>\n\n      {/* Micro histogram. Seeded, and the last column is the live one. */}\n      <div style={{ display: \"flex\", alignItems: \"flex-end\", gap: m.hair * 3, height: m.value * 1.1 }}>\n        {Array.from({ length: BARS }, (_, b) => {\n          const v = 0.28 + random(`${seed}:tile:${index}:${b}`) * 0.72;\n          const grow = clamp(s.enter * 1.4 - b * 0.05, 0, 1);\n          return (\n            <div\n              key={b}\n              style={{\n                flex: 1,\n                height: `${v * 100 * grow}%`,\n                borderRadius: m.hair,\n                backgroundColor: b === BARS - 1 ? tokens.accent : tokens.rule,\n              }}\n            />\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\nconst Footer: React.FC<{\n  tokens: BrandTokens;\n  width: number;\n  height: number;\n  m: Metrics;\n  caption: string;\n}> = ({ tokens, width, height, m, caption }) => {\n  const s = useStaggeredStage(BUDGET, PIECE.foot, PIECE_STEP);\n  if (!caption) return null;\n\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: GEO.header.x * width,\n        top: GEO.foot * height,\n        width: GEO.header.w * width,\n        display: \"flex\",\n        alignItems: \"center\",\n        gap: m.tag * 0.9,\n        opacity: s.enter,\n      }}\n    >\n      <div style={{ width: m.tag * 2, height: m.hair, backgroundColor: tokens.accent }} />\n      <div\n        style={{\n          fontFamily: tokens.fontBody,\n          fontSize: m.tag * 1.15,\n          letterSpacing: m.tag * 0.01,\n          color: tokens.inkMuted,\n        }}\n      >\n        {caption}\n      </div>\n    </div>\n  );\n};\n\n/* ---------------------------------------------------------------- reticle */\n\n/**\n * The region, marked in screen space before the camera commits to it.\n *\n * Because it is projected rather than drawn into the dashboard, the brackets\n * stay hairline-thin and visibly converge on the frame edges as the push runs —\n * they are the same rect the whole time, seen from closer. They fade out by the\n * time the move is half done, and come back on the way out.\n */\nconst Reticle: React.FC<{\n  tokens: BrandTokens;\n  m: Metrics;\n  cam: Camera;\n  target: Rect;\n  width: number;\n  height: number;\n}> = ({ tokens, m, cam, target, width, height }) => {\n  const stage = useStage(BUDGET);\n\n  const arrive = interpolate(stage.frame, RETICLE, [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const yieldToCamera = interpolate(cam.k, [0.08, 0.46], [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n  const alpha = arrive * yieldToCamera;\n  if (alpha <= 0.001) return null;\n\n  const l = projX(cam, target.x, width);\n  const tp = projY(cam, target.y, height);\n  const w = target.w * width * cam.scale;\n  const h = target.h * height * cam.scale;\n  const arm = Math.min(w, h) * 0.16;\n\n  const corner = (dx: number, dy: number): React.CSSProperties => ({\n    position: \"absolute\",\n    width: arm,\n    height: arm,\n    left: dx === 0 ? 0 : w - arm,\n    top: dy === 0 ? 0 : h - arm,\n    borderTop: dy === 0 ? `${m.hair * 1.5}px solid ${tokens.accent}` : undefined,\n    borderBottom: dy === 1 ? `${m.hair * 1.5}px solid ${tokens.accent}` : undefined,\n    borderLeft: dx === 0 ? `${m.hair * 1.5}px solid ${tokens.accent}` : undefined,\n    borderRight: dx === 1 ? `${m.hair * 1.5}px solid ${tokens.accent}` : undefined,\n  });\n\n  return (\n    <div style={{ position: \"absolute\", left: l, top: tp, width: w, height: h, opacity: alpha }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          right: 0,\n          bottom: 0,\n          backgroundColor: tokens.accent,\n          opacity: 0.055,\n        }}\n      />\n      <div style={corner(0, 0)} />\n      <div style={corner(1, 0)} />\n      <div style={corner(0, 1)} />\n      <div style={corner(1, 1)} />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          bottom: h + m.tag * 0.8,\n          display: \"flex\",\n          alignItems: \"center\",\n          gap: m.tag * 0.6,\n          fontFamily: tokens.fontMono,\n          fontSize: m.tag * 1.05,\n          fontWeight: 600,\n          letterSpacing: m.tag * 0.16,\n          textTransform: \"uppercase\",\n          color: tokens.accent,\n          whiteSpace: \"nowrap\",\n        }}\n      >\n        Region\n        <span style={{ opacity: 0.55 }}>{`×${cam.full.toFixed(2)}`}</span>\n      </div>\n    </div>\n  );\n};\n\n/* -------------------------------------------------------------- annotation */\n\n/**\n * The payoff: a ring drawn onto the detail, a leader, and a two-line card.\n *\n * All of it is screen-space, positioned by projecting the focus point through\n * the same camera — so the ring is a true circle at a fixed stroke weight\n * instead of a 2.35×-scaled ellipse, and the label is set at a size chosen for\n * the finished frame rather than for the dashboard it sits on. This is the one\n * accent gesture in the component, and the ring drawing on is it.\n */\nconst Annotation: React.FC<{\n  tokens: BrandTokens;\n  m: Metrics;\n  cam: Camera;\n  frame: number;\n  width: number;\n  height: number;\n  fx: number;\n  fy: number;\n  label: string;\n  value: string;\n}> = ({ tokens, m, cam, frame, width, height, fx, fy, label, value }) => {\n  const draw = interpolate(frame, RING, [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.travel,\n  });\n  const arrive = interpolate(frame, LABEL, [0, 1], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.enter,\n  });\n  const leave = interpolate(frame, FADE, [1, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: EASING.exit,\n  });\n  if (draw <= 0.001 || leave <= 0.001) return null;\n\n  const cx = projX(cam, fx, width);\n  const cy = projY(cam, fy, height);\n  const r = m.ring;\n  const ring = makeCircle({ radius: r });\n  const evolved = evolvePath(draw, ring.path);\n\n  /* Card measure. Whichever line sets wider decides the width; the pad and the\n     leader are the same unit, so the whole assembly reads as one object. */\n  const gap = m.annTag * 1.4;\n  const pad = m.annTag * 1.05;\n  const cardW =\n    Math.max(runWidth(value, m.annValue, 0.55), runWidth(label, m.annTag, 0.74)) + pad * 2;\n  const cardH = pad * 2 + m.annTag * 1.2 + m.annValue * 1.15;\n\n  /* Flip to the other side of the ring rather than run off the frame. */\n  const margin = width * 0.04;\n  const flip = cx + r + gap + cardW > width - margin;\n  const cardX = flip ? cx - r - gap - cardW : cx + r + gap;\n  const cardY = clamp(cy - cardH * 0.5, margin, height - margin - cardH);\n\n  return (\n    <div style={{ opacity: leave }}>\n      <svg\n        width={ring.width}\n        height={ring.height}\n        viewBox={`0 0 ${ring.width} ${ring.height}`}\n        style={{ position: \"absolute\", left: cx - r, top: cy - r, overflow: \"visible\" }}\n      >\n        <path\n          d={ring.path}\n          fill=\"none\"\n          stroke={tokens.accent}\n          strokeWidth={m.hair * 2}\n          strokeLinecap=\"round\"\n          {...evolved}\n        />\n      </svg>\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: flip ? cardX + cardW : cx + r,\n          top: cy - m.hair / 2,\n          width: gap,\n          height: m.hair,\n          backgroundColor: tokens.accent,\n          transform: `scaleX(${arrive})`,\n          transformOrigin: flip ? \"right center\" : \"left center\",\n        }}\n      />\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: cardX,\n          top: cardY,\n          width: cardW,\n          boxSizing: \"border-box\",\n          padding: pad,\n          backgroundColor: tokens.surface,\n          border: `${m.hair}px solid ${tokens.rule}`,\n          borderLeft: flip ? `${m.hair}px solid ${tokens.rule}` : `${m.hair * 2}px solid ${tokens.accent}`,\n          borderRight: flip ? `${m.hair * 2}px solid ${tokens.accent}` : `${m.hair}px solid ${tokens.rule}`,\n          borderRadius: m.radius,\n          boxShadow: `0 ${m.hair * 4}px ${m.hair * 14}px rgba(0,0,0,0.3)`,\n          opacity: arrive,\n          transform: `translateX(${(1 - arrive) * (flip ? m.annTag : -m.annTag)}px)`,\n        }}\n      >\n        <div\n          style={{\n            fontFamily: tokens.fontMono,\n            fontSize: m.annTag,\n            fontWeight: 600,\n            letterSpacing: m.annTag * 0.12,\n            textTransform: \"uppercase\",\n            color: tokens.accent,\n          }}\n        >\n          {label}\n        </div>\n        <div\n          style={{\n            marginTop: m.annTag * 0.42,\n            fontFamily: tokens.fontDisplay,\n            fontSize: m.annValue,\n            fontWeight: 700,\n            lineHeight: 1.1,\n            letterSpacing: m.annValue * -0.022,\n            color: tokens.ink,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          {value}\n        </div>\n      </div>\n    </div>\n  );\n};\n\nexport default defineMotionItem({\n  id: \"zoom-to-region\",\n  name: \"Zoom To Region\",\n  category: \"product\",\n  summary:\n    \"A synthesised dashboard, then a keyframed camera push into one named region and back out, with an annotation ring on the detail\",\n  tags: [\"camera\", \"pan and zoom\", \"product shot\", \"dashboard\", \"ui mock\", \"callout\", \"annotation\"],\n  budget: BUDGET,\n  aspect: \"16:9\",\n  schema: zoomToRegionSchema,\n  defaults: {\n    title: \"Activation, week over week\",\n    meta: \"Growth workspace · rolling 13 weeks\",\n    metricLabel: \"Weekly activations\",\n    metricValue: \"3,520\",\n    metricDelta: \"22.2% w/w\",\n    focusLabel: \"Week 8 spike\",\n    focusValue: \"2× the prior period\",\n    caption: \"One layer, two keyframes — the camera is a transform, not a cut\",\n    targetX: 0.2,\n    targetY: 0.24,\n    targetW: 0.4,\n    targetH: 0.3,\n    zoom: 0.94,\n    focusX: 0.419,\n    focusY: 0.325,\n    reticle: true,\n    seed: \"region-01\",\n    brand: \"slate\",\n  },\n  component: ZoomToRegion,\n  file: \"src/registry/items/zoom-to-region.tsx\",\n  origin: {\n    prompt:\n      \"Draw a wide fake analytics dashboard — header, a big chart card, a KPI stack, a row of tiles — then treat the whole thing as one layer and fly a camera into it: scale and translate so a named rect fills the frame, hold there while a ring draws onto the detail and a small label names it, then pull back out. Let me pass the target rect as x/y/w/h in frame units plus a zoom amount. Mark the region with hairline brackets before the push. Start and end at rest.\",\n    tool: \"Claude Code\",\n    model: \"Opus 5\",\n    author: \"motionstash\",\n  },\n});\n",
      "origin": {
        "prompt": "Draw a wide fake analytics dashboard — header, a big chart card, a KPI stack, a row of tiles — then treat the whole thing as one layer and fly a camera into it: scale and translate so a named rect fills the frame, hold there while a ring draws onto the detail and a small label names it, then pull back out. Let me pass the target rect as x/y/w/h in frame units plus a zoom amount. Mark the region with hairline brackets before the push. Start and end at rest.",
        "tool": "Claude Code",
        "model": "Opus 5",
        "author": "motionstash"
      },
      "ground": "dark",
      "score": 0.2697,
      "props": [
        {
          "name": "title",
          "type": "string",
          "description": "Display heading of the synthesised dashboard, top left. Three to six words; it is read in the wide shot only, so it can afford to be a full sentence fragment.",
          "required": false,
          "defaultValue": "Activation, week over week",
          "options": null
        },
        {
          "name": "meta",
          "type": "string",
          "description": "Small mono line under the title. Use it for scope and range — the thing that tells a viewer what they are looking at before the camera moves.",
          "required": false,
          "defaultValue": "Growth workspace · rolling 13 weeks",
          "options": null
        },
        {
          "name": "metricLabel",
          "type": "string",
          "description": "Label of the big chart card. This one IS read at full zoom, so keep it two or three words and make it the name of the series.",
          "required": false,
          "defaultValue": "Weekly activations",
          "options": null
        },
        {
          "name": "metricValue",
          "type": "string",
          "description": "Headline figure on the chart card, set in the display face. Pre-format it — thousands separators, units, currency — the component never touches your number. The default reads the peak off a 4k axis, which is where the camera is going.",
          "required": false,
          "defaultValue": "3,520",
          "options": null
        },
        {
          "name": "metricDelta",
          "type": "string",
          "description": "Text of the accent-tinted chip beside the headline figure, led by an up caret. Empty string for no chip.",
          "required": false,
          "defaultValue": "22.2% w/w",
          "options": null
        },
        {
          "name": "focusLabel",
          "type": "string",
          "description": "Mono label on the annotation that appears once the camera has parked. Two or three words: this is the caption on the thing you zoomed in to see.",
          "required": false,
          "defaultValue": "Week 8 spike",
          "options": null
        },
        {
          "name": "focusValue",
          "type": "string",
          "description": "Second line of the annotation, set larger — the fact the zoom exists to deliver, so make it something the wide shot could not say. The card sizes itself to whichever of the two lines is wider, and flips to the other side of the ring rather than run off the frame.",
          "required": false,
          "defaultValue": "2× the prior period",
          "options": null
        },
        {
          "name": "caption",
          "type": "string",
          "description": "Muted footer line led by an accent dash, aligned to the dashboard's left edge. Empty string for none.",
          "required": false,
          "defaultValue": "One layer, two keyframes — the camera is a transform, not…",
          "options": null
        },
        {
          "name": "targetX",
          "type": "number",
          "description": "Left edge of the region the camera flies to, as a fraction of frame WIDTH. 0 is the left edge of the frame.",
          "required": false,
          "defaultValue": "0.2",
          "options": null
        },
        {
          "name": "targetY",
          "type": "number",
          "description": "Top edge of the target region, as a fraction of frame HEIGHT. 0 is the top of the frame.",
          "required": false,
          "defaultValue": "0.24",
          "options": null
        },
        {
          "name": "targetW",
          "type": "number",
          "description": "Width of the target region, as a fraction of frame width. Copy a rect out of the GEO object at the top of this file to frame a specific card: the chart is 0.045/0.192/0.555/0.398.",
          "required": false,
          "defaultValue": "0.4",
          "options": null
        },
        {
          "name": "targetH",
          "type": "number",
          "description": "Height of the target region, as a fraction of frame height. The tighter of width and height decides the scale, so the region is always fully inside the held frame and usually a little more.",
          "required": false,
          "defaultValue": "0.3",
          "options": null
        },
        {
          "name": "zoom",
          "type": "number",
          "description": "Camera push, as a multiple of the scale at which the target region exactly fits the frame. 1 puts the region edge to edge on its tighter axis, 0.94 leaves a hair of margin, above 1 crops inside it. The scale is clamped at 1 from below, so the camera can never pull back past the wide shot.",
          "required": false,
          "defaultValue": "0.94",
          "options": null
        },
        {
          "name": "focusX",
          "type": "number",
          "description": "Centre of the annotation ring, in the same frame units as the target region — put it on the thing you actually want circled, which is rarely the exact centre of the rect. The default sits on the chart's peak marker.",
          "required": false,
          "defaultValue": "0.419",
          "options": null
        },
        {
          "name": "focusY",
          "type": "number",
          "description": "Vertical centre of the annotation ring, as a fraction of frame height. The default sits on the chart's peak marker.",
          "required": false,
          "defaultValue": "0.325",
          "options": null
        },
        {
          "name": "reticle",
          "type": "boolean",
          "description": "Bracket the target region with hairline corner marks and a mono scale readout before the push, and again on the way out. Turn it off for a straight cinematic move with no instrumentation.",
          "required": false,
          "defaultValue": "true",
          "options": null
        },
        {
          "name": "seed",
          "type": "string",
          "description": "Seeds the ragged heights of the micro histograms in the tile row along the bottom. Change it for a differently-shaped dashboard of identical duration; the same seed always renders the same frames.",
          "required": false,
          "defaultValue": "region-01",
          "options": null
        },
        {
          "name": "brand",
          "type": "string",
          "description": "Brand token id — one of the keys of BRANDS.",
          "required": false,
          "defaultValue": "slate",
          "options": null
        }
      ]
    }
  ]
}
