{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "changed-mind",
  "title": "Changed Mind",
  "description": "A big serif pull quote, \"The only tool I use now.\", with a solo-founder attribution.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/changed-mind/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/YoungSerif\";\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 serif = loadFont(\"normal\", { weights: [\"400\"], subsets: [\"latin\"] });\n\nexport interface ChangedMindStep {\n  quote: string;\n  when: string;\n}\n\nexport interface ChangedMindProps {\n  steps?: ChangedMindStep[];\n  name?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst DEFAULT_STEPS: ChangedMindStep[] = [\n  { quote: \"Another tool I won’t use.\", when: \"on day one\" },\n  { quote: \"Another tool I might use.\", when: \"in week two\" },\n  { quote: \"The only tool I use now.\", when: \"two months in\" },\n];\n\nconst THEMES = {\n  dark: { bg: \"#101828\", ink: \"#BFE3FF\", accent: \"#F6C453\" },\n  light: { bg: \"#CFE8FF\", ink: \"#101828\", accent: \"#A66E00\" },\n} as const;\n\nconst TRACK = -0.02;\nconst LH = 1.1;\nconst OPEN = \"“\";\nconst CLOSE = \"”\";\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_IN = Easing.bezier(0.5, 0, 0.75, 0);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Tok {\n  text: string;\n  attach: boolean;\n}\n\ninterface Placed {\n  x: number;\n  line: number;\n}\n\nfunction stripQuotes(s: string) {\n  return s\n    .trim()\n    .replace(/^[\"“”]+/, \"\")\n    .replace(/[\"“”]+$/, \"\");\n}\n\nfunction tokenize(s: string): Tok[] {\n  const words = stripQuotes(s).split(/\\s+/).filter(Boolean);\n  return words.map((word, i) => ({\n    text: i === words.length - 1 ? `${word}${CLOSE}` : word,\n    attach: false,\n  }));\n}\n\nfunction coreOf(s: string) {\n  return s.replace(/[\"“”.,!?;:…]+$/g, \"\").replace(/^[\"“”]+/, \"\");\n}\n\nfunction lcsPairs(a: string[], b: string[]): [number, number][] {\n  const dp: number[][] = Array.from({ length: a.length + 1 }, () =>\n    new Array<number>(b.length + 1).fill(0),\n  );\n  for (let i = a.length - 1; i >= 0; i--) {\n    for (let j = b.length - 1; j >= 0; j--) {\n      dp[i][j] =\n        a[i] === b[j]\n          ? dp[i + 1][j + 1] + 1\n          : Math.max(dp[i + 1][j], dp[i][j + 1]);\n    }\n  }\n  const pairs: [number, number][] = [];\n  let i = 0;\n  let j = 0;\n  while (i < a.length && j < b.length) {\n    if (a[i] === b[j]) {\n      pairs.push([i, j]);\n      i++;\n      j++;\n    } else if (dp[i + 1][j] >= dp[i][j + 1]) {\n      i++;\n    } else {\n      j++;\n    }\n  }\n  return pairs;\n}\n\nfunction place(\n  toks: Tok[],\n  adv: (s: string) => number,\n  space: number,\n  maxEm: number,\n) {\n  const pos: Placed[] = [];\n  const widths: number[] = [0];\n  let line = 0;\n  let x = 0;\n  toks.forEach((tk, i) => {\n    const w = adv(tk.text);\n    const gap = i === 0 || tk.attach || x === 0 ? 0 : space;\n    if (!tk.attach && x > 0 && x + gap + w > maxEm) {\n      line++;\n      x = 0;\n      widths.push(0);\n      pos.push({ x: 0, line });\n      x = w;\n    } else {\n      pos.push({ x: x + gap, line });\n      x += gap + w;\n    }\n    widths[line] = x;\n  });\n  return { pos, lines: line + 1, widths };\n}\n\nfunction wrap(\n  toks: Tok[],\n  adv: (s: string) => number,\n  space: number,\n  maxEm: number,\n) {\n  const greedy = place(toks, adv, space, maxEm);\n  const L = greedy.lines;\n  if (L <= 1) return greedy;\n  const n = toks.length;\n  const ws = toks.map((tk) => adv(tk.text));\n  const glued = (k: number) => toks[k].attach;\n  const lineW = (i: number, j: number) => {\n    let x = 0;\n    for (let k = i; k < j; k++) x += (k === i || glued(k) ? 0 : space) + ws[k];\n    return x;\n  };\n  const avg = lineW(0, n) / L;\n  const INF = 1e12;\n  const dp = Array.from({ length: L + 1 }, () =>\n    new Array<number>(n + 1).fill(INF),\n  );\n  const back = Array.from({ length: L + 1 }, () =>\n    new Array<number>(n + 1).fill(0),\n  );\n  dp[0][0] = 0;\n  for (let l = 1; l <= L; l++) {\n    for (let j = 1; j <= n; j++) {\n      if (j < n && glued(j)) continue;\n      for (let i = l - 1; i < j; i++) {\n        if (dp[l - 1][i] >= INF) continue;\n        const lw = lineW(i, j);\n        if (lw > maxEm + 1e-6) continue;\n        let cost = (lw - avg) ** 2;\n        if (j < n) {\n          const end = toks[j - 1].text;\n          if (/[.?!…][\"”’]?$/.test(end)) cost -= avg * avg * 0.5;\n          else if (/[,;:—–][\"”’]?$/.test(end)) cost -= avg * avg * 0.15;\n        }\n        if (dp[l - 1][i] + cost < dp[l][j]) {\n          dp[l][j] = dp[l - 1][i] + cost;\n          back[l][j] = i;\n        }\n      }\n    }\n  }\n  if (dp[L][n] >= INF) return greedy;\n  const starts: number[] = [];\n  let j = n;\n  for (let l = L; l > 0; l--) {\n    const i = back[l][j];\n    starts.unshift(i);\n    j = i;\n  }\n  const pos: { x: number; line: number }[] = [];\n  const widths: number[] = [];\n  starts.forEach((s, l) => {\n    const e = l + 1 < starts.length ? starts[l + 1] : n;\n    let x = 0;\n    for (let k = s; k < e; k++) {\n      const gap = k === s || glued(k) ? 0 : space;\n      pos.push({ x: x + gap, line: l });\n      x += gap + ws[k];\n    }\n    widths.push(x);\n  });\n  return { pos, lines: L, widths };\n}\n\nfunction useWidths(strings: string[]) {\n  const key = strings.join(\"\\u0001\");\n  const [state, setState] = useState<{\n    key: string;\n    w: Record<string, number>;\n  } | null>(null);\n  const [handle] = useState(() => delayRender(\"changed-mind: measuring\"));\n  const released = useRef(false);\n  const ref = useRef(strings);\n  ref.current = strings;\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      serif.waitUntilDone(),\n      document.fonts.load(`400 100px \"${serif.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        const ctx = document.createElement(\"canvas\").getContext(\"2d\");\n        const w: Record<string, number> = {};\n        if (ctx) ctx.font = `400 100px \"${serif.fontFamily}\"`;\n        for (const s of ref.current) {\n          w[s] = ctx ? ctx.measureText(s).width / 100 : s.length * 0.5;\n        }\n        setState({ key, w });\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.w : null;\n}\n\nexport function ChangedMind({\n  steps = DEFAULT_STEPS,\n  name = \"Ines Vargas, solo founder\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: ChangedMindProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, durationInFrames: T } = 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 = steps.length > 0 ? steps : DEFAULT_STEPS;\n  const rows = list.map((s) => tokenize(s.quote));\n  const strings = Array.from(\n    new Set([\n      \" \",\n      OPEN,\n      `— ${name}`,\n      ...list.map((s) => s.when),\n      ...rows.flatMap((r) => r.map((tk) => tk.text)),\n    ]),\n  );\n  const w = useWidths(strings);\n\n  if (!w) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const adv = (s: string) => (w[s] ?? 0) + TRACK * Array.from(s).length;\n  const space = w[\" \"] ?? 0.25;\n\n  const ids: string[][] = [];\n  const fresh: Set<string>[] = [];\n  rows.forEach((row, k) => {\n    const rowIds = row.map((_, i) => `s${k}-${i}`);\n    const isNew = new Set<string>();\n    if (k > 0) {\n      const pairs = lcsPairs(\n        rows[k - 1].map((tk) => coreOf(tk.text)),\n        row.map((tk) => coreOf(tk.text)),\n      );\n      const matched = new Set<number>();\n      for (const [a, b] of pairs) {\n        rowIds[b] = ids[k - 1][a];\n        matched.add(b);\n      }\n      row.forEach((_, i) => {\n        if (!matched.has(i)) isNew.add(rowIds[i]);\n      });\n    }\n    ids.push(rowIds);\n    fresh.push(isNew);\n  });\n\n  const portrait = H > W * 1.1;\n  const unit = Math.min(W, H);\n  const marginX = W * (portrait ? 0.1 : 0.085);\n  const marginY = H * (portrait ? 0.1 : 0.12);\n  const measure = W - marginX * 2;\n  const maxLines = portrait ? 4 : 3;\n  const budget = H * (portrait ? 0.5 : 0.56);\n\n  let F = portrait ? W * 0.16 : H * 0.235;\n  let layouts = rows.map((r) => wrap(r, adv, space, measure / F));\n  for (let k = 0; k < 60; k++) {\n    const most = Math.max(...layouts.map((l) => l.lines));\n    if (most <= maxLines && most * LH * F <= budget) break;\n    F *= 0.96;\n    layouts = rows.map((r) => wrap(r, adv, space, measure / F));\n  }\n\n  const n = rows.length;\n  const intro = 18;\n  const tail = Math.max(30, T * 0.22);\n  const span = Math.max(1, T - tail - intro);\n  const gap = n > 1 ? span / (n - 1) : span;\n  const TR = Math.min(18, gap * 0.45);\n  const startOf = (k: number) => intro + gap * k - TR;\n\n  let cur = 0;\n  for (let k = 1; k < n; k++) if (t >= startOf(k)) cur = k;\n  const lt = cur > 0 ? t - startOf(cur) : 0;\n  const inTransition = cur > 0 && lt < TR + 14;\n  const from = inTransition ? cur - 1 : cur;\n  const to = cur;\n\n  const mostLines = Math.max(...layouts.map((l) => l.lines));\n  const top = portrait\n    ? Math.max(\n        marginY,\n        (H - mostLines * LH * F - F * 0.7 - unit * 0.054 * 1.3 * 3) / 2 -\n          H * 0.04,\n      )\n    : marginY;\n  const lineH = LH * F;\n  const colorIn = (k: number, id: string) =>\n    k > 0 && fresh[k].has(id) ? hi : c.ink;\n\n  const nodes: React.ReactNode[] = [];\n  const glyph = (\n    key: string,\n    text: string,\n    x: number,\n    y: number,\n    opacity: number,\n    color: string,\n  ) => (\n    <div\n      key={key}\n      style={{\n        position: \"absolute\",\n        left: x,\n        top: y,\n        height: lineH,\n        lineHeight: `${lineH}px`,\n        fontFamily: serif.fontFamily,\n        fontWeight: 400,\n        fontSize: F,\n        letterSpacing: `${TRACK}em`,\n        whiteSpace: \"pre\",\n        color,\n        opacity,\n      }}\n    >\n      {text}\n    </div>\n  );\n\n  const xy = (k: number, i: number) => {\n    const p = layouts[k].pos[i];\n    return { x: marginX + p.x * F, y: top + p.line * lineH };\n  };\n\n  const enterOf = (i: number) =>\n    interpolate(t, [2 + i * 2, 18 + i * 2], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n  const lift = F * 0.3;\n\n  const openIn = enterOf(0);\n  nodes.push(\n    glyph(\n      \"open\",\n      OPEN,\n      marginX - adv(OPEN) * F,\n      top + lift * (1 - openIn),\n      openIn,\n      c.ink,\n    ),\n  );\n\n  if (!inTransition) {\n    rows[to].forEach((tk, i) => {\n      const { x, y } = xy(to, i);\n      const e = to === 0 ? enterOf(i + 1) : 1;\n      nodes.push(\n        glyph(\n          ids[to][i],\n          tk.text,\n          x,\n          y + lift * (1 - e),\n          e,\n          colorIn(to, ids[to][i]),\n        ),\n      );\n    });\n  } else {\n    const idxA = new Map(ids[from].map((id, i) => [id, i]));\n    const idxB = new Map(ids[to].map((id, i) => [id, i]));\n    const glide = interpolate(lt, [TR * 0.18, TR * 0.82], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IO,\n    });\n    let outOrder = 0;\n    ids[from].forEach((id, i) => {\n      const a = xy(from, i);\n      const j = idxB.get(id);\n      if (j !== undefined) {\n        const b = xy(to, j);\n        const gx = a.x + (b.x - a.x) * glide;\n        const gy = a.y + (b.y - a.y) * glide;\n        const col = interpolateColors(\n          glide,\n          [0, 1],\n          [colorIn(from, id), colorIn(to, id)],\n        );\n        const ta = rows[from][i].text;\n        const tb = rows[to][j].text;\n        if (ta === tb) {\n          nodes.push(glyph(id, tb, gx, gy, 1, col));\n        } else {\n          const swap = interpolate(lt, [TR * 0.1, TR * 0.4], [0, 1], CLAMP);\n          if (swap < 1) nodes.push(glyph(`${id}-a`, ta, gx, gy, 1 - swap, col));\n          nodes.push(glyph(id, tb, gx, gy, swap, col));\n        }\n      } else {\n        const u = interpolate(lt, [outOrder, outOrder + TR * 0.34], [0, 1], {\n          ...CLAMP,\n          easing: EASE_IN,\n        });\n        outOrder++;\n        if (u < 1) {\n          nodes.push(\n            glyph(\n              `${id}-out`,\n              rows[from][i].text,\n              a.x,\n              a.y + lift * u,\n              1 - u,\n              colorIn(from, id),\n            ),\n          );\n        }\n      }\n    });\n    let inOrder = 0;\n    ids[to].forEach((id, j) => {\n      if (idxA.has(id)) return;\n      const b = xy(to, j);\n      const s = TR * 0.5 + inOrder * 1.5;\n      const v = interpolate(lt, [s, s + TR * 0.5], [0, 1], {\n        ...CLAMP,\n        easing: EASE_OUT,\n      });\n      inOrder++;\n      if (v > 0) {\n        nodes.push(\n          glyph(\n            id,\n            rows[to][j].text,\n            b.x,\n            b.y + lift * (1 - v),\n            v,\n            colorIn(to, id),\n          ),\n        );\n      }\n    });\n  }\n\n  const L = Math.max(16, unit * 0.054);\n  const attrIn = interpolate(t, [12, 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const whenMix =\n    cur > 0\n      ? interpolate(lt, [TR * 0.3, TR * 0.85], [0, 1], {\n          ...CLAMP,\n          easing: EASE_IO,\n        })\n      : 1;\n  const whenPrev = cur > 0 ? list[cur - 1].when : list[0].when;\n  const whenNext = list[cur].when;\n  const dashW = adv(\"— \") * L;\n  const slotH = L * 1.3;\n  const quoteBottom =\n    top + Math.max(...layouts.map((l) => l.lines)) * lineH + F * 0.7;\n\n  const whenLine = (text: string, dy: number, opacity: number) => (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        top: 0,\n        lineHeight: `${slotH}px`,\n        whiteSpace: \"pre\",\n        transform: `translateY(${dy.toFixed(2)}px)`,\n        opacity,\n      }}\n    >\n      {text}\n    </div>\n  );\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      {nodes}\n      <div\n        style={{\n          position: \"absolute\",\n          left: marginX,\n          width: measure,\n          ...(portrait ? { top: quoteBottom } : { bottom: marginY }),\n          fontFamily: serif.fontFamily,\n          fontWeight: 400,\n          fontSize: L,\n          letterSpacing: \"-0.005em\",\n          opacity: attrIn,\n          transform: `translateY(${((1 - attrIn) * L * 0.6).toFixed(2)}px)`,\n        }}\n      >\n        <div\n          style={{\n            lineHeight: `${slotH}px`,\n            paddingLeft: dashW,\n            textIndent: -dashW,\n            textWrap: \"balance\",\n            color: c.ink,\n            opacity: 0.72,\n          }}\n        >\n          {`— ${name}`}\n        </div>\n        <div\n          style={{\n            position: \"relative\",\n            marginLeft: dashW,\n            height: slotH,\n            width: measure - dashW,\n            overflow: \"hidden\",\n            color: hi,\n          }}\n        >\n          {whenMix < 1\n            ? whenLine(whenPrev, -slotH * whenMix, 1 - whenMix)\n            : null}\n          {whenMix > 0 && cur > 0\n            ? whenLine(whenNext, slotH * (1 - whenMix), whenMix)\n            : null}\n          {cur === 0 ? whenLine(whenNext, 0, 1) : null}\n        </div>\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/changed-mind.tsx"
    }
  ],
  "type": "registry:component"
}
