{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "width-lock",
  "title": "Width Lock",
  "description": "A condensed line stays width-locked while a word swaps, 'Ship the launch video by tonight.'.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/width-lock/index.tsx",
      "content": "\"use client\";\n\nimport { loadVariableFont } from \"@remotion/google-fonts/RobotoFlex\";\nimport type React from \"react\";\nimport { useEffect, useId, useMemo, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst flex = loadVariableFont(\"oblique 0deg 10deg\", { subsets: [\"latin\"] });\n\nexport interface WidthLockProps {\n  before?: string;\n  words?: string[];\n  after?: string;\n  hold?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst LIMB = [\"#e9ecff\", \"#8a8bff\", \"#4b2bd6\", \"#160c3a\"] as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    ink: \"#F4F3F0\",\n    hl: (a: string) =>\n      `linear-gradient(100deg, ${LIMB[0]} 0%, ${LIMB[1]} 30%, color-mix(in oklab, ${a} 78%, ${LIMB[1]}) 62%, ${LIMB[0]} 100%)`,\n    haze: (a: string) =>\n      [\n        `radial-gradient(64% 52% at 50% 122%, color-mix(in oklab, ${LIMB[2]} 46%, transparent) 0%, color-mix(in oklab, ${LIMB[3]} 50%, transparent) 50%, transparent 80%)`,\n        `radial-gradient(30% 22% at 50% 106%, color-mix(in oklab, ${a} 24%, transparent) 0%, transparent 80%)`,\n      ].join(\", \"),\n    grain: 0.05,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    ink: \"#111113\",\n    hl: (a: string) =>\n      `linear-gradient(100deg, ${LIMB[3]} 0%, ${LIMB[2]} 34%, ${a} 64%, ${LIMB[3]} 100%)`,\n    haze: (a: string) =>\n      `radial-gradient(60% 48% at 50% 120%, color-mix(in oklab, ${LIMB[1]} 34%, transparent) 0%, color-mix(in oklab, ${a} 8%, transparent) 50%, transparent 80%)`,\n    grain: 0.035,\n  },\n} as const;\n\nconst WDTH_MIN = 25;\nconst WDTH_MAX = 151;\nconst SAMPLES = [25, 35, 50, 65, 80, 100, 115, 130, 151];\nconst WGHT = 480;\nconst TRACK = -0.02;\nconst MAX_EXTRA_TRACK = 0.04;\nconst LONGEST_AT = 46;\nconst LINE = 0.98;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN = Easing.bezier(0.55, 0, 0.85, 0.35);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\nconst LOCK_SPRING = { damping: 13, stiffness: 120, mass: 0.9 };\n\nconst fvs = (wdth: number) =>\n  `\"wdth\" ${wdth.toFixed(2)}, \"wght\" ${WGHT}, \"slnt\" 0, \"opsz\" 72`;\n\ntype Curve = number[];\n\nfunction widthAt(curve: Curve, wdth: number) {\n  const w = Math.min(WDTH_MAX, Math.max(WDTH_MIN, wdth));\n  for (let i = 0; i < SAMPLES.length - 1; i++) {\n    if (w <= SAMPLES[i + 1]) {\n      const k = (w - SAMPLES[i]) / (SAMPLES[i + 1] - SAMPLES[i]);\n      return curve[i] + (curve[i + 1] - curve[i]) * k;\n    }\n  }\n  return curve[curve.length - 1];\n}\n\nfunction solveWdth(curve: Curve, target: number) {\n  if (target <= curve[0]) return WDTH_MIN;\n  for (let i = 0; i < SAMPLES.length - 1; i++) {\n    if (target <= curve[i + 1]) {\n      const k = (target - curve[i]) / Math.max(1e-6, curve[i + 1] - curve[i]);\n      return SAMPLES[i] + (SAMPLES[i + 1] - SAMPLES[i]) * k;\n    }\n  }\n  return WDTH_MAX;\n}\n\nfunction measure(strings: string[]): Curve[] {\n  const probe = document.createElement(\"span\");\n  Object.assign(probe.style, {\n    position: \"absolute\",\n    left: \"-99999px\",\n    top: \"0\",\n    visibility: \"hidden\",\n    whiteSpace: \"pre\",\n    fontFamily: `\"${flex.fontFamily}\"`,\n    fontStyle: \"normal\",\n    fontSize: \"100px\",\n    lineHeight: \"1\",\n    letterSpacing: `${TRACK}em`,\n  });\n  document.body.appendChild(probe);\n  const curves = strings.map((s) =>\n    SAMPLES.map((wd) => {\n      probe.style.fontVariationSettings = fvs(wd);\n      probe.textContent = s;\n      return probe.getBoundingClientRect().width / 100 - TRACK;\n    }),\n  );\n  probe.remove();\n  return curves;\n}\n\nfunction useCurves(strings: string[]) {\n  const key = strings.join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; c: Curve[] } | null>(null);\n  const [handle] = useState(() => delayRender(\"width-lock: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      flex.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${flex.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, c: 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.c : null;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `wl${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={5}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\ninterface Fit {\n  wdth: number;\n  extra: number;\n}\n\nconst NATURAL: Fit = { wdth: 100, extra: 0 };\n\nexport function WidthLock({\n  before = \"Ship the\",\n  words = [\"trailer\", \"transitions\", \"captions\", \"launch video\"],\n  after = \"by tonight.\",\n  hold = 30,\n  accent = \"#3B5BFD\",\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: WidthLockProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const list = words.length > 0 ? words : [\"launch video\"];\n  const strings = useMemo(\n    () => [before, after, ...list].map((s) => s || \" \"),\n    [before, after, list],\n  );\n  const curves = useCurves(strings);\n\n  const transparent = background === \"transparent\";\n  const stage = transparent ? \"transparent\" : (background ?? c.bg);\n\n  if (!curves) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const portrait = H > W * 1.1;\n  const measureW = W * (portrait ? 0.86 : 0.74);\n  const rows = (before ? 1 : 0) + 1 + (after ? 1 : 0);\n\n  const live = strings\n    .map((s, i) => ({ s, curve: curves[i] }))\n    .filter((r) => r.s.trim());\n  const floor = Math.max(...live.map((r) => r.curve[0]));\n  const ceil = Math.min(...live.map((r) => r.curve[r.curve.length - 1]));\n  const pref = Math.max(...live.map((r) => widthAt(r.curve, LONGEST_AT)));\n  const M =\n    ceil >= floor\n      ? Math.min(ceil, Math.max(floor, pref))\n      : Math.max(floor, 0.1);\n  const F = Math.min(\n    measureW / M,\n    (H * (portrait ? 0.5 : 0.72)) / (rows * LINE),\n  );\n\n  const fit = (i: number): Fit => {\n    const curve = curves[i];\n    const wdth = solveWdth(curve, M);\n    const n = Math.max(1, Array.from(strings[i]).length - 1);\n    const short = M - widthAt(curve, wdth);\n    return {\n      wdth,\n      extra: short > 0.001 ? Math.min(MAX_EXTRA_TRACK, short / n) : 0,\n    };\n  };\n\n  const LOCK_AT = 18;\n  const FIRST_SWAP = 46;\n  const STEP = Math.max(16, hold);\n  const swapAt = (k: number) => FIRST_SWAP + (k - 1) * STEP;\n  const OUT = 7;\n  const IN_AT = 6;\n  const IN = 10;\n  const SWAP_LOCK = 13;\n\n  const lineH = F * LINE;\n  const blockH = rows * lineH;\n  const top = H / 2 - blockH / 2 - F * 0.02;\n  const cx = W / 2;\n\n  const renderRun = (\n    i: number,\n    state: Fit,\n    key: string,\n    style: React.CSSProperties,\n    gradient: boolean,\n  ) => {\n    const s = strings[i];\n    const n = Math.max(1, Array.from(s).length - 1);\n    const w = (widthAt(curves[i], state.wdth) + state.extra * n) * F;\n    return (\n      <div\n        key={key}\n        style={{\n          position: \"absolute\",\n          left: cx - w / 2,\n          top: 0,\n          height: lineH * 1.24,\n          lineHeight: `${lineH * 1.24}px`,\n          whiteSpace: \"pre\",\n          fontFamily: flex.fontFamily,\n          fontStyle: \"normal\",\n          fontSize: F,\n          fontVariationSettings: fvs(state.wdth),\n          letterSpacing: `${TRACK + state.extra}em`,\n          color: c.ink,\n          ...(gradient\n            ? {\n                backgroundImage: c.hl(accent),\n                backgroundSize: `${Math.max(w, 1)}px 100%`,\n                WebkitBackgroundClip: \"text\",\n                backgroundClip: \"text\",\n                color: \"transparent\",\n              }\n            : null),\n          ...style,\n        }}\n      >\n        {s}\n      </div>\n    );\n  };\n\n  const lerpFit = (a: Fit, b: Fit, k: number): Fit => ({\n    wdth: a.wdth + (b.wdth - a.wdth) * k,\n    extra: Math.max(0, a.extra + (b.extra - a.extra) * k),\n  });\n\n  const lock = (start: number) =>\n    spring({ frame: t - start, fps, config: LOCK_SPRING });\n\n  const riseY = (start: number, len: number) => {\n    const p = interpolate(t, [start, start + len], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    return { y: (1 - p) * lineH * 0.34, o: p, blur: (1 - p) * F * 0.05 };\n  };\n\n  const band = (row: number, children: React.ReactNode, key: string) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: 0,\n        width: W,\n        top: top + row * lineH - lineH * 0.12,\n        height: lineH * 1.24,\n      }}\n    >\n      {children}\n    </div>\n  );\n\n  const bands: React.ReactNode[] = [];\n  let row = 0;\n\n  const fixed = (idx: number, enterAt: number) => {\n    const r = riseY(enterAt, 18);\n    const state = lerpFit(NATURAL, fit(idx), lock(LOCK_AT + row * 3));\n    bands.push(\n      band(\n        row,\n        renderRun(\n          idx,\n          state,\n          `f${idx}`,\n          {\n            transform: `translateY(${r.y.toFixed(2)}px)`,\n            opacity: r.o,\n            filter: r.blur > 0.05 ? `blur(${r.blur.toFixed(2)}px)` : undefined,\n          },\n          false,\n        ),\n        `band${row}`,\n      ),\n    );\n    row++;\n  };\n\n  if (before) fixed(0, 0);\n\n  {\n    const runs: React.ReactNode[] = [];\n    list.forEach((_, k) => {\n      const idx = 2 + k;\n      const enterAt = k === 0 ? 3 : swapAt(k) + IN_AT;\n      const lockAt = k === 0 ? LOCK_AT + row * 3 : swapAt(k) + SWAP_LOCK;\n      const leaveAt = k < list.length - 1 ? swapAt(k + 1) : null;\n      if (t < enterAt - 1) return;\n      if (leaveAt !== null && t > leaveAt + OUT + 1) return;\n      const r = riseY(enterAt, k === 0 ? 18 : IN);\n      const state = lerpFit(NATURAL, fit(idx), lock(lockAt));\n      let o = r.o;\n      let blur = r.blur;\n      let sx = 1;\n      if (leaveAt !== null) {\n        const p = interpolate(t, [leaveAt, leaveAt + OUT], [0, 1], {\n          ...CLAMP,\n          easing: EASE_IN,\n        });\n        sx = 1 - p * 0.94;\n        o *= 1 - p;\n        blur += p * F * 0.03;\n      }\n      runs.push(\n        renderRun(\n          idx,\n          state,\n          `w${k}`,\n          {\n            transform: `translateY(${r.y.toFixed(2)}px) scaleX(${sx.toFixed(4)})`,\n            opacity: o,\n            filter: blur > 0.05 ? `blur(${blur.toFixed(2)}px)` : undefined,\n          },\n          true,\n        ),\n      );\n    });\n    bands.push(band(row, runs, `band${row}`));\n    row++;\n  }\n\n  if (after) fixed(1, 6);\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {transparent ? null : (\n        <div\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            background: c.haze(accent),\n            opacity: interpolate(t, [0, 30], [0.4, 1], CLAMP),\n          }}\n        />\n      )}\n      {bands}\n      {transparent ? null : <Grain opacity={c.grain} />}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/width-lock.tsx"
    }
  ],
  "type": "registry:component"
}
