{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "mono-settle",
  "title": "Mono Settle",
  "description": "Two lines set in monospace settle into proportional type: 'From your editor to the edit.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/mono-settle/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadGeist } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadGeistMono } from \"@remotion/google-fonts/GeistMono\";\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 geist = loadGeist(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\nconst geistMono = loadGeistMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface MonoSettleProps {\n  lines?: string[];\n  highlight?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: {\n    pink: \"#F472B6\",\n    bg: \"#14151A\",\n    ink: \"#FBF0DF\",\n    mono: \"rgba(251,240,223,0.72)\",\n  },\n  light: {\n    pink: \"#C2185B\",\n    bg: \"#FBF0DF\",\n    ink: \"#14151A\",\n    mono: \"rgba(20,21,26,0.7)\",\n  },\n} as const;\n\nconst TRACK = -0.035;\nconst LEADING = 1.04;\n\nconst RISE_GAP = 0.9;\nconst RISE_LEN = 16;\nconst LINE_LAG = 4;\nconst WAVE_AT = 36;\nconst WAVE_SPAN = 26;\nconst MORPH_LEN = 14;\nconst HL_AT = 72;\nconst HL_LEN = 14;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface LineMetrics {\n  xm: number[];\n  xp: number[];\n  wm: number;\n  wp: number;\n}\n\ninterface Metrics {\n  lines: LineMetrics[];\n  capP: number;\n  baseP: number;\n  baseM: number;\n}\n\nconst baseOf = (asc: number, desc: number) => (1 - (asc + desc)) / 2 + asc;\n\nfunction measure(lines: string[]): Metrics {\n  const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n  if (!ctx) throw new Error(\"mono-settle: no 2d context\");\n  const run = (s: string, font: string, track: number) => {\n    ctx.font = font;\n    const chars = Array.from(s);\n    const xs: number[] = [];\n    let prefix = \"\";\n    for (let i = 0; i < chars.length; i++) {\n      xs.push(prefix ? ctx.measureText(prefix).width / 100 + track * i : 0);\n      prefix += chars[i];\n    }\n    const total = ctx.measureText(s).width / 100 + track * (chars.length - 1);\n    return { xs, total };\n  };\n  const monoFont = `400 100px \"${geistMono.fontFamily}\"`;\n  const propFont = `500 100px \"${geist.fontFamily}\"`;\n  const out = lines.map((s) => {\n    const mm = run(s, monoFont, 0);\n    const pp = run(s, propFont, TRACK);\n    return { xm: mm.xs, xp: pp.xs, wm: mm.total, wp: pp.total };\n  });\n  ctx.font = propFont;\n  const hp = ctx.measureText(\"H\");\n  ctx.font = monoFont;\n  const hm = ctx.measureText(\"H\");\n  return {\n    lines: out,\n    capP: hp.actualBoundingBoxAscent / 100,\n    baseP: baseOf(\n      hp.fontBoundingBoxAscent / 100,\n      hp.fontBoundingBoxDescent / 100,\n    ),\n    baseM: baseOf(\n      hm.fontBoundingBoxAscent / 100,\n      hm.fontBoundingBoxDescent / 100,\n    ),\n  };\n}\n\nfunction useMetrics(lines: string[]) {\n  const key = lines.join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"mono-settle: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      geist.waitUntilDone(),\n      geistMono.waitUntilDone(),\n      document.fonts.load(`500 100px \"${geist.fontFamily}\"`),\n      document.fonts.load(`400 100px \"${geistMono.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(key.split(\"\\u0001\")) });\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 highlightMask(lines: string[], needle: string) {\n  const masks = lines.map((l) => Array.from(l).map(() => false));\n  const n = needle.trim();\n  if (!n) return masks;\n  lines.forEach((l, li) => {\n    const at = l.indexOf(n);\n    if (at < 0) return;\n    const start = Array.from(l.slice(0, at)).length;\n    const len = Array.from(n).length;\n    for (let i = start; i < start + len; i++) masks[li][i] = true;\n  });\n  return masks;\n}\n\nexport function MonoSettle({\n  lines = [\"From your editor\", \"to the edit.\"],\n  highlight = \"the edit.\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: MonoSettleProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n  const spot = accent ?? c.pink;\n  const list = useMemo(\n    () =>\n      (lines.length > 0 ? lines : [\"From your editor\"]).map((l) => l || \" \"),\n    [lines],\n  );\n  const m = useMetrics(list);\n  const hlMask = useMemo(\n    () => highlightMask(list, highlight),\n    [list, highlight],\n  );\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\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 = Math.min(W, H) * (portrait ? 0.08 : 0.085);\n  const measureW = W - 2 * margin;\n  const maxMono = Math.max(...m.lines.map((l) => l.wm));\n\n  const prog = m.lines.map((l, li) =>\n    l.xm.map((x) => {\n      const at =\n        WAVE_AT + (x / Math.max(0.001, maxMono)) * WAVE_SPAN + li * LINE_LAG;\n      return interpolate(t, [at, at + MORPH_LEN], [0, 1], CLAMP);\n    }),\n  );\n  const xs = m.lines.map((l, li) => {\n    const out: number[] = [];\n    let run = 0;\n    for (let i = 0; i < l.xm.length; i++) {\n      out.push(run);\n      const am = (i + 1 < l.xm.length ? l.xm[i + 1] : l.wm) - l.xm[i];\n      const ap = (i + 1 < l.xp.length ? l.xp[i + 1] : l.wp) - l.xp[i];\n      run += am + (ap - am) * EASE_IO(prog[li][i]);\n    }\n    out.push(run);\n    return out;\n  });\n  const widest = Math.max(...xs.map((x) => x[x.length - 1]));\n  const capF = H * (portrait ? 0.12 : 0.24) * (list.length > 2 ? 0.75 : 1);\n  const F = Math.min(capF, measureW / widest);\n  const lineH = F * LEADING;\n  const lastBase = H - margin;\n  const firstBase = lastBase - lineH * (list.length - 1);\n\n  const hlP = interpolate(t, [HL_AT, HL_AT + HL_LEN], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const glyphs = list.flatMap((line, li) => {\n    const chars = Array.from(line);\n    const baseY = firstBase + li * lineH;\n    return chars.map((ch, i) => {\n      if (ch === \" \") return null;\n      const e = EASE_IO(prog[li][i]);\n      const x = margin + xs[li][i] * F;\n      const r = interpolate(\n        t,\n        [li * LINE_LAG + i * RISE_GAP, li * LINE_LAG + i * RISE_GAP + RISE_LEN],\n        [0, 1],\n        { ...CLAMP, easing: EASE_OUT },\n      );\n      const lift = (1 - r) * F * 1.1;\n      const sansColor = hlMask[li][i]\n        ? `color-mix(in oklab, ${spot} ${(hlP * 100).toFixed(1)}%, ${c.ink})`\n        : c.ink;\n      const top = baseY - F * 1.05;\n      const h = F * 1.3;\n      const cut = (1 - e) * h;\n      return (\n        <div\n          key={`${li}-${i}`}\n          style={{\n            position: \"absolute\",\n            left: x,\n            top,\n            width: F,\n            height: h,\n            overflow: \"hidden\",\n          }}\n        >\n          {e < 0.999 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: F * 1.05 - m.baseM * F + lift,\n                height: F,\n                lineHeight: `${F}px`,\n                fontFamily: geistMono.fontFamily,\n                fontWeight: 400,\n                fontSize: F,\n                whiteSpace: \"pre\",\n                color: c.mono,\n                clipPath: `inset(${Math.max(0, h - cut - (F * 1.05 - m.baseM * F)).toFixed(2)}px 0 0 0)`,\n              }}\n            >\n              {ch}\n            </div>\n          ) : null}\n          {e > 0.001 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: F * 1.05 - m.baseP * F + lift,\n                height: F,\n                lineHeight: `${F}px`,\n                fontFamily: geist.fontFamily,\n                fontWeight: 500,\n                fontSize: F,\n                whiteSpace: \"pre\",\n                color: sansColor,\n                clipPath:\n                  e >= 0.999\n                    ? undefined\n                    : `inset(0 0 ${Math.max(0, F - (h - cut - (F * 1.05 - m.baseP * F))).toFixed(2)}px 0)`,\n              }}\n            >\n              {ch}\n            </div>\n          ) : null}\n        </div>\n      );\n    });\n  });\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {glyphs}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/mono-settle.tsx"
    }
  ],
  "type": "registry:component"
}
