{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "two-hit",
  "title": "Two Hit",
  "description": "A two-hit headline, \"Streaming now.\", in white then blue, right-aligned on black.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/two-hit/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/Urbanist\";\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 urbanist = loadVariableFont(\"normal\", { subsets: [\"latin\"] });\n\nexport interface TwoHitProps {\n  before?: string;\n  after?: string;\n  hitAt?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#0B0B0B\", ink: \"#FFFFFF\", blue: \"#2F6BFF\" },\n  light: { bg: \"#FFFFFF\", ink: \"#0A0A0A\", blue: \"#1F4FE0\" },\n} as const;\n\nconst WGHT = 400;\nconst REST = -0.035;\nconst FIRST_GAIN = 0.32;\nconst MAX_TRACK = 0.75;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_ATTACK = Easing.bezier(0.2, 0.9, 0.3, 1);\nconst EASE_RELEASE = Easing.bezier(0.45, 0, 0.15, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Metrics {\n  widths: number[][];\n  baseline: number;\n  cap: number;\n}\n\nfunction measure(phrases: string[][]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  let widths = phrases.map((lines) => lines.map((l) => l.length * 0.5));\n  let baseline = 0.8;\n  let cap = 0.7;\n  if (ctx) {\n    ctx.font = `${WGHT} 200px \"${urbanist.fontFamily}\"`;\n    widths = phrases.map((lines) =>\n      lines.map((l) => ctx.measureText(l).width / 200),\n    );\n    const fm = ctx.measureText(\"H\");\n    const asc = fm.fontBoundingBoxAscent / 200;\n    const desc = fm.fontBoundingBoxDescent / 200;\n    baseline = (1 - (asc + desc)) / 2 + asc;\n    cap = fm.actualBoundingBoxAscent / 200;\n  }\n  return { widths, baseline, cap };\n}\n\nfunction useMetrics(phrases: string[][]) {\n  const key = phrases.map((l) => l.join(\"\\u0002\")).join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"two-hit: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const list = key.split(\"\\u0001\").map((l) => l.split(\"\\u0002\"));\n    Promise.all([\n      urbanist.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${urbanist.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(list) });\n        if (!released.current) {\n          released.current = true;\n          continueRender(handle);\n        }\n      });\n    return () => {\n      live = false;\n    };\n  }, [key, handle]);\n  return state && state.key === key ? state.m : null;\n}\n\nfunction pulse(x: number, attack: number, hold: number, release: number) {\n  if (x <= 0) return 0;\n  if (x < attack) return EASE_ATTACK(x / attack);\n  if (x < attack + hold) return 1;\n  return 1 - EASE_RELEASE(Math.min(1, (x - attack - hold) / release));\n}\n\nfunction splitLast(text: string) {\n  const clean = text.trim().replace(/\\s+/g, \" \");\n  const i = clean.lastIndexOf(\" \");\n  if (i < 0) return [clean];\n  return [clean.slice(0, i), clean.slice(i + 1)];\n}\n\nfunction normalizeHex(c: string | undefined, fallback: string) {\n  if (c && /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(c)) return c;\n  return fallback;\n}\n\nexport function TwoHit({\n  before = \"Premieres Friday.\",\n  after = \"Streaming now.\",\n  hitAt = 34,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: TwoHitProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const blue = normalizeHex(accent, c.blue);\n  const a = before.trim() || \"Soon.\";\n  const b = after.trim() || \"Now.\";\n  const phrases = useMemo(() => [splitLast(a), splitLast(b)], [a, b]);\n  const m = useMetrics(phrases);\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\n\n  if (!m) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const portrait = H > W * 1.1;\n  const margin = W * (portrait ? 0.06 : 0.055);\n  const span = W - margin * 2;\n  const lens = phrases.map((lines) => lines.map((l) => Array.from(l).length));\n  const restW = (i: number, j: number) =>\n    m.widths[i][j] + REST * (lens[i][j] - 1);\n  let widest = 0;\n  phrases.forEach((lines, i) => {\n    lines.forEach((_, j) => {\n      widest = Math.max(widest, restW(i, j));\n    });\n  });\n  const rows = Math.max(phrases[0].length, phrases[1].length);\n  const LEAD = 0.98;\n  const F = Math.min(\n    (W * (portrait ? 0.86 : 0.62)) / widest,\n    (H * (portrait ? 0.3 : 0.62)) / (m.cap + (rows - 1) * LEAD),\n  );\n  const peak = (i: number, j: number) =>\n    Math.min(\n      MAX_TRACK,\n      Math.max(REST, (span / F - m.widths[i][j]) / Math.max(1, lens[i][j] - 1)),\n    );\n\n  const h1 = hitAt;\n  const h2 = hitAt + 24;\n  const swapAt = h2 + 7;\n  const e1 = FIRST_GAIN * pulse(t - h1, 4, 0, 16);\n  const e2 = pulse(t - h2, 5, 4, 24);\n  const e = Math.max(e1, e2);\n  const idx = t < swapAt ? 0 : 1;\n  const lines = phrases[idx];\n\n  const enter = (j: number) =>\n    interpolate(t, [2 + j * 4, 20 + j * 4], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n\n  const blockH = (m.cap + (lines.length - 1) * LEAD) * F;\n  const firstBase = H * (portrait ? 0.5 : 0.53) - blockH / 2 + m.cap * F;\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: stage,\n        overflow: \"hidden\",\n        fontFamily: urbanist.fontFamily,\n      }}\n    >\n      {lines.map((line, j) => {\n        const track = REST + e * (peak(idx, j) - REST);\n        const baseY = firstBase + j * LEAD * F;\n        const p = enter(j);\n        const last = j === lines.length - 1 && lines.length > 1;\n        return (\n          <div\n            key={`${idx}-${j}`}\n            style={{\n              position: \"absolute\",\n              right: margin,\n              top: baseY - m.baseline * F - F * 0.1,\n              height: F * 1.25,\n              overflow: \"hidden\",\n            }}\n          >\n            <div\n              style={{\n                paddingTop: F * 0.1,\n                fontSize: F,\n                lineHeight: `${F}px`,\n                fontVariationSettings: `\"wght\" ${WGHT}`,\n                letterSpacing: `${track.toFixed(4)}em`,\n                marginRight: `${(-track).toFixed(4)}em`,\n                whiteSpace: \"pre\",\n                color: last ? blue : c.ink,\n                transform: `translateY(${((1 - p) * F * 1.05).toFixed(2)}px)`,\n              }}\n            >\n              {line}\n            </div>\n          </div>\n        );\n      })}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/two-hit.tsx"
    }
  ],
  "type": "registry:component"
}
