{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "baseline-drop",
  "title": "Baseline Drop",
  "description": "Words drop onto the baseline in a stagger, with a gradient streak beneath.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/baseline-drop/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/SchibstedGrotesk\";\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\n\nexport interface BaselineDropProps {\n  text?: string;\n  words?: string[];\n  stagger?: number;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\n/** Schibsted Grotesk 500 advance widths (per 1000 em) for ASCII 32–126. */\nconst SCHIBSTED_500 = [\n  242, 251, 366, 672, 574, 867, 688, 205, 319, 319, 433, 635, 272, 481, 287,\n  454, 617, 360, 579, 583, 596, 590, 601, 541, 588, 600, 287, 276, 635, 635,\n  635, 539, 895, 710, 654, 707, 708, 591, 567, 721, 729, 373, 550, 678, 555,\n  886, 739, 769, 630, 769, 648, 623, 656, 685, 688, 955, 675, 633, 606, 324,\n  454, 324, 635, 500, 500, 563, 600, 560, 600, 580, 349, 562, 577, 253, 280,\n  567, 271, 883, 591, 601, 600, 600, 417, 521, 351, 591, 567, 830, 572, 564,\n  515, 336, 249, 336, 635,\n];\nconst TRACK = -0.03;\nconst SPACE = 0.24;\nconst BASELINE = 0.8594;\nconst CAP = 0.703;\n\nconst DRAW = [4, 28] as const;\nconst FIRST_LAND = 30;\nconst FALL = 15;\nconst RETRACT_AFTER = 20;\nconst RETRACT_LEN = 22;\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_FALL = Easing.bezier(0.5, 0, 0.9, 0.5);\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  // gradients.sh \"aurora\": the ground under the baseline glows as words land.\n  dark: {\n    bg: \"#03050B\",\n    fg: \"#F2F4F1\",\n    line: \"rgba(242,244,241,0.66)\",\n    grain: 0.07,\n    glow: [\"#A0F0C8\", \"#3FB5A3\", \"#5147B6\", \"#B25C9C\"],\n    glowMix: 62,\n    bandMix: 34,\n  },\n  // gradients.sh \"ribbon\" (light): the same ground in pink, orange, gold and violet.\n  light: {\n    bg: \"#F6F4F0\",\n    fg: \"#121214\",\n    line: \"rgba(18,18,20,0.55)\",\n    grain: 0.045,\n    glow: [\"#FF8A3D\", \"#FF3D9A\", \"#7A5CFF\", \"#FFC83D\"],\n    glowMix: 34,\n    bandMix: 22,\n  },\n} as const;\n\nfunction auroraGround(\n  colors: readonly string[],\n  mix: number,\n  band: number,\n  drift: number,\n) {\n  const n = colors.length;\n  const blobs = colors.map((col, i) => {\n    const x = 15 + (70 * i) / (n - 1) + Math.sin(drift * 2.4 + i * 1.9) * 3;\n    const y = 4 + Math.cos(drift * 2 + i * 1.3) * 3;\n    return `radial-gradient(30% 62% at ${x.toFixed(2)}% ${y.toFixed(2)}%, color-mix(in oklab, ${col} ${mix}%, transparent) 0%, transparent 70%)`;\n  });\n  return [\n    ...blobs,\n    `linear-gradient(180deg, color-mix(in oklab, ${colors[0]} ${band}%, transparent) 0%, transparent 46%)`,\n  ].join(\", \");\n}\n\nfunction em(text: string) {\n  let w = 0;\n  for (const ch of text) {\n    const code = ch.charCodeAt(0);\n    w += (code >= 32 && code < 127 ? SCHIBSTED_500[code - 32] : 600) / 1000;\n    w += TRACK;\n  }\n  return w;\n}\n\nfunction landing(tau: number, amp: number) {\n  if (tau <= 0) return 0;\n  const omega = (2 * Math.PI) / 13;\n  return amp * Math.exp(-tau * 0.26) * Math.sin(omega * tau);\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={23}\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\nexport function BaselineDrop({\n  text = \"Ship the story.\",\n  words: wordsProp,\n  stagger = 11,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: BaselineDropProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n\n  const words = useMemo(\n    () => wordsProp ?? text.trim().split(/\\s+/),\n    [wordsProp, text],\n  );\n  const total = useMemo(\n    () => words.reduce((s, w) => s + em(w), 0) + SPACE * (words.length - 1),\n    [words],\n  );\n  const F = Math.min(H * 0.2, (W * 0.78) / total);\n  const baseY = H / 2 + (CAP * F) / 2;\n  const rowTop = baseY - BASELINE * F;\n  const line = Math.max(1, (H / 720) * 1.5);\n  const margin = W * 0.08;\n\n  const land = (k: number) => FIRST_LAND + k * stagger;\n  const lastLand = land(words.length - 1);\n  const retractStart = lastLand + RETRACT_AFTER;\n\n  const drawn = interpolate(t, DRAW, [0, 1], { ...CLAMP, easing: EASE_IO });\n  const retracted = interpolate(\n    t,\n    [retractStart, retractStart + RETRACT_LEN],\n    [0, 1],\n    { ...CLAMP, easing: EASE_IO },\n  );\n  const push = interpolate(t, [0, retractStart + 60], [0, 1], CLAMP);\n\n  const dropOf = (k: number) => {\n    const tau = t - land(k);\n    if (tau >= 0) return { y: landing(tau, F * 0.04), seen: true };\n    const p = interpolate(t, [land(k) - FALL, land(k)], [0, 1], {\n      ...CLAMP,\n      easing: EASE_FALL,\n    });\n    const fallFrom = rowTop + F * 1.4;\n    return { y: -fallFrom * (1 - p), seen: t > land(k) - FALL };\n  };\n\n  const landed = words.reduce(\n    (sum, _, k) =>\n      sum + interpolate(t, [land(k) - 2, land(k) + 10], [0, 1], CLAMP),\n    0,\n  );\n  const glowLevel = 0.3 + (0.7 * landed) / Math.max(1, words.length);\n  const ground = auroraGround(\n    c.glow,\n    c.glowMix,\n    c.bandMix,\n    interpolate(t, [0, retractStart + 60], [0, 1], CLAMP),\n  );\n\n  const contact = (k: number) =>\n    interpolate(t - land(k), [0, 2, 24], [0, 1, 0], CLAMP);\n\n  const row = (children: (w: string, k: number) => React.ReactNode) => (\n    <div\n      style={{\n        position: \"absolute\",\n        left: 0,\n        width: W,\n        top: rowTop,\n        display: \"flex\",\n        justifyContent: \"center\",\n        gap: SPACE * F,\n        whiteSpace: \"pre\",\n        fontFamily: sans,\n        fontWeight: 500,\n        fontSize: F,\n        lineHeight: 1,\n        letterSpacing: `${TRACK}em`,\n      }}\n    >\n      {words.map((w, k) => children(w, k))}\n    </div>\n  );\n\n  return (\n    <AbsoluteFill style={{ background: c.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `scale(${1 + push * 0.02})`,\n          transformOrigin: `50% ${(baseY / H) * 100}%`,\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: 0,\n            width: W,\n            top: baseY + line,\n            bottom: -H * 0.05,\n            background: ground,\n            opacity: glowLevel * drawn,\n            maskImage: `linear-gradient(90deg, transparent ${(1 - drawn) * 40}%, black ${8 + (1 - drawn) * 40}%, black ${92 - (1 - drawn) * 40}%, transparent ${100 - (1 - drawn) * 40}%)`,\n            WebkitMaskImage: `linear-gradient(90deg, transparent ${(1 - drawn) * 40}%, black ${8 + (1 - drawn) * 40}%, black ${92 - (1 - drawn) * 40}%, transparent ${100 - (1 - drawn) * 40}%)`,\n          }}\n        />\n        <div\n          style={{\n            position: \"absolute\",\n            left: margin,\n            width: W - margin * 2,\n            top: baseY,\n            height: line,\n            background: c.line,\n            clipPath: `inset(0 ${(1 - drawn) * 100}% 0 ${retracted * 100}%)`,\n          }}\n        />\n        {row((w, k) => (\n          <span\n            key={`c${k}`}\n            style={{\n              position: \"relative\",\n              display: \"inline-block\",\n              color: \"transparent\",\n            }}\n          >\n            {w}\n            <span\n              style={{\n                position: \"absolute\",\n                left: -F * 0.04,\n                right: -F * 0.04,\n                top: BASELINE * F,\n                height: line * 2.5,\n                background: accent,\n                opacity: contact(k) * (1 - retracted),\n                transform: `scaleX(${0.6 + 0.4 * contact(k) ** 0.5})`,\n              }}\n            />\n          </span>\n        ))}\n        {row((w, k) => {\n          const { y, seen } = dropOf(k);\n          return (\n            <span\n              key={`w${k}`}\n              style={{\n                display: \"inline-block\",\n                color: c.fg,\n                opacity: seen ? 1 : 0,\n                transform: `translateY(${y}px)`,\n              }}\n            >\n              {w}\n            </span>\n          );\n        })}\n      </AbsoluteFill>\n      <Grain opacity={c.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/baseline-drop.tsx"
    }
  ],
  "type": "registry:component"
}
