{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "post-script",
  "title": "Post Script",
  "description": "A red card with a \"Thanks for watching\" line and a \"P.S. The next launch film is yours.\".",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/post-script/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Lexend\";\nimport { useEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst lexend = loadFont(\"normal\", {\n  weights: [\"300\", \"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface PostScriptProps {\n  signoff?: string;\n  signature?: string;\n  label?: string;\n  ps?: string;\n  install?: string;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#E8392B\", ink: \"#FFFFFF\", accent: \"#0A0A0A\" },\n  light: { bg: \"#FFFFFF\", ink: \"#0A0A0A\", accent: \"#E8392B\" },\n} as const;\n\nconst TRACK = -0.03;\nconst SMALL_TRACK = -0.01;\nconst LH = 1.08;\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 Spec {\n  text: string;\n  weight: number;\n}\n\nfunction useWidths(specs: Spec[]) {\n  const key = specs.map((s) => `${s.weight}|${s.text}`).join(\"\\u0001\");\n  const [state, setState] = useState<{\n    key: string;\n    w: Record<string, number>;\n  } | null>(null);\n  const [handle] = useState(() => delayRender(\"post-script: measuring\"));\n  const released = useRef(false);\n  const ref = useRef(specs);\n  ref.current = specs;\n  useEffect(() => {\n    let live = true;\n    const weights = Array.from(new Set(ref.current.map((s) => s.weight)));\n    Promise.all([\n      lexend.waitUntilDone(),\n      ...weights.map((wt) =>\n        document.fonts.load(`${wt} 100px \"${lexend.fontFamily}\"`),\n      ),\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        for (const s of ref.current) {\n          const k = `${s.weight}|${s.text}`;\n          if (ctx) {\n            ctx.font = `${s.weight} 100px \"${lexend.fontFamily}\"`;\n            w[k] = ctx.measureText(s.text).width / 100;\n          } else {\n            w[k] = s.text.length * 0.56;\n          }\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\nfunction greedy(\n  words: string[],\n  adv: (s: string) => number,\n  space: number,\n  maxEm: number,\n) {\n  const lines: string[][] = [];\n  let line: string[] = [];\n  let x = 0;\n  for (const word of words) {\n    const w = adv(word);\n    if (line.length > 0 && x + space + w > maxEm) {\n      lines.push(line);\n      line = [word];\n      x = w;\n    } else {\n      x += (line.length > 0 ? space : 0) + w;\n      line.push(word);\n    }\n  }\n  if (line.length > 0) lines.push(line);\n  return lines;\n}\n\nfunction balanced(\n  words: string[],\n  adv: (s: string) => number,\n  space: number,\n  maxEm: number,\n) {\n  const base = greedy(words, adv, space, maxEm);\n  if (base.length <= 1) return base;\n  let lo = 0;\n  let hi = maxEm;\n  for (let k = 0; k < 24; k++) {\n    const mid = (lo + hi) / 2;\n    if (greedy(words, adv, space, mid).length <= base.length) hi = mid;\n    else lo = mid;\n  }\n  return greedy(words, adv, space, hi);\n}\n\nconst splitWords = (s: string) => s.split(/\\s+/).filter(Boolean);\n\nexport function PostScript({\n  signoff = \"Thanks for watching.\",\n  signature = \"— the remocn team\",\n  label = \"P.S.\",\n  ps = \"The next launch film is yours.\",\n  install = \"npx shadcn add @remocn/zoom-blur\",\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: PostScriptProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, durationInFrames } = useVideoConfig();\n  const Tt = durationInFrames * speed;\n  const c = THEMES[theme];\n  const hi = accent ?? c.accent;\n  const stage =\n    background === \"transparent\" ? \"transparent\" : (background ?? c.bg);\n\n  const soWords = splitWords(signoff);\n  const psWords = [label, ...splitWords(ps)].filter(Boolean);\n  const w = useWidths([\n    { text: \" \", weight: 500 },\n    ...soWords.map((text) => ({ text, weight: 500 })),\n    ...psWords.map((text) => ({ text, weight: 500 })),\n    { text: signature, weight: 300 },\n    { text: install, weight: 400 },\n  ]);\n\n  if (!w) {\n    return (\n      <div style={{ position: \"absolute\", inset: 0, background: stage }} />\n    );\n  }\n\n  const adv = (s: string) =>\n    (w[`500|${s}`] ?? s.length * 0.56) + TRACK * Array.from(s).length;\n  const space = w[\"500| \"] ?? 0.25;\n  const smallW = (s: string, wt: number) =>\n    (w[`${wt}|${s}`] ?? s.length * 0.56) + SMALL_TRACK * Array.from(s).length;\n\n  const portrait = H > W * 1.1;\n  const maxW = W * (portrait ? 0.86 : 0.8);\n  const mB = H * (portrait ? 0.2 : 0.1);\n  let fS = portrait ? W * 0.044 : H * 0.044;\n  const fitSmall = Math.min(\n    maxW / Math.max(0.01, smallW(signature, 300)),\n    maxW / Math.max(0.01, smallW(install, 400)),\n  );\n  fS = Math.min(fS, fitSmall);\n  const fI = fS;\n  const sB = 0.46;\n\n  let fB = portrait ? W * 0.145 : H * 0.165;\n  let soLines = balanced(soWords, adv, space, maxW / fB);\n  let psLines = balanced(psWords, adv, space, maxW / fB);\n  const layout = (size: number) => {\n    const soH = soLines.length * LH * size;\n    const sigH = signature ? LH * fS : 0;\n    const gapSig = signature ? size * 0.3 : 0;\n    const blockH = soH + gapSig + sigH;\n    const psH = psLines.length * LH * size;\n    const installH = install ? LH * fI : 0;\n    const sA = (fS * 1.12) / size;\n    const totalA = blockH + size * 0.42 + psH * sA;\n    const totalB =\n      installH + (install ? size * 0.34 : 0) + psH + size * 0.4 + blockH * sB;\n    return { soH, sigH, gapSig, blockH, psH, installH, sA, totalA, totalB };\n  };\n  const limit = H - mB - H * (portrait ? 0.14 : 0.08);\n  for (let k = 0; k < 80; k++) {\n    const L = layout(fB);\n    if (\n      soLines.length <= 3 &&\n      psLines.length <= 3 &&\n      L.totalA <= limit &&\n      L.totalB <= limit\n    )\n      break;\n    fB *= 0.96;\n    soLines = balanced(soWords, adv, space, maxW / fB);\n    psLines = balanced(psWords, adv, space, maxW / fB);\n  }\n  const L = layout(fB);\n\n  const psBottomA = H - mB;\n  const psTopA = psBottomA - L.psH * L.sA;\n  const soTopA = psTopA - fB * 0.42 - L.blockH;\n  const installTop = H - mB - L.installH;\n  const psBottomB = install ? installTop - fB * 0.34 : H - mB;\n  const psTopB = psBottomB - L.psH;\n  const soTopB = psTopB - fB * 0.4 - L.blockH * sB;\n\n  const soStart = 4;\n  const soStagger = 3;\n  const sigAt = soStart + soWords.length * soStagger + 8;\n  const psAt = sigAt + (signature ? 14 : 0);\n  const flipAt = Math.max(psAt + 24, Math.min(Tt * 0.36, psAt + 48));\n  const flipDur = 30;\n  const installAt = flipAt + flipDur - 2;\n\n  const p = interpolate(t, [flipAt, flipAt + flipDur], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const lerp = (a: number, b: number) => a + (b - a) * p;\n\n  const soTop = lerp(soTopA, soTopB);\n  const soScale = lerp(1, sB);\n  const soDim = lerp(1, 0.7);\n  const psTop = lerp(psTopA, psTopB);\n  const psScale = lerp(L.sA, 1);\n\n  const rise = (start: number, dur = 18) =>\n    interpolate(t, [start, start + dur], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n\n  let soIndex = 0;\n  const soNodes = soLines.map((line, li) => (\n    <div\n      key={`so${li}`}\n      style={{\n        position: \"absolute\",\n        left: 0,\n        width: W,\n        top: li * LH * fB,\n        height: LH * fB,\n        lineHeight: `${LH * fB}px`,\n        fontSize: fB,\n        textAlign: \"center\",\n        whiteSpace: \"pre\",\n        letterSpacing: `${TRACK}em`,\n        fontWeight: 500,\n        color: c.ink,\n      }}\n    >\n      {line.map((word, wi) => {\n        const e = rise(soStart + soIndex * soStagger);\n        soIndex++;\n        return (\n          <span key={`sw${wi}`}>\n            {wi > 0 ? \" \" : \"\"}\n            <span\n              style={{\n                display: \"inline-block\",\n                opacity: e,\n                transform: `translateY(${((1 - e) * fB * 0.32).toFixed(2)}px)`,\n              }}\n            >\n              {word}\n            </span>\n          </span>\n        );\n      })}\n    </div>\n  ));\n\n  const sigE = rise(sigAt, 16);\n  const psE = rise(psAt, 16);\n\n  let psIndex = 0;\n  const psNodes = psLines.map((line, li) => (\n    <div\n      key={`ps${li}`}\n      style={{\n        position: \"absolute\",\n        left: 0,\n        width: W,\n        top: li * LH * fB,\n        height: LH * fB,\n        lineHeight: `${LH * fB}px`,\n        fontSize: fB,\n        textAlign: \"center\",\n        whiteSpace: \"pre\",\n        letterSpacing: `${TRACK}em`,\n        fontWeight: 500,\n      }}\n    >\n      {line.map((word, wi) => {\n        const isLabel = psIndex === 0 && label !== \"\";\n        psIndex++;\n        return (\n          <span key={`pw${wi}`}>\n            {wi > 0 ? \" \" : \"\"}\n            <span\n              style={{\n                color: isLabel ? hi : c.ink,\n                fontWeight: isLabel ? 400 : 500,\n              }}\n            >\n              {word}\n            </span>\n          </span>\n        );\n      })}\n    </div>\n  ));\n\n  const wipe = interpolate(t, [installAt, installAt + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  return (\n    <AbsoluteFill\n      style={{\n        background: stage,\n        overflow: \"hidden\",\n        fontFamily: lexend.fontFamily,\n      }}\n    >\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: W,\n          height: L.blockH,\n          transform: `translateY(${soTop.toFixed(2)}px) scale(${soScale.toFixed(4)})`,\n          transformOrigin: `${W / 2}px 0px`,\n          opacity: soDim,\n        }}\n      >\n        {soNodes}\n        {signature ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: 0,\n              width: W,\n              top: L.soH + L.gapSig,\n              height: L.sigH,\n              lineHeight: `${L.sigH}px`,\n              fontSize: fS,\n              fontWeight: 300,\n              textAlign: \"center\",\n              whiteSpace: \"pre\",\n              letterSpacing: `${SMALL_TRACK}em`,\n              color: c.ink,\n              opacity: sigE,\n              transform: `translateY(${((1 - sigE) * fS * 0.5).toFixed(2)}px)`,\n            }}\n          >\n            {signature}\n          </div>\n        ) : null}\n      </div>\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: W,\n          height: L.psH,\n          transform: `translateY(${(psTop + (1 - psE) * fS * 0.6).toFixed(2)}px) scale(${psScale.toFixed(4)})`,\n          transformOrigin: `${W / 2}px 0px`,\n          opacity: psE,\n        }}\n      >\n        {psNodes}\n      </div>\n      {install ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            width: W,\n            top: installTop,\n            height: L.installH,\n            lineHeight: `${L.installH}px`,\n            fontSize: fI,\n            fontWeight: 400,\n            textAlign: \"center\",\n            whiteSpace: \"pre\",\n            letterSpacing: `${SMALL_TRACK}em`,\n            color: hi,\n          }}\n        >\n          <span\n            style={{\n              display: \"inline-block\",\n              clipPath: `inset(-20% ${((1 - wipe) * 100).toFixed(2)}% -20% 0)`,\n              transform: `translateX(${((1 - wipe) * -fI * 0.6).toFixed(2)}px)`,\n            }}\n          >\n            {install}\n          </span>\n        </div>\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/post-script.tsx"
    }
  ],
  "type": "registry:component"
}
