{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "answer-first",
  "title": "Answer First",
  "description": "The question \"Who made this video?\" is answered by a big \"A pull request.\".",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/answer-first/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Outfit\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst outfit = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface AnswerFirstProps {\n  answer?: string;\n  question?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#2445E5\", ink: \"#FFFFFF\", accent: \"#FF8FB1\" },\n  light: { bg: \"#FFFFFF\", ink: \"#2445E5\", accent: \"#E0457B\" },\n} as const;\n\nconst WGHT = 500;\nconst TRACK = -0.035;\nconst Q_TRACK = -0.025;\nconst PITCH = 1.02;\nconst MEASURE_PX = 100;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_MOVE = Easing.bezier(0.7, 0, 0.2, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst A_LEN = 14;\nconst A_STAGGER = 3;\nconst MOVE_AT = 34;\nconst MOVE_LEN = 26;\nconst Q_AT = 46;\nconst Q_STAGGER = 4;\nconst Q_LEN = 14;\nconst MIN_HOLD = 26;\n\nlet measureCtx: CanvasRenderingContext2D | null = null;\n\nfunction ctx2d(weight: number) {\n  if (!measureCtx) {\n    measureCtx = document.createElement(\"canvas\").getContext(\"2d\");\n  }\n  if (!measureCtx) throw new Error(\"answer-first: no 2d context\");\n  measureCtx.font = `${weight} ${MEASURE_PX}px \"${outfit.fontFamily}\"`;\n  return measureCtx;\n}\n\nfunction em(s: string, track = TRACK) {\n  return (\n    ctx2d(WGHT).measureText(s).width / MEASURE_PX + track * Array.from(s).length\n  );\n}\n\nfunction useFontsReady() {\n  const [ready, setReady] = useState(false);\n  const [handle] = useState(() => delayRender(\"answer-first: fonts\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      outfit.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${outfit.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (live) setReady(true);\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [handle]);\n  return ready;\n}\n\ninterface Row {\n  words: { text: string; x: number }[];\n  w: number;\n}\n\ninterface Layout {\n  answer: Row[];\n  answerW: number;\n  question: Row[];\n  F1: number;\n  F2: number;\n  FQ: number;\n  baseline: number;\n  cap: number;\n  portrait: boolean;\n}\n\nfunction split(s: string) {\n  return s.split(/\\s+/).filter(Boolean);\n}\n\nfunction toRow(words: string[], track: number): Row {\n  const space = em(\" \", track);\n  let x = 0;\n  const out = words.map((text) => {\n    const at = x;\n    x += em(text, track) + space;\n    return { text, x: at };\n  });\n  return { words: out, w: em(words.join(\" \"), track) };\n}\n\nfunction balanced(words: string[], n: number): string[][] {\n  if (n <= 1) return [words];\n  let best: string[][] = [words];\n  let bestW = Number.POSITIVE_INFINITY;\n  const walk = (start: number, left: number, acc: string[][]) => {\n    if (left === 1) {\n      const rows = [...acc, words.slice(start)];\n      const w = Math.max(...rows.map((r) => em(r.join(\" \"))));\n      if (w < bestW) {\n        bestW = w;\n        best = rows;\n      }\n      return;\n    }\n    for (let i = start + 1; i <= words.length - left + 1; i++) {\n      walk(i, left - 1, [...acc, words.slice(start, i)]);\n    }\n  };\n  walk(0, n, []);\n  return best;\n}\n\nfunction boxH(n: number) {\n  return (n - 1) * PITCH + 1;\n}\n\nfunction buildLayout(\n  answer: string,\n  question: string,\n  W: number,\n  H: number,\n): Layout {\n  const portrait = H > W * 1.1;\n  const aWords = split(answer);\n  let rows: string[][] = [aWords];\n  let F1 = 0;\n  const maxN = Math.min(aWords.length, portrait ? 4 : 2);\n  for (let n = 1; n <= maxN; n++) {\n    const cand = balanced(aWords, n);\n    const w = Math.max(...cand.map((r) => em(r.join(\" \"))));\n    const f = Math.min(\n      (W * (portrait ? 0.86 : 0.84)) / w,\n      (H * (portrait ? 0.5 : 0.62)) / boxH(n),\n    );\n    if (f > F1 * 1.12) {\n      F1 = f;\n      rows = cand;\n    }\n  }\n  const answerRows = rows.map((r) => toRow(r, TRACK));\n  const answerW = Math.max(...answerRows.map((r) => r.w));\n  const F2 = Math.min(\n    F1 * (portrait ? 0.8 : 0.74),\n    (W * (portrait ? 0.86 : 0.7)) / answerW,\n    (H * (portrait ? 0.38 : 0.5)) / boxH(answerRows.length),\n  );\n\n  const qWords = split(question);\n  const qMeasure = W * (portrait ? 0.86 : 0.62);\n  let FQ = Math.min(F2 * (portrait ? 0.6 : 0.56), H * 0.11);\n  let qRows: Row[] = [];\n  for (let guard = 0; guard < 12; guard++) {\n    const out: string[][] = [];\n    let cur: string[] = [];\n    for (const w of qWords) {\n      const next = [...cur, w];\n      if (cur.length > 0 && em(next.join(\" \"), Q_TRACK) * FQ > qMeasure) {\n        out.push(cur);\n        cur = [w];\n      } else {\n        cur = next;\n      }\n    }\n    if (cur.length) out.push(cur);\n    const even = out.length > 1 ? balanced(qWords, out.length) : out;\n    qRows = even.map((r) => toRow(r, Q_TRACK));\n    const widest = Math.max(...qRows.map((r) => r.w)) * FQ;\n    if (qRows.length <= (portrait ? 4 : 3) && widest <= qMeasure) break;\n    FQ *= 0.9;\n  }\n\n  const c = ctx2d(WGHT);\n  const fm = c.measureText(\"Hxg\");\n  const asc = fm.fontBoundingBoxAscent / MEASURE_PX;\n  const desc = fm.fontBoundingBoxDescent / MEASURE_PX;\n  const cap = c.measureText(\"H\").actualBoundingBoxAscent / MEASURE_PX;\n  const baseline = (1 - asc - desc) / 2 + asc;\n\n  return {\n    answer: answerRows,\n    answerW,\n    question: qRows,\n    F1,\n    F2,\n    FQ,\n    baseline,\n    cap,\n    portrait,\n  };\n}\n\nexport function AnswerFirst({\n  answer = \"A pull request.\",\n  question = \"Who made this video?\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: AnswerFirstProps) {\n  const frame = useCurrentFrame();\n  const { width: W, height: H, durationInFrames } = useVideoConfig();\n  const c = THEMES[theme];\n  const hi = accent ?? c.accent;\n  const stage =\n    background === \"transparent\" ? \"transparent\" : (background ?? c.bg);\n  const ready = useFontsReady();\n\n  const layout = useMemo(\n    () => (ready ? buildLayout(answer, question, W, H) : null),\n    [ready, answer, question, W, H],\n  );\n\n  if (!layout) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const qCount = layout.question.reduce((n, r) => n + r.words.length, 0);\n  const motionEnd = Q_AT + (qCount - 1) * Q_STAGGER + Q_LEN;\n  const room = durationInFrames * speed - MIN_HOLD;\n  const k = Math.max(0.55, Math.min(1, room / motionEnd));\n  const t = (frame * speed) / k;\n\n  const { F1, F2, FQ, baseline, cap, answerW, portrait } = layout;\n  const mx = W * (portrait ? 0.08 : 0.07);\n  const my = H * (portrait ? 0.07 : 0.1);\n  const nA = layout.answer.length;\n\n  const p = interpolate(t, [MOVE_AT, MOVE_AT + MOVE_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_MOVE,\n  });\n  const creep = interpolate(t, [A_LEN, MOVE_AT], [0, 0.02], CLAMP);\n  const sEnd = F2 / F1;\n  const s = Math.exp(Math.log(sEnd) * p) * (1 + creep * (1 - p));\n\n  const visTop = baseline - cap;\n  const visBottom = (nA - 1) * PITCH + baseline;\n  const bx0 = W / 2 - (answerW * F1 * s) / 2;\n  const by0 = H / 2 - ((visTop + visBottom) / 2) * F1 * s;\n  const bx1 = W - mx - answerW * F2;\n  const by1 = H - my - visBottom * F2;\n  const bx = bx0 + (bx1 - bx0) * p;\n  const by = by0 + (by1 - by0) * p;\n  const align = p;\n\n  const R = F1;\n  const travel = 1.15 * R;\n  const clipTop = (baseline - 1.0) * R;\n  const clipH = 1.3 * R;\n\n  let wordIndex = 0;\n  const answerNodes = layout.answer.map((row, li) => {\n    const offset = ((answerW - row.w) / 2) * (1 + align);\n    return (\n      <div\n        key={`a-${li}`}\n        style={{\n          position: \"absolute\",\n          left: (offset - 0.2) * R,\n          top: li * PITCH * R + clipTop,\n          width: (row.w + 0.5) * R,\n          height: clipH,\n          overflow: \"hidden\",\n        }}\n      >\n        {row.words.map((w) => {\n          const i = wordIndex++;\n          const q = interpolate(\n            t,\n            [i * A_STAGGER, i * A_STAGGER + A_LEN],\n            [0, 1],\n            { ...CLAMP, easing: EASE_OUT },\n          );\n          return (\n            <div\n              key={`aw-${i}`}\n              style={{\n                position: \"absolute\",\n                left: (w.x + 0.2) * R,\n                top: -clipTop,\n                height: R,\n                fontFamily: outfit.fontFamily,\n                fontWeight: WGHT,\n                fontSize: R,\n                lineHeight: `${R}px`,\n                letterSpacing: `${TRACK}em`,\n                whiteSpace: \"pre\",\n                color: c.ink,\n                transform: `translateY(${((1 - q) * travel).toFixed(2)}px)`,\n              }}\n            >\n              {w.text}\n            </div>\n          );\n        })}\n      </div>\n    );\n  });\n\n  const qTop = my - visTop * FQ;\n  const qClipTop = (baseline - 1.0) * FQ;\n  let qIndex = 0;\n  const questionNodes = layout.question.map((row, li) => (\n    <div\n      key={`q-${li}`}\n      style={{\n        position: \"absolute\",\n        left: mx - 0.2 * FQ,\n        top: qTop + li * PITCH * FQ + qClipTop,\n        width: (row.w + 0.5) * FQ,\n        height: 1.3 * FQ,\n        overflow: \"hidden\",\n      }}\n    >\n      {row.words.map((w) => {\n        const i = qIndex++;\n        const at = Q_AT + i * Q_STAGGER;\n        const q = interpolate(t, [at, at + Q_LEN], [0, 1], {\n          ...CLAMP,\n          easing: EASE_OUT,\n        });\n        return (\n          <div\n            key={`qw-${i}`}\n            style={{\n              position: \"absolute\",\n              left: (w.x + 0.2) * FQ,\n              top: -qClipTop,\n              height: FQ,\n              fontFamily: outfit.fontFamily,\n              fontWeight: WGHT,\n              fontSize: FQ,\n              lineHeight: `${FQ}px`,\n              letterSpacing: `${Q_TRACK}em`,\n              whiteSpace: \"pre\",\n              color: hi,\n              transform: `translateY(${((1 - q) * 1.15 * FQ).toFixed(2)}px)`,\n            }}\n          >\n            {w.text}\n          </div>\n        );\n      })}\n    </div>\n  ));\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {questionNodes}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          transformOrigin: \"0 0\",\n          transform: `translate(${bx.toFixed(2)}px, ${by.toFixed(2)}px) scale(${s.toFixed(5)})`,\n        }}\n      >\n        {answerNodes}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/answer-first.tsx"
    }
  ],
  "type": "registry:component"
}
