{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stem-stretch",
  "title": "Stem Stretch",
  "description": "The stems of '4 hours' stretch into vertical stripes with a gradient glow, small caption below.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/stem-stretch/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadInstrument } from \"@remotion/google-fonts/InstrumentSans\";\nimport type React from \"react\";\nimport { useEffect, useId, 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 instrument = loadInstrument(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface StemStretchProps {\n  value?: string;\n  label?: string;\n  steps?: number;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst THEMES = {\n  dark: { bg: \"#0A0A0B\", ink: \"#F4F3F0\", muted: \"#8E8D94\", grain: 0.05 },\n  light: { bg: \"#F5F4F0\", ink: \"#121214\", muted: \"#6E6C67\", grain: 0.035 },\n};\n\nconst STRETCH = {\n  dark: (accent: string) =>\n    `linear-gradient(180deg, #F4F3F0 0%, #FFF1E2 8%, #FF8A3D 34%, #C2452A 58%, ${accent} 92%, #F4F3F0 100%)`,\n  light: (accent: string) =>\n    `linear-gradient(180deg, #121214 0%, #3A1A14 6%, #F2552C 34%, #E889B5 62%, ${accent} 92%, #121214 100%)`,\n};\nconst GLOW = {\n  dark: \"radial-gradient(60% 50% at 50% 50%, color-mix(in oklab, #ff8a3d 10%, transparent) 0%, transparent 70%), radial-gradient(80% 50% at 50% 115%, color-mix(in oklab, #a3301a 30%, transparent) 0%, transparent 70%)\",\n  light:\n    \"radial-gradient(60% 50% at 50% 50%, color-mix(in oklab, #f7b23b 12%, transparent) 0%, transparent 70%), radial-gradient(80% 50% at 50% 115%, color-mix(in oklab, #f2552c 12%, transparent) 0%, transparent 70%)\",\n};\n\nconst TRACK = -0.035;\nconst RISE_LEN = 18;\nconst RISE_GAP = 3;\nconst SNAP_AT = 26;\nconst SNAP_GAP = 12;\nconst LABEL_AT = 66;\nconst SLICE = 0.5;\n\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst FONT_PX = 400;\n\ninterface Metrics {\n  width: number;\n  label: number;\n  adv: number[];\n  baseline: number;\n  cap: number;\n  cut: number;\n  desc: number;\n}\n\nfunction findCut(ctx: CanvasRenderingContext2D, text: string, cap: number) {\n  const W = Math.ceil(ctx.measureText(text).width) + 40;\n  const H = Math.ceil(FONT_PX * 1.4);\n  ctx.canvas.width = W;\n  ctx.canvas.height = H;\n  ctx.font = `500 ${FONT_PX}px \"${instrument.fontFamily}\"`;\n  ctx.textBaseline = \"alphabetic\";\n  ctx.fillStyle = \"#000\";\n  const base = Math.round(FONT_PX * 1.1);\n  ctx.fillText(text, 20, base);\n  const data = ctx.getImageData(0, 0, W, H).data;\n  const a = (x: number, y: number) => data[(y * W + x) * 4 + 3];\n  const capPx = cap * FONT_PX;\n  let best = 0.45;\n  let bestScore = Number.POSITIVE_INFINITY;\n  for (let f = 0.3; f <= 0.66; f += 0.01) {\n    const y = Math.round(base - capPx * f);\n    let diff = 0;\n    let ink = 0;\n    for (let x = 0; x < W; x++) {\n      const v = a(x, y);\n      ink += v;\n      diff += Math.abs(v - a(x, y - 3)) + Math.abs(v - a(x, y + 3));\n    }\n    if (ink === 0) continue;\n    const score = diff / ink + Math.abs(f - 0.48) * 0.6;\n    if (score < bestScore) {\n      bestScore = score;\n      best = f;\n    }\n  }\n  return best * cap;\n}\n\nfunction measure(text: string, label: string): Metrics {\n  const canvas = document.createElement(\"canvas\");\n  const ctx = canvas.getContext(\"2d\", { willReadFrequently: true });\n  if (!ctx) throw new Error(\"stem-stretch: no 2d context\");\n  ctx.font = `500 ${FONT_PX}px \"${instrument.fontFamily}\"`;\n  const chars = Array.from(text);\n  const adv = chars.map((ch) => ctx.measureText(ch).width / FONT_PX + TRACK);\n  const width =\n    ctx.measureText(text).width / FONT_PX +\n    TRACK * Math.max(0, chars.length - 1);\n  const fm = ctx.measureText(\"H\");\n  const asc = fm.fontBoundingBoxAscent / FONT_PX;\n  const desc = fm.fontBoundingBoxDescent / FONT_PX;\n  const cap = fm.actualBoundingBoxAscent / FONT_PX;\n  const inkDesc = Math.max(\n    0,\n    ctx.measureText(text).actualBoundingBoxDescent / FONT_PX,\n  );\n  ctx.font = `400 ${FONT_PX}px \"${instrument.fontFamily}\"`;\n  const lab = label ? ctx.measureText(label).width / FONT_PX : 0;\n  const cut = findCut(ctx, text, cap);\n  return {\n    width,\n    label: lab,\n    adv,\n    baseline: (1 - (asc + desc)) / 2 + asc,\n    cap,\n    cut,\n    desc: inkDesc,\n  };\n}\n\nfunction useMetrics(text: string, label: string) {\n  const key = `${text}\\u0000${label}`;\n  const [state, setState] = useState<{ key: string; m: Metrics } | null>(null);\n  const [handle] = useState(() => delayRender(\"stem-stretch: measuring\"));\n  const released = useRef(false);\n  useEffect(() => {\n    let live = true;\n    const [tx, lb] = key.split(\"\\u0000\");\n    Promise.all([\n      instrument.waitUntilDone(),\n      document.fonts.load(`500 100px \"${instrument.fontFamily}\"`),\n      document.fonts.load(`400 100px \"${instrument.fontFamily}\"`),\n    ])\n      .catch(() => undefined)\n      .then(() => {\n        if (!live) return;\n        setState({ key, m: measure(tx, lb) });\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?.m ?? null;\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `ss${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{\n        position: \"absolute\",\n        inset: 0,\n        opacity,\n        mixBlendMode: \"overlay\",\n        pointerEvents: \"none\",\n      }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={3}\n        />\n        <feColorMatrix type=\"saturate\" values=\"0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nexport function StemStretch({\n  value = \"4 hours\",\n  label = \"back, every week\",\n  steps = 4,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: StemStretchProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const m = useMetrics(value, label);\n\n  if (!m) {\n    return <div style={{ position: \"absolute\", inset: 0, background: c.bg }} />;\n  }\n\n  const n = Math.max(1, Math.round(steps));\n  const portrait = H > W;\n  const F = Math.min(H * (portrait ? 0.2 : 0.3), (W * 0.84) / m.width);\n  const capPx = m.cap * F;\n  const gapMax = Math.max(capPx * 0.6, H * (portrait ? 0.42 : 0.62) - capPx);\n  const block = gapMax / n;\n\n  let snapped = 0;\n  for (let k = 0; k < n; k++) {\n    snapped += spring({\n      frame: t - (SNAP_AT + k * SNAP_GAP),\n      fps,\n      config: { damping: 13, stiffness: 210, mass: 0.7 },\n    });\n  }\n  const gap = block * snapped;\n\n  const labelSize = Math.min(\n    Math.max(F * 0.17, H * 0.044),\n    H * 0.056,\n    (W * 0.84) / Math.max(0.1, m.label),\n  );\n  const labelGap = label ? Math.max(F * 0.2, H * 0.04) : 0;\n  const groupH = capPx + gap + (label ? labelGap + labelSize : 0);\n  const capTop = (H - groupH) / 2;\n  const baseY = capTop + capPx + gap;\n  const lineTop0 = capTop + capPx - m.baseline * F;\n  const cutY0 = capTop + capPx - m.cut * F;\n  const left = (W - m.width * F) / 2;\n\n  const chars = Array.from(value);\n  const glyphs = (keyPrefix: string, style: React.CSSProperties) => {\n    let x = 0;\n    return chars.map((ch, i) => {\n      const gx = x;\n      x += m.adv[i] * F;\n      const p = interpolate(\n        t,\n        [i * RISE_GAP - 2, i * RISE_GAP - 2 + RISE_LEN],\n        [0, 1],\n        { ...CLAMP, easing: EASE_OUT },\n      );\n      return (\n        <span\n          key={`${keyPrefix}-${i}-${ch}`}\n          style={{\n            position: \"absolute\",\n            left: gx,\n            top: 0,\n            transform: `translateY(${((1 - p) * F * 0.9).toFixed(2)}px)`,\n            ...style,\n          }}\n        >\n          {ch}\n        </span>\n      );\n    });\n  };\n\n  const lineStyle: React.CSSProperties = {\n    position: \"absolute\",\n    left,\n    width: m.width * F + F,\n    height: F,\n    lineHeight: `${F}px`,\n    whiteSpace: \"pre\",\n    fontFamily: instrument.fontFamily,\n    fontWeight: 500,\n    fontSize: F,\n    fontFeatureSettings: '\"tnum\" 1',\n  };\n\n  const k = gap / SLICE;\n  const cutLocal = cutY0 - lineTop0;\n  const drift = (t / 30) * 2;\n  const stretchFill = STRETCH[theme](accent);\n  const groupBg = theme === \"dark\" ? \"#000\" : \"#fff\";\n  const groupInk = theme === \"dark\" ? \"#fff\" : \"#000\";\n\n  const maskTop = capTop - F * 0.3;\n\n  const labelWords = label ? label.split(\" \") : [];\n\n  return (\n    <AbsoluteFill style={{ background: c.bg, overflow: \"hidden\" }}>\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          backgroundImage: GLOW[theme],\n          transform: `translateY(${(Math.sin(drift * 0.3) * H * 0.01).toFixed(2)}px)`,\n        }}\n      />\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          width: W,\n          top: maskTop,\n          height: cutY0 - maskTop,\n          overflow: \"hidden\",\n        }}\n      >\n        <div style={{ ...lineStyle, top: lineTop0 - maskTop, color: c.ink }}>\n          {glyphs(\"top\", {})}\n        </div>\n      </div>\n      {gap > 0.5 ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            width: W,\n            top: cutY0,\n            height: gap,\n            overflow: \"hidden\",\n            isolation: \"isolate\",\n            background: groupBg,\n            mixBlendMode: theme === \"dark\" ? \"screen\" : \"multiply\",\n          }}\n        >\n          <div\n            style={{\n              ...lineStyle,\n              top: -cutLocal,\n              transformOrigin: `0 ${cutLocal}px`,\n              transform: `scaleY(${k.toFixed(3)})`,\n              color: groupInk,\n            }}\n          >\n            {glyphs(\"mid\", {})}\n          </div>\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              backgroundImage: stretchFill,\n              backgroundSize: \"100% 114%\",\n              backgroundPosition: `0 ${(50 + Math.sin(drift * 0.5) * 45).toFixed(2)}%`,\n              mixBlendMode: theme === \"dark\" ? \"multiply\" : \"screen\",\n            }}\n          />\n          {Array.from({ length: n - 1 }, (_, j) => {\n            const y = (j + 1) * block;\n            if (y >= gap - 1) return null;\n            return (\n              <div\n                key={`tick-${j}`}\n                style={{\n                  position: \"absolute\",\n                  left: left - F * 0.04,\n                  width: m.width * F + F * 0.08,\n                  top: y - Math.max(1, F * 0.016),\n                  height: Math.max(2, F * 0.032),\n                  background: groupBg,\n                }}\n              />\n            );\n          })}\n        </div>\n      ) : null}\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          width: W,\n          top: cutY0 + gap,\n          height: baseY + (m.desc + 0.02) * F - (cutY0 + gap),\n          overflow: \"hidden\",\n        }}\n      >\n        <div\n          style={{\n            ...lineStyle,\n            top: -cutLocal,\n            color: c.ink,\n          }}\n        >\n          {glyphs(\"bot\", {})}\n        </div>\n      </div>\n      {label ? (\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            width: W,\n            top: baseY + labelGap,\n            textAlign: \"center\",\n            fontFamily: instrument.fontFamily,\n            fontWeight: 400,\n            fontSize: labelSize,\n            letterSpacing: \"-0.01em\",\n            color: c.muted,\n            whiteSpace: \"pre\",\n            lineHeight: 1,\n          }}\n        >\n          {labelWords.map((w, i) => {\n            const p = interpolate(\n              t,\n              [LABEL_AT + i * 3, LABEL_AT + i * 3 + 16],\n              [0, 1],\n              { ...CLAMP, easing: EASE_OUT },\n            );\n            return (\n              <span\n                key={`${w}-${i}`}\n                style={{\n                  display: \"inline-block\",\n                  opacity: p,\n                  transform: `translateY(${((1 - p) * labelSize * 0.6).toFixed(2)}px)`,\n                }}\n              >\n                {w}\n                {i < labelWords.length - 1 ? \" \" : \"\"}\n              </span>\n            );\n          })}\n        </div>\n      ) : null}\n      <Grain opacity={c.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/stem-stretch.tsx"
    }
  ],
  "type": "registry:component"
}
