{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "answer-stream",
  "title": "Answer Stream",
  "description": "The beat after send: a macro shot on the button that cuts — hard, on its fastest frame — to the answer building itself on the page, while the camera pulls back 1.364× about a focal point above the frame to keep up with it. Every word arrives hot and cools to the foreground.",
  "dependencies": [
    "remotion",
    "@remotion/google-fonts"
  ],
  "registryDependencies": [
    "https://snapcn.dev/r/snap-cn-ui.json",
    "https://snapcn.dev/r/input.json"
  ],
  "files": [
    {
      "path": "registry/snap-cn/answer-stream/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/Inter\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/SourceSerif4\";\nimport {\n  AbsoluteFill,\n  getRemotionEnvironment,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport { inputStyleContext } from \"@/components/snap-cn/input\";\nimport {\n  clamp01,\n  easings,\n  mixOklch,\n  type SnapCnTheme,\n  useSnapCnTheme,\n} from \"@/lib/snap-cn-ui\";\n\n// Loaded through @remotion/google-fonts, never a CSS variable — a Remotion\n// bundle has none of the app's CSS, so a `var(--font-…)` gets you the right face\n// in the Player and a fallback in the mp4 (design-system rule 4).\nconst { fontFamily: SERIF } = loadSerif(\"normal\", {\n  weights: [\"400\", \"600\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: SANS } = loadSans(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface AnswerCard {\n  title: string;\n  body: string;\n  /** SVG path for the card's lead glyph, on a 24×24 viewBox. */\n  icon?: string;\n}\n\nexport interface AnswerStreamProps {\n  /** The prompt. Fills the pill, and stays in the composer. */\n  question?: string;\n  /** The reply. `\\n` is a hard break; everything else wraps. */\n  answer?: string;\n  /** The line that lands under the reply and introduces the cards. */\n  headline?: string;\n  /** The plan. Each lands as an empty card, then fills. */\n  cards?: AnswerCard[];\n  /** Composer footer. */\n  model?: string;\n\n  // --- timeline (seconds) ---\n  /** When the push into the send button starts accelerating. */\n  commitAt?: number;\n  /** When the shot **cuts** to the answer. Not a transition — one frame. */\n  cutAt?: number;\n  /** When the camera starts pulling back to keep up with the answer. */\n  pullbackAt?: number;\n  /** How long the pull-back takes. */\n  pullbackDuration?: number;\n  /** Streaming rate, in words per second. */\n  wordsPerSecond?: number;\n  /** How long a word stays hot before it has cooled to `foreground`. */\n  coolSeconds?: number;\n\n  // --- camera ---\n  /** Scale of the opening macro shot on the send button. */\n  macroZoom?: number;\n  /** How far in the answer shot starts, relative to where it settles. */\n  pullbackFrom?: number;\n  /** How far past its mark the pull-back goes before easing back onto it. */\n  pullbackUndershoot?: number;\n  /** The pull-back's focal point, as a fraction of the frame. It is *above*\n   *  the top edge, which is what makes the page rise as it shrinks. */\n  focusX?: number;\n  focusY?: number;\n  /** Peak motion blur, in reference px, at the pull-back's fastest frame. */\n  blur?: number;\n\n  /** The send button and the hot edge of the stream. Defaults to `theme.primary`. */\n  accentColor?: string;\n  theme?: Partial<SnapCnTheme>;\n  mode?: \"light\" | \"dark\";\n  speed?: number;\n}\n\n// --- Pure helpers (unit-tested) -------------------------------------------\n\n/**\n * The answer shot's camera scale, where 1 is the framing it settles on.\n *\n * Three segments, all measured: it holds where the cut left it, pulls back on\n * a **symmetric** curve to slightly past its mark, and creeps back onto it.\n *\n * The symmetry is the measurement, not a default. Tracking the column's two\n * edges across the reference puts peak velocity at 48–52% of the move — dead\n * centre, which is a cubic in-out and *not* a spring (a spring peaks at a\n * third). Re-measured on the *rendered* frames against the reference's, the two\n * curves start together, end together, and part by at most **0.048 of scale**\n * around the midpoint — the reference is fractionally front-loaded against a\n * symmetric cubic. A quadratic in-out closes about a third of that. It is not\n * worth a second easing curve: the residual is one frame of timing in the\n * middle of a 33-frame move, and `easings.inOut` is the one the rest of the\n * registry already moves on.\n *\n * `1` is where the shot **settles**, not where the pull-back bottoms out. That\n * distinction is worth a sentence because getting it wrong is silent: normalise\n * against the bottom of the move instead and every constant here is 2.8% out,\n * the layout never lands where it was measured, and the curve reads as lagging\n * the reference by two frames in the middle with the ends still matching.\n */\nexport function shotBScale(\n  fc: number,\n  pullF: number,\n  pullDurF: number,\n  from: number,\n  undershoot: number,\n  recoverF: number,\n): number {\n  if (fc <= pullF) return from;\n  const bottom = 1 - undershoot;\n  const u = clamp01((fc - pullF) / pullDurF);\n  if (u < 1) return from + (bottom - from) * easings.inOut(u);\n  // The reference does not stop dead. It sits ~3 frames at the bottom of the\n  // move and then comes back in by 2.8% — the same slow push that is under\n  // every other shot in the clip, and like the rest of them it runs at a\n  // constant rate, so this is linear and not an ease.\n  //\n  // **Bounded**, unlike the drift itself: model it as an open-ended creep and\n  // the composer walks off the bottom edge somewhere after four seconds, which\n  // is a bug you only ever see in a long config.\n  const v = clamp01((fc - pullF - pullDurF - 3) / recoverF);\n  return bottom + undershoot * v;\n}\n\n/**\n * How cooled a word is: 0 the frame it lands, 1 once it has settled to\n * `foreground`. Words are born hot and cool on a fixed clock, so the hot band\n * is `wordsPerSecond × coolSeconds` words wide no matter how fast the stream\n * runs — on the reference, six.\n */\nexport function heat(fc: number, bornF: number, coolF: number): number {\n  if (coolF <= 0) return 1;\n  return clamp01((fc - bornF) / coolF);\n}\n\n/** Frame at which word `i` of a stream that opened at `startF` lands. */\nexport function wordBirth(\n  i: number,\n  startF: number,\n  wordsPerFrame: number,\n): number {\n  return wordsPerFrame <= 0 ? startF : startF + i / wordsPerFrame;\n}\n\n// --- Icons -----------------------------------------------------------------\n\nfunction Glyph({ d, size, color }: { d: string; size: number; color: string }) {\n  return (\n    <svg\n      viewBox=\"0 0 24 24\"\n      width={size}\n      height={size}\n      fill=\"none\"\n      stroke={color}\n      strokeWidth={1.8}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      style={{ display: \"block\", flexShrink: 0 }}\n    >\n      <title>icon</title>\n      <path d={d} />\n    </svg>\n  );\n}\n\nconst CARD_ICONS = [\n  \"M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4Z\", // pencil\n  \"M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7L12 19\", // link\n  \"M12 2 4 6v6c0 5 3.4 8.9 8 10 4.6-1.1 8-5 8-10V6l-8-4Z\", // shield\n  \"M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20ZM2 12h20M12 2a15 15 0 0 1 0 20 15 15 0 0 1 0-20Z\", // globe\n];\n\nconst DEFAULT_CARDS: AnswerCard[] = [\n  {\n    title: \"Content\",\n    body: \"Build pages around the exact questions buyers ask, so there is something accurate to cite.\",\n  },\n  {\n    title: \"Citations\",\n    body: \"Run outreach to the sources that already get quoted for those questions.\",\n  },\n  {\n    title: \"Authority\",\n    body: \"Earn links from the publishers the models already trust.\",\n  },\n  {\n    title: \"Coverage\",\n    body: \"Place press so the name turns up wherever people go looking.\",\n  },\n];\n\n// --- Streaming text --------------------------------------------------------\n\n/**\n * A block of text that arrives a word at a time, each word hot and cooling.\n *\n * The separator is a text node **between** the spans, never a trailing space\n * inside one: a trailing space at the end of an inline box is stripped by CSS,\n * and per-word spans render as `Noextracharge`.\n */\nfunction Stream({\n  text,\n  fc,\n  startF,\n  wordsPerFrame,\n  coolF,\n  ramp,\n  style,\n}: {\n  text: string;\n  fc: number;\n  startF: number;\n  wordsPerFrame: number;\n  coolF: number;\n  /** Colours from hot (index 0) to cooled (last). Quantised on purpose. */\n  ramp: string[];\n  style: React.CSSProperties;\n}) {\n  // Hard breaks are the author's; everything else is left to wrap.\n  const lines = text.split(\"\\n\");\n  let n = 0;\n  return (\n    <div style={{ ...style, textRendering: \"geometricPrecision\" }}>\n      {lines.map((line, li) => {\n        const words = line.split(\" \");\n        return (\n          // biome-ignore lint/suspicious/noArrayIndexKey: lines are positional\n          <div key={li}>\n            {words.map((w, wi) => {\n              const born = wordBirth(n++, startF, wordsPerFrame);\n              const h = heat(fc, born, coolF);\n              // Hidden, not absent. A word that is removed until its frame lets\n              // the line reflow under it, and every word already on screen\n              // shifts as the next one lands — on the reference they do not\n              // move at all. The cool-down is the reveal; there is no fade\n              // under it, because on the reference a word arrives at full\n              // accent and only the *colour* moves after that.\n              const opacity = fc >= born ? 1 : 0;\n              return (\n                <span\n                  // biome-ignore lint/suspicious/noArrayIndexKey: words are positional\n                  key={wi}\n                  style={{\n                    color:\n                      ramp[\n                        Math.min(\n                          ramp.length - 1,\n                          Math.round(h * (ramp.length - 1)),\n                        )\n                      ],\n                    opacity,\n                  }}\n                >\n                  {wi > 0 ? \" \" : \"\"}\n                  {w}\n                </span>\n              );\n            })}\n          </div>\n        );\n      })}\n    </div>\n  );\n}\n\n// --- The reference's own frame ---------------------------------------------\n//\n// Every number below was measured on a 886×498 recording, in that recording's\n// pixels, at the framing the shot **settles** on. Laying out in these units\n// means no number here is a conversion of a number that was measured.\n\nconst REF_W = 886;\nconst REF_H = 498;\n\nconst PILL = { right: 706, top: 22, h: 28, padX: 15, size: 11 };\nconst ANSWER = { x: 171, top: 82, w: 400, size: 13, line: 17 };\nconst HEAD = { x: 171, top: 152, size: 16 };\nconst ROW = { x: 171, top: 190, w: 128, h: 140, gap: 5, r: 6 };\nconst BOX = { x: 163, y: 362, w: 549, h: 128, r: 20 };\n// The send button, measured inside the composer. It is what the opening macro\n// shot is framed on, so it has to be right in both shots — it is one element.\nconst SEND = { size: 28, r: 9, right: 19, bottom: 28 };\n// Where the macro shot parks the send button, as a fraction of the frame.\n// Measured once the shot has finished decelerating (reference frames 70–82).\nconst MACRO_X = 0.571;\nconst MACRO_Y = 0.591;\n\n// --- Main composition ------------------------------------------------------\n\n/**\n * The beat after you press send: the macro shot on the button, the **cut**, and\n * the answer building itself on the page while the camera pulls back to keep up\n * with it.\n *\n * ## Scene to scene, the frame cuts. It never transitions.\n *\n * Read frame by frame, the reference has exactly one grammar for changing\n * shot, and it is a hard cut — one frame wide, no blur, no ramp. What makes the\n * cuts invisible is not a transition, it is what happens **either side** of\n * them:\n *\n * - **Every cut lands on motion.** The camera here is 8 frames into an\n *   accelerating push toward the button when the cut fires. It is moving fastest\n *   at the moment it is replaced, which is the oldest trick there is for hiding\n *   an edit.\n * - **Every cut is followed by a glide, not a stop.** The frame arrives slightly\n *   off and eases in — measured, the answer page lands 27px high and settles\n *   down over 15 frames on an ease-out. Cut to a static frame and the edit is\n *   the loudest thing in the shot.\n *\n * The only *move* in the whole reference is inside a shot, and there are two\n * kinds: a slow idle drift that never stops, and one fast blurred push to\n * whatever is about to happen.\n *\n * ## The pull-back is fitted to the content, not to the clock\n *\n * As the answer grows — paragraph, then headline, then four cards, then the\n * composer sliding up — the camera pulls back and rises to keep the block in\n * frame. Tracking the column's left and right edges across the move recovers a\n * scale of **1.404× → 1.0** about a fixed point at **(0.5, −0.548)** of the\n * frame: dead centre horizontally, and 273px *above the top edge*. Both edges\n * agree on that point to within 1px, which is what makes it a measurement and\n * not a guess. A focal point above the frame is the whole reason the page\n * appears to rise as it shrinks, instead of collapsing toward its middle.\n *\n * Peak velocity sits at 48–52% of the move, so the curve is a cubic in-out —\n * see `shotBScale`. And the move carries **motion blur** at its fastest frames,\n * derived from the camera's own speed rather than dialled in by eye.\n *\n * ## Containers land empty; content streams into them\n *\n * The pill, and every card, arrive as an empty surface first and fill after.\n * The text itself arrives a word at a time, each word **hot** — at the accent —\n * cooling to `foreground` on a fixed clock, which keeps a moving band of about\n * six words lit at the head of the stream.\n *\n * ## What is not measured\n *\n * The colours and the copy. The reference is a specific product with its own\n * coral, its own four icon tints and its own marketing lines. None of that\n * belongs in a component that lands next to somebody else's `Input`\n * (design-system rule 5). The paint is the shadcn token set, the composer takes\n * its surface from the `Input` primitive's own style context, and every string\n * is a prop.\n */\nexport function AnswerStream({\n  question = \"How do I rank higher in AI answers?\",\n  answer = \"Analysing your prompt gaps…\\nFound 27 prompts you should rank for and don't yet, so your name shows up everywhere AI looks.\",\n  headline = \"Your AI visibility score: 34% — here's how I'd fix it:\",\n  cards = DEFAULT_CARDS,\n  model = \"Auto\",\n  commitAt = 1.0,\n  cutAt = 1.284,\n  pullbackAt = 1.933,\n  pullbackDuration = 1.1,\n  wordsPerSecond = 25,\n  coolSeconds = 0.23,\n  macroZoom = 2.36,\n  pullbackFrom = 1.364,\n  pullbackUndershoot = 0.028,\n  focusX = 0.5,\n  focusY = -0.548,\n  blur = 3,\n  accentColor,\n  theme,\n  mode,\n  speed = 1,\n}: AnswerStreamProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const t = useSnapCnTheme(theme, mode);\n  const ui = inputStyleContext(t);\n  const accent = accentColor ?? t.primary;\n\n  const fc = frame * speed;\n  const stageScale = Math.min(width / REF_W, height / REF_H);\n\n  // A hairline is specified at a 40px control; these surfaces are 128–140 tall,\n  // so the token is walked toward the foreground through the system's own mix\n  // rather than by picking a darker grey (design-system rule 3b). No drop\n  // shadow — under a surface this size a visible one is a grey smear (rule 3).\n  const border = `1px solid ${mixOklch(ui.idleBorder, t.foreground, 0.18)}`;\n\n  // Hot → cooled, quantised to 9 steps and built once. A frame of this shot\n  // colours ~110 words; 110 live `mixOklch` calls per frame is the one thing in\n  // here a Player cannot afford, and nobody can see the ninth of a step.\n  const ramp = Array.from({ length: 9 }, (_, i) =>\n    mixOklch(accent, t.foreground, i / 8),\n  );\n  const mutedRamp = Array.from({ length: 9 }, (_, i) =>\n    mixOklch(t.mutedForeground, t.foreground, i / 8),\n  );\n\n  // --- timeline (frames) ---\n  const commitF = commitAt * fps;\n  const cutF = cutAt * fps;\n  const pullF = pullbackAt * fps;\n  const pullDurF = pullbackDuration * fps;\n  const wpf = wordsPerSecond / fps;\n  const coolF = coolSeconds * fps;\n  const cut = fc >= cutF;\n\n  const isRendering = getRemotionEnvironment().isRendering;\n  // Right for the Player, wrong for the render: parallel render tabs inherit a\n  // stale raster and the type shimmers while standing still.\n  const willChange = isRendering ? undefined : (\"transform\" as const);\n\n  // --- shot A: the macro on the send button -------------------------------\n  //\n  // Same composer as the answer shot, scaled about the button. One element, two\n  // framings, so the two cannot drift apart.\n  const btnX = BOX.x + BOX.w - SEND.right - SEND.size / 2;\n  const btnY = BOX.y + BOX.h - SEND.bottom - SEND.size / 2;\n  // The shot opens still decelerating out of the push that arrived on it —\n  // measured, the button slides 158px left and 14px up over 15 frames.\n  const settle = easings.out(clamp01(fc / 15));\n  // …and closes accelerating into the next one. The cut fires here, at the\n  // fastest frame of the move, which is what hides it.\n  const commit = clamp01((fc - commitF) / Math.max(1, cutF - commitF)) ** 3;\n  // The press, on its own clock so it *causes* the push rather than competing\n  // with it: three frames down, five back. The reference has none — its cursor\n  // is already on the button when the shot opens, and the accelerating push\n  // does all the work. Synthesised, the cut needs a visible cause.\n  const press =\n    fc < commitF\n      ? 0\n      : fc < commitF + 3\n        ? (fc - commitF) / 3\n        : 1 - clamp01((fc - commitF - 3) / 5);\n\n  // --- shot B: the answer -------------------------------------------------\n  const cam = (f: number) =>\n    shotBScale(f, pullF, pullDurF, pullbackFrom, pullbackUndershoot, 26);\n  const sB = cam(fc);\n  // Blur from the camera's own speed: a frame that carries a typical element\n  // d px smears it by d/4. The radius is taken to the frame's *centre*, not its\n  // far edge — a corner-radius model is four times too strong, and measured on\n  // the reference the smear at peak is 4–6px on type that is still legible, not\n  // the wash that comes out of a far-edge estimate.\n  // ponytail: uniform blur, not radial. The reference's is radial about the\n  // focal point; at 3px nobody reads the difference. Per-element radial blur\n  // needs a filter per element — reach for it only if a slower push shows it up.\n  const camBlur = Math.min(\n    blur,\n    (Math.abs(sB - cam(fc - 1)) * (REF_H / 2 - focusY * REF_H)) / 4,\n  );\n  // The cut lands 27px high and eases down. A cut to a static frame is the\n  // loudest edit in the shot.\n  const entry = (1 - easings.out(clamp01((fc - cutF) / 15))) * -27;\n\n  const streamStart = cutF + 0.075 * fps;\n  const headStart = streamStart + (2.717 - 1.359) * fps;\n  const composerIn = easings.out(\n    clamp01((fc - (cutF + (2.442 - 1.284) * fps)) / 12),\n  );\n  const cardsAt = cutF + (2.642 - 1.284) * fps;\n\n  const composer = (\n    <div\n      style={{\n        position: \"absolute\",\n        left: BOX.x,\n        top: BOX.y,\n        width: BOX.w,\n        height: BOX.h,\n        borderRadius: BOX.r,\n        background: t.card,\n        border,\n        boxShadow: \"none\",\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: 28,\n          top: 26,\n          right: 28,\n          fontFamily: SANS,\n          fontSize: 14,\n          lineHeight: 1.4,\n          color: t.foreground,\n          textRendering: \"geometricPrecision\",\n        }}\n      >\n        {question}\n      </div>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 28,\n          right: SEND.right,\n          bottom: SEND.bottom - SEND.size / 2,\n          height: SEND.size,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"space-between\",\n          fontFamily: SANS,\n          fontSize: 9,\n          color: t.mutedForeground,\n        }}\n      >\n        <Glyph d=\"M12 5v14M5 12h14\" size={16} color={t.mutedForeground} />\n        <div style={{ display: \"flex\", alignItems: \"center\", gap: 10 }}>\n          <span>{model}</span>\n          <div\n            style={{\n              width: SEND.size,\n              height: SEND.size,\n              borderRadius: SEND.r,\n              background: accent,\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"center\",\n              transform: `scale(${1 - 0.07 * press})`,\n            }}\n          >\n            <Glyph\n              d=\"M12 19V5M5 12l7-7 7 7\"\n              size={15}\n              color={t.primaryForeground}\n            />\n          </div>\n        </div>\n      </div>\n    </div>\n  );\n\n  return (\n    <AbsoluteFill style={{ background: t.background, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          left: \"50%\",\n          top: \"50%\",\n          width: REF_W,\n          height: REF_H,\n          overflow: \"hidden\",\n          transform: `translate(-50%, -50%) scale(${stageScale})`,\n        }}\n      >\n        {cut ? (\n          // ---- Shot B: the answer.\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              transform: `translateY(${entry}px)`,\n              filter:\n                camBlur > 0.15 ? `blur(${camBlur.toFixed(2)}px)` : undefined,\n              willChange,\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                transform: `scale(${sB})`,\n                transformOrigin: `${focusX * REF_W}px ${focusY * REF_H}px`,\n                willChange,\n              }}\n            >\n              {/* ---- The question, as a pill. It lands empty and fills. */}\n              <div\n                style={{\n                  position: \"absolute\",\n                  right: REF_W - PILL.right,\n                  top: PILL.top,\n                  height: PILL.h,\n                  padding: `0 ${PILL.padX}px`,\n                  borderRadius: PILL.h / 2,\n                  background: t.muted,\n                  display: \"flex\",\n                  alignItems: \"center\",\n                }}\n              >\n                <Stream\n                  text={question}\n                  fc={fc}\n                  startF={cutF}\n                  wordsPerFrame={16 / fps}\n                  coolF={coolF}\n                  ramp={mutedRamp}\n                  style={{\n                    fontFamily: SANS,\n                    fontSize: PILL.size,\n                    lineHeight: 1,\n                    whiteSpace: \"nowrap\",\n                  }}\n                />\n              </div>\n\n              {/* ---- The reply. */}\n              <Stream\n                text={answer}\n                fc={fc}\n                startF={streamStart}\n                wordsPerFrame={wpf}\n                coolF={coolF}\n                ramp={ramp}\n                style={{\n                  position: \"absolute\",\n                  left: ANSWER.x,\n                  top: ANSWER.top,\n                  width: ANSWER.w,\n                  fontFamily: SERIF,\n                  fontWeight: 600,\n                  fontSize: ANSWER.size,\n                  lineHeight: `${ANSWER.line}px`,\n                }}\n              />\n\n              {/* ---- The line that introduces the plan. */}\n              <Stream\n                text={headline}\n                fc={fc}\n                startF={headStart}\n                wordsPerFrame={16 / fps}\n                coolF={coolF}\n                ramp={ramp}\n                style={{\n                  position: \"absolute\",\n                  left: HEAD.x,\n                  top: HEAD.top,\n                  width: ROW.w * cards.length + ROW.gap * (cards.length - 1),\n                  fontFamily: SERIF,\n                  fontWeight: 400,\n                  fontSize: HEAD.size,\n                  lineHeight: 1.25,\n                }}\n              />\n\n              {/* ---- The plan. Each card lands empty, then fills. */}\n              {cards.map((c, i) => {\n                const boxF = cardsAt + i * 3.5;\n                if (fc < boxF) return null;\n                const fill = boxF + 4;\n                return (\n                  <div\n                    key={c.title}\n                    style={{\n                      position: \"absolute\",\n                      left: ROW.x + i * (ROW.w + ROW.gap),\n                      top: ROW.top,\n                      width: ROW.w,\n                      height: ROW.h,\n                      borderRadius: ROW.r,\n                      background: t.card,\n                      border,\n                      opacity: clamp01((fc - boxF) / 3),\n                    }}\n                  >\n                    <div style={{ position: \"absolute\", left: 12, top: 12 }}>\n                      <Glyph\n                        d={c.icon ?? CARD_ICONS[i % CARD_ICONS.length]}\n                        size={11}\n                        color={t.mutedForeground}\n                      />\n                    </div>\n                    <Stream\n                      text={c.title}\n                      fc={fc}\n                      startF={fill}\n                      wordsPerFrame={wpf}\n                      coolF={coolF}\n                      ramp={ramp}\n                      style={{\n                        position: \"absolute\",\n                        left: 12,\n                        top: 32,\n                        right: 12,\n                        fontFamily: SERIF,\n                        fontWeight: 600,\n                        fontSize: 8,\n                        lineHeight: 1.2,\n                      }}\n                    />\n                    <Stream\n                      text={c.body}\n                      fc={fc}\n                      startF={fill + 2}\n                      wordsPerFrame={wpf}\n                      coolF={coolF}\n                      ramp={ramp}\n                      style={{\n                        position: \"absolute\",\n                        left: 12,\n                        top: 46,\n                        right: 12,\n                        fontFamily: SANS,\n                        fontSize: 7,\n                        lineHeight: \"10px\",\n                      }}\n                    />\n                  </div>\n                );\n              })}\n\n              {/* ---- The composer, sliding back up under the answer. */}\n              <div\n                style={{\n                  transform: `translateY(${(1 - composerIn) * (REF_H - BOX.y)}px)`,\n                  willChange,\n                }}\n              >\n                {composer}\n              </div>\n            </div>\n          </div>\n        ) : (\n          // ---- Shot A: the macro on the send button.\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              transform: `translate(${(1 - settle) * 158}px, ${(1 - settle) * 14 + commit * 82}px)`,\n              willChange,\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                transform: `translate(${MACRO_X * REF_W - btnX}px, ${MACRO_Y * REF_H - btnY}px) scale(${macroZoom * (1 + 0.12 * commit)})`,\n                transformOrigin: `${btnX}px ${btnY}px`,\n                willChange,\n              }}\n            >\n              {composer}\n            </div>\n          </div>\n        )}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/snap-cn/answer-stream.tsx"
    }
  ],
  "type": "registry:component"
}