{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "half-built",
  "title": "Half Built",
  "description": "A sentence where unfinished verbs are outlined and fill in, tagged Summer and Later.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/half-built/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/GolosText\";\nimport type React from \"react\";\nimport { useEffect, useLayoutEffect, useRef, useState } from \"react\";\nimport {\n  AbsoluteFill,\n  continueRender,\n  delayRender,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst golos = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface HalfBuiltPart {\n  text: string;\n  when?: string;\n}\n\nexport interface HalfBuiltProps {\n  parts?: HalfBuiltPart[];\n  progress?: number;\n  accent?: string;\n  background?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#1D2B53\", ink: \"#FFFFFF\", accent: \"#FFB38A\", hollow: 0.62 },\n  light: { bg: \"#FFF4EC\", ink: \"#1D2B53\", accent: \"#D9623B\", hollow: 0.55 },\n} as const;\n\nconst DEFAULT_PARTS: HalfBuiltPart[] = [\n  { text: \"It cuts,\" },\n  { text: \"captions,\", when: \"Spring\" },\n  { text: \"narrates\", when: \"Summer\" },\n  { text: \"and\" },\n  { text: \"translates\", when: \"Later\" },\n  { text: \"your launch film.\" },\n];\n\nconst LH = 1.34;\nconst TAG_EM = 0.22;\nconst WORD_GAP = 3;\nconst TAG_GAP = 5;\nconst FILL_FULL = 30;\nconst FILL_PAUSE = 12;\nconst SETTLE = 10;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst EASE_FILL = Easing.bezier(0.45, 0, 0.25, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\ninterface Unit {\n  key: string;\n  text: string;\n  plan: number;\n  when?: string;\n}\n\nfunction toUnits(parts: HalfBuiltPart[]) {\n  const units: Unit[] = [];\n  let plan = 0;\n  parts.forEach((p, pi) => {\n    if (p.when !== undefined) {\n      units.push({ key: `p${pi}`, text: p.text.trim(), plan, when: p.when });\n      plan += 1;\n    } else {\n      p.text\n        .split(/\\s+/)\n        .filter(Boolean)\n        .forEach((w, wi) => {\n          units.push({ key: `p${pi}-${wi}`, text: w, plan: -1 });\n        });\n    }\n  });\n  return { units, planned: plan };\n}\n\nexport function HalfBuilt({\n  parts = DEFAULT_PARTS,\n  progress = 1.6,\n  accent,\n  background,\n  speed = 1,\n  theme = \"dark\",\n}: HalfBuiltProps) {\n  const frame = useCurrentFrame();\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 { units, planned } = toUnits(parts);\n  const goal = Math.max(0, Math.min(planned, progress));\n  const wordsIn = 2 + units.length * WORD_GAP + 12;\n  const tagsAt = Math.max(16, wordsIn - 8);\n  const fillAt = tagsAt + planned * TAG_GAP + 14;\n  const segments: { start: number; end: number; amount: number }[] = [];\n  let cursor = fillAt;\n  for (let j = 0; j < Math.ceil(goal); j++) {\n    const amount = Math.min(1, goal - j);\n    const dur = Math.max(16, FILL_FULL * amount);\n    segments.push({ start: cursor, end: cursor + dur, amount });\n    cursor += dur + FILL_PAUSE + (amount >= 1 ? SETTLE : 0);\n  }\n  const naturalEnd = segments.length\n    ? segments[segments.length - 1].end + SETTLE\n    : fillAt;\n  const pace = Math.max(1, (naturalEnd + 30) / Math.max(1, durationInFrames));\n  const t = frame * speed * pace;\n\n  const portrait = H > W * 1.1;\n  const margin = W * (portrait ? 0.08 : 0.07);\n  const boxW = W - margin * 2;\n  const maxH = H * (portrait ? 0.62 : 0.66);\n  const sizeCap = Math.min(H * 0.2, W * (portrait ? 0.16 : 0.12));\n\n  const probeRef = useRef<HTMLDivElement>(null);\n  const [handle] = useState(() => delayRender(\"half-built: measuring\"));\n  const released = useRef(false);\n  const [fontsOk, setFontsOk] = useState(false);\n  const [size, setSize] = useState<number | null>(null);\n\n  useEffect(() => {\n    let live = true;\n    Promise.all([\n      golos.waitUntilDone(),\n      document.fonts.load(`500 100px \"${golos.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (live) setFontsOk(true);\n      });\n    return () => {\n      live = false;\n    };\n  }, []);\n\n  useLayoutEffect(() => {\n    if (!fontsOk) return;\n    const probe = probeRef.current;\n    if (!probe) return;\n    let s = sizeCap;\n    for (let i = 0; i < 60; i++) {\n      probe.style.fontSize = `${s}px`;\n      if (probe.offsetHeight <= maxH && probe.scrollWidth <= boxW + 1) break;\n      s *= 0.96;\n    }\n    const next = Math.round(s * 10) / 10;\n    setSize((prev) => (prev === next ? prev : next));\n  });\n\n  useEffect(() => {\n    if (size !== null && !released.current) {\n      released.current = true;\n      continueRender(handle);\n    }\n  }, [size, handle]);\n\n  const fillOf = (plan: number) => {\n    const seg = segments[plan];\n    if (!seg) return 0;\n    return (\n      interpolate(t, [seg.start, seg.end], [0, 1], {\n        ...CLAMP,\n        easing: EASE_FILL,\n      }) * seg.amount\n    );\n  };\n  const doneOf = (plan: number) => {\n    const seg = segments[plan];\n    if (!seg || seg.amount < 1) return 0;\n    return interpolate(t, [seg.end + 2, seg.end + 2 + SETTLE], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n  };\n  const rise = (start: number, dur = 12) =>\n    interpolate(t, [start, start + dur], [0, 1], {\n      ...CLAMP,\n      easing: EASE_OUT,\n    });\n\n  const font: React.CSSProperties = {\n    fontFamily: golos.fontFamily,\n    fontWeight: 500,\n    lineHeight: LH,\n    letterSpacing: \"-0.035em\",\n  };\n  const stroke = `${Math.max(1, (size ?? 100) * 0.016).toFixed(2)}px`;\n\n  const renderUnit = (u: Unit, i: number, live: boolean) => {\n    const p = live ? rise(2 + i * WORD_GAP) : 1;\n    const style: React.CSSProperties = {\n      position: \"relative\",\n      display: \"inline-block\",\n      whiteSpace: \"nowrap\",\n      marginRight: \"0.24em\",\n      opacity: p,\n      transform: `translateY(${((1 - p) * 0.28).toFixed(3)}em)`,\n    };\n    if (u.plan < 0) {\n      return (\n        <span key={u.key} style={{ ...style, color: c.ink }}>\n          {u.text}\n        </span>\n      );\n    }\n    const fill = live ? fillOf(u.plan) : 0;\n    const done = live ? doneOf(u.plan) : 0;\n    const tagP = live ? rise(tagsAt + u.plan * TAG_GAP, 14) : 1;\n    return (\n      <span key={u.key} style={style}>\n        <span\n          style={{\n            color: \"transparent\",\n            WebkitTextStroke: `${stroke} ${c.ink}`,\n            opacity: c.hollow * (1 - done),\n          }}\n        >\n          {u.text}\n        </span>\n        <span\n          aria-hidden\n          style={{\n            position: \"absolute\",\n            left: 0,\n            top: 0,\n            color: done > 0 ? c.ink : hi,\n            opacity: 1,\n            clipPath: `inset(-20% ${((1 - fill) * 100).toFixed(2)}% -20% -2%)`,\n          }}\n        >\n          <span style={{ color: hi, opacity: 1 - done }}>{u.text}</span>\n          <span\n            style={{\n              position: \"absolute\",\n              left: 0,\n              top: 0,\n              color: c.ink,\n              opacity: done,\n            }}\n          >\n            {u.text}\n          </span>\n        </span>\n        <span\n          style={{\n            position: \"absolute\",\n            left: \"0.02em\",\n            bottom: `${((LH - 0.1) / TAG_EM).toFixed(3)}em`,\n            fontSize: `${TAG_EM}em`,\n            lineHeight: 1,\n            letterSpacing: \"0em\",\n            color: hi,\n            whiteSpace: \"nowrap\",\n            opacity: tagP * (1 - done),\n            transform: `translateY(${((1 - tagP) * 0.5).toFixed(3)}em)`,\n          }}\n        >\n          {u.when}\n        </span>\n      </span>\n    );\n  };\n\n  return (\n    <AbsoluteFill style={{ background: stage, overflow: \"hidden\" }}>\n      <div\n        ref={probeRef}\n        style={{\n          ...font,\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: boxW,\n          visibility: \"hidden\",\n        }}\n      >\n        {units.map((u, i) => renderUnit(u, i, false))}\n      </div>\n      <div\n        style={{\n          ...font,\n          position: \"absolute\",\n          left: margin,\n          top: \"50%\",\n          width: boxW,\n          fontSize: size ?? sizeCap,\n          transform: \"translateY(-50%)\",\n          opacity: size === null ? 0 : 1,\n        }}\n      >\n        {units.map((u, i) => renderUnit(u, i, true))}\n      </div>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/half-built.tsx"
    }
  ],
  "type": "registry:component"
}
