{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "factor-out",
  "title": "Factor Out",
  "description": "Three short imperatives stack above a big blue \"in your editor.\".",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/factor-out/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Rubik\";\nimport type React from \"react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst rubik = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface FactorOutProps {\n  heads?: string[];\n  tail?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#2A1033\", ink: \"#FFFFFF\", accent: \"#7CC4FF\" },\n  light: { bg: \"#F3EEF5\", ink: \"#2A1033\", accent: \"#2B7BD6\" },\n} as const;\n\nconst DEFAULT_HEADS = [\"Write the scenes\", \"Pick the colors\", \"Fix the typos\"];\nconst DEFAULT_TAIL = \"in your editor.\";\n\nconst WGHT = 500;\nconst TRACK = -0.03;\nconst LH = 1.16;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IN_OUT = Easing.bezier(0.65, 0, 0.35, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ntype Metrics = Record<string, number>;\n\nfunction measureAll(texts: string[]): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\");\n  if (!ctx) throw new Error(\"factor-out: no 2d context\");\n  ctx.font = `${WGHT} 100px \"${rubik.fontFamily}\"`;\n  const w: Metrics = {};\n  for (const s of texts) {\n    w[s] = ctx.measureText(s).width / 100 + TRACK * Array.from(s).length;\n  }\n  return w;\n}\n\nfunction useMetrics(texts: string[]) {\n  const key = texts.join(\"\\u0001\");\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"factor-out: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      rubik.waitUntilDone(),\n      document.fonts.load(`${WGHT} 100px \"${rubik.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measureAll(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\nexport function FactorOut({\n  heads = DEFAULT_HEADS,\n  tail = DEFAULT_TAIL,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: FactorOutProps) {\n  const t = useCurrentFrame() * speed;\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\n  const list = heads.length > 0 ? heads : DEFAULT_HEADS;\n  const n = list.length;\n  const m = useMetrics([\" \", tail, ...list]);\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 mX = W * (portrait ? 0.08 : 0.075);\n  const availW = W - mX * 2;\n  const maxHead = Math.max(...list.map((h) => m[h]));\n  const sp = m[\" \"];\n  const tailW = Math.max(0.1, m[tail]);\n  const rowsUnits = n * LH;\n  let F = Math.min(\n    H * (portrait ? 0.058 : 0.1),\n    (availW * 0.98) / (maxHead + sp + tailW),\n    (H * 0.8) / (rowsUnits + 0.4 + 1.6 * LH),\n  );\n  let G = Math.min(2.3, (availW * 0.98) / (tailW * F));\n  const fitH = (H * 0.82 - F * (rowsUnits + 0.4)) / (F * LH);\n  if (G > fitH) G = Math.max(1, fitH);\n  if (G < 1) {\n    F = F * G;\n    G = 1;\n  }\n  const rowH = F * LH;\n  const gapBelow = F * 0.42;\n  const finalH = n * rowH + gapBelow + G * F * LH;\n  const colX = mX + (maxHead + sp) * F;\n\n  const span = durationInFrames * speed;\n  const k = Math.min(1.5, Math.max(0.85, (span - 50) / 140));\n  const enterAt = (i: number) => (6 + i * 8) * k;\n  const spotAt = (i: number) => (44 + i * 4) * k;\n  const alignAt = 62 * k;\n  const MERGE = 15 * k;\n  const mergeAt = (i: number) => (86 + i * 18) * k;\n  const liftAt = mergeAt(n - 2) + MERGE + 8 * k;\n  const LIFT = 26 * k;\n\n  const lineStyle: React.CSSProperties = {\n    fontFamily: rubik.fontFamily,\n    fontWeight: WGHT,\n    fontSize: F,\n    lineHeight: `${rowH}px`,\n    height: rowH,\n    letterSpacing: `${TRACK}em`,\n    whiteSpace: \"pre\",\n  };\n\n  const nodes: React.ReactNode[] = [];\n\n  const align = interpolate(t, [alignAt, alignAt + 22 * k], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IN_OUT,\n  });\n  const lift = interpolate(t, [liftAt, liftAt + LIFT], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IN_OUT,\n  });\n  const top = (H - finalH) / 2 + (1 - lift) * ((finalH - n * rowH) / 2);\n  const rowY = (i: number) => top + i * rowH;\n  const finalY = top + n * rowH + gapBelow;\n\n  for (let i = 0; i < n; i++) {\n    const d = enterAt(i);\n    if (t < d - 1) continue;\n    const pin = interpolate(t, [d, d + 18], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n    const y = rowY(i);\n    const rise = (1 - pin) * rowH * 0.85;\n    nodes.push(\n      <div\n        key={`h-${i}`}\n        style={{\n          position: \"absolute\",\n          left: mX - F * 0.1,\n          top: y - F * 0.1,\n          height: rowH + F * 0.2,\n          paddingLeft: F * 0.1,\n          paddingRight: F * 0.2,\n          paddingTop: F * 0.1,\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            ...lineStyle,\n            color: c.ink,\n            transform: `translateY(${rise.toFixed(2)}px)`,\n          }}\n        >\n          {list[i]}\n        </div>\n      </div>,\n    );\n\n    const x0 = mX + (m[list[i]] + sp) * F;\n    const xA = x0 + (colX - x0) * align;\n    const last = i === n - 1;\n    const mp = last\n      ? 0\n      : interpolate(t, [mergeAt(i), mergeAt(i) + MERGE], [0, 1], {\n          ...CLAMP,\n          easing: EASE_OUT,\n        });\n    if (mp >= 1) continue;\n    const landAt = i > 0 ? mergeAt(i - 1) + MERGE * 0.35 : -1000;\n    const bump =\n      Math.sin(\n        Math.PI *\n          interpolate(t, [landAt, landAt + 10 * k], [0, 1], { ...CLAMP }),\n      ) * 0.045;\n    const xL = xA + (mX - xA) * lift;\n    const yL = y + (finalY - y) * lift;\n    const scale = 1 + bump + (last ? (G - 1) * lift : 0);\n    const tint = interpolate(t, [spotAt(i), spotAt(i) + 12], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IN_OUT,\n    });\n    nodes.push(\n      <div\n        key={`t-${i}`}\n        style={{\n          position: \"absolute\",\n          left: (last ? xL : xA) - F * 0.1,\n          top: (last ? yL : y) - F * 0.1,\n          height: rowH + F * 0.2,\n          paddingLeft: F * 0.1,\n          paddingRight: F * 0.2,\n          paddingTop: F * 0.1,\n          overflow: pin >= 1 ? \"visible\" : \"hidden\",\n          zIndex: last ? 2 : 1,\n          transform: `scale(${scale.toFixed(4)})`,\n          transformOrigin: `${F * 0.1}px ${F * 0.1}px`,\n        }}\n      >\n        <div\n          style={{\n            ...lineStyle,\n            color: interpolateColors(tint, [0, 1], [c.ink, hi]),\n            opacity: interpolate(mp, [0.05, 0.85], [1, 0], CLAMP),\n            transform: `translateY(${(rise + mp * rowH * 0.55).toFixed(2)}px)`,\n          }}\n        >\n          {tail}\n        </div>\n      </div>,\n    );\n  }\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {nodes}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/factor-out.tsx"
    }
  ],
  "type": "registry:component"
}
