{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waveform-title",
  "title": "Waveform Title",
  "description": "A headline rises over an aurora waveform line that settles into an underline beneath it.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/waveform-title/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/InstrumentSans\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: display } = loadSans(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface WaveformTitleProps {\n  title?: string;\n  /** How many trailing words are set in the italic serif accent. */\n  accentWords?: number;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\n// gradients.sh \"aurora\" palette. Dark uses all four stops; light drops the\n// pale mint so the line stays legible on the off-white stage.\nconst AURORA = [\"#a0f0c8\", \"#3fb5a3\", \"#5147b6\", \"#b25c9c\"] as const;\n\nconst THEMES = {\n  dark: {\n    bg: \"#03050b\",\n    fg: \"#F3F2EF\",\n    wash: \"rgba(255,255,255,0.04)\",\n    ghost: [0.22, 0.09],\n    line: AURORA,\n    haze: 0.42,\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    wash: \"rgba(255,255,255,0.75)\",\n    ghost: [0.2, 0.08],\n    line: [\"#2f9f8e\", \"#3fb5a3\", \"#5147b6\", \"#b25c9c\"],\n    haze: 0.22,\n    grain: 0.045,\n  },\n} as const;\n\nconst OPEN = [0, 16] as const;\nconst SWEEP = [40, 82] as const;\nconst SETTLE = [82, 104] as const;\nconst RISE = 84;\nconst STAGGER = 4;\nconst SAMPLES = 360;\n\nfunction hash(n: number) {\n  const s = Math.sin(n * 127.1 + 311.7) * 43758.5453;\n  return s - Math.floor(s);\n}\n\nconst smooth = (a: number, b: number, x: number) => {\n  const k = Math.min(1, Math.max(0, (x - a) / (b - a)));\n  return k * k * (3 - 2 * k);\n};\n\nconst SYLLABLES = Array.from({ length: 16 }, (_, i) => ({\n  at: i * 0.09 + hash(i + 3) * 0.04,\n  width: 0.018 + hash(i + 17) * 0.03,\n  peak: hash(i + 41) < 0.12 ? 0 : 0.3 + hash(i + 59) * 0.7,\n}));\n\nfunction envelope(s: number, sec: number) {\n  let e = 0.025;\n  for (const syl of SYLLABLES) {\n    const c = ((((syl.at - sec * 0.2) % 1.44) + 1.44) % 1.44) - 0.2;\n    const d = (s - c) / syl.width;\n    e = Math.max(e, syl.peak * Math.exp(-d * d * 0.8));\n  }\n  return e;\n}\n\nfunction wave(s: number, sec: number) {\n  const carrier =\n    0.5 * Math.sin(s * 270 - sec * 14 + 0.7) +\n    0.3 * Math.sin(s * 433 + sec * 9 + 2.3) +\n    0.2 * Math.sin(s * 88 - sec * 4.4 + 4.1);\n  const env = envelope(s, sec);\n  return env * carrier * (0.75 + 0.25 * Math.sin(s * 23 + sec * 2));\n}\n\nfunction Grain({ opacity }: { opacity: number }) {\n  const id = `g${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  return (\n    <svg\n      width=\"100%\"\n      height=\"100%\"\n      style={{ position: \"absolute\", inset: 0, opacity, pointerEvents: \"none\" }}\n      aria-hidden\n    >\n      <filter id={id}>\n        <feTurbulence\n          type=\"fractalNoise\"\n          baseFrequency=\"0.9\"\n          numOctaves={2}\n          seed={29}\n          stitchTiles=\"stitch\"\n        />\n        <feColorMatrix values=\"0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0 0.5  0 0 0 0.9 0\" />\n      </filter>\n      <rect width=\"100%\" height=\"100%\" filter={`url(#${id})`} />\n    </svg>\n  );\n}\n\nconst lerp = (a: number, b: number, k: number) => a + (b - a) * k;\n\nexport function WaveformTitle({\n  title = \"Your voice, captioned\",\n  accentWords = 1,\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: WaveformTitleProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const u = height / 720;\n  const p = THEMES[theme];\n  const id = useId().replace(/[^a-zA-Z0-9_-]/g, \"\");\n\n  const x0 = width * 0.1;\n  const x1 = width * 0.9;\n  const span = x1 - x0;\n  const lineY = height * 0.6;\n\n  const open = interpolate(t, OPEN, [0, 1], { ...CLAMP, easing: EASE_OUT });\n  const sweep = interpolate(t, SWEEP, [0, 1], { ...CLAMP, easing: EASE_IO });\n  const settle = interpolate(t, SETTLE, [0, 1], { ...CLAMP, easing: EASE_IO });\n  const head = lerp(-0.04, 1.04, sweep);\n  const centreY = lerp(height * 0.5, lineY, sweep);\n  const amp = 150 * u * lerp(0.55, 1, open) * (1 - 0.35 * sweep);\n  const reach = lerp(0.28, 1, open);\n\n  const shape = (lag: number) => {\n    const sec = Math.max(0, t - lag) / fps;\n    const pts: string[] = [];\n    for (let i = 0; i <= SAMPLES; i++) {\n      const s = i / SAMPLES;\n      const fromMid = Math.abs(s - 0.5) * 2;\n      if (fromMid > reach) continue;\n      const taper =\n        smooth(0, 0.1, s) *\n        smooth(1, 0.9, s) *\n        smooth(reach, reach - 0.12, fromMid);\n      const calm = smooth(head - 0.02, head + 0.16, s);\n      const y = centreY - amp * taper * calm * wave(s, sec);\n      pts.push(\n        `${pts.length ? \"L\" : \"M\"}${(x0 + s * span).toFixed(2)} ${y.toFixed(2)}`,\n      );\n    }\n    return pts.join(\" \");\n  };\n\n  const headX = x0 + Math.min(1, Math.max(0, head)) * span;\n  const headO =\n    interpolate(t, [SWEEP[0], SWEEP[0] + 6], [0, 1], CLAMP) *\n    interpolate(t, [SWEEP[1] - 6, SWEEP[1] + 2], [1, 0], CLAMP);\n  const handoff = t >= SETTLE[0];\n  const words = title.split(\" \");\n  const serifFrom =\n    words.length - Math.max(0, Math.min(accentWords, words.length));\n  const fontSize = Math.max(\n    92 * u,\n    Math.min(112 * u, (span * 0.98) / (title.length * 0.5)),\n  );\n  const stroke = 4 * u;\n  const lineGrad = `linear-gradient(90deg, ${p.line.join(\", \")})`;\n  const drift = Math.sin((t / fps) * 0.6) * 3;\n  const hazeIn = interpolate(t, [0, 30], [0.35, 1], CLAMP);\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse ${width * 0.5}px ${height * 0.42}px at ${handoff ? width / 2 : headX}px ${centreY}px, ${p.wash}, transparent 100%)`,\n        }}\n      />\n\n      <AbsoluteFill\n        style={{\n          opacity: p.haze * hazeIn,\n          WebkitMaskImage: `linear-gradient(180deg, #000 ${(lineY / height) * 100 - 2}%, transparent ${(lineY / height) * 100 + 12}%)`,\n          maskImage: `linear-gradient(180deg, #000 ${(lineY / height) * 100 - 2}%, transparent ${(lineY / height) * 100 + 12}%)`,\n          background: [\n            ...p.line.map(\n              (c, i) =>\n                `radial-gradient(30% 42% at ${15 + (70 * i) / (p.line.length - 1) + drift * (i % 2 ? 1 : -1)}% ${(lineY / height) * 100 - 12 + (i % 2) * 5}%, color-mix(in oklab, ${c} 75%, transparent) 0%, transparent 70%)`,\n            ),\n            `linear-gradient(180deg, transparent 18%, color-mix(in oklab, ${p.line[1]} 30%, transparent) ${(lineY / height) * 100 - 2}%, transparent ${(lineY / height) * 100 + 8}%)`,\n          ].join(\", \"),\n        }}\n      />\n\n      {!handoff && (\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={`${id}s`}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={x0}\n              y1={0}\n              x2={x1}\n              y2={0}\n            >\n              {p.line.map((c, i) => (\n                <stop\n                  key={c + i}\n                  offset={i / (p.line.length - 1)}\n                  stopColor={c}\n                />\n              ))}\n            </linearGradient>\n          </defs>\n          <path\n            d={shape(6)}\n            fill=\"none\"\n            stroke={p.fg}\n            strokeWidth={1.2 * u}\n            opacity={p.ghost[1] * (1 - sweep)}\n          />\n          <path\n            d={shape(3)}\n            fill=\"none\"\n            stroke={p.fg}\n            strokeWidth={1.3 * u}\n            opacity={p.ghost[0] * (1 - sweep)}\n          />\n          <path\n            d={shape(0)}\n            fill=\"none\"\n            stroke={`url(#${id}s)`}\n            strokeWidth={lerp(2 * u, stroke, sweep)}\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n          />\n          <circle\n            cx={headX}\n            cy={centreY}\n            r={4.5 * u}\n            fill={accent}\n            opacity={headO}\n          />\n        </svg>\n      )}\n\n      <div\n        style={{\n          position: \"absolute\",\n          left: 0,\n          right: 0,\n          top: 0,\n          height: lineY,\n          display: \"flex\",\n          justifyContent: \"center\",\n          alignItems: \"flex-end\",\n        }}\n      >\n        <div style={{ position: \"relative\", display: \"inline-block\" }}>\n          <div\n            style={{\n              overflow: \"hidden\",\n              paddingBottom: 30 * u,\n              paddingTop: 10 * u,\n              maxWidth: span,\n            }}\n          >\n            <div\n              style={{\n                fontFamily: display,\n                fontWeight: 500,\n                fontSize,\n                lineHeight: 1,\n                letterSpacing: \"-0.035em\",\n                color: p.fg,\n                whiteSpace: \"nowrap\",\n                textAlign: \"center\",\n              }}\n            >\n              {words.map((w, i) => {\n                const k = interpolate(\n                  t,\n                  [RISE + i * STAGGER, RISE + i * STAGGER + 22],\n                  [0, 1],\n                  {\n                    ...CLAMP,\n                    easing: EASE_OUT,\n                  },\n                );\n                const isSerif = i >= serifFrom;\n                return (\n                  <span\n                    key={`${w}-${i}`}\n                    style={{\n                      display: \"inline-block\",\n                      marginRight: i < words.length - 1 ? \"0.22em\" : 0,\n                      transform: `translateY(${(1 - k) * fontSize * 1.6}px)`,\n                      ...(isSerif\n                        ? {\n                            fontFamily: serif,\n                            fontStyle: \"italic\",\n                            fontWeight: 400,\n                            fontSize: \"1.12em\",\n                            letterSpacing: \"-0.02em\",\n                            lineHeight: 0.9,\n                          }\n                        : null),\n                    }}\n                  >\n                    {w}\n                  </span>\n                );\n              })}\n            </div>\n          </div>\n          {handoff && (\n            <div\n              style={{\n                position: \"absolute\",\n                left: \"50%\",\n                bottom: -stroke / 2,\n                height: stroke,\n                borderRadius: stroke,\n                backgroundImage: lineGrad,\n                width: `calc(${settle * 100}% + ${(1 - settle) * span}px)`,\n                transform: \"translateX(-50%)\",\n              }}\n            />\n          )}\n        </div>\n      </div>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 80% 80% at 50% 50%, transparent 60%, ${p.bg} 100%)`,\n          opacity: 0.7,\n          pointerEvents: \"none\",\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/waveform-title.tsx"
    }
  ],
  "type": "registry:component"
}
