{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "oval-type",
  "title": "Oval Type",
  "description": "Each word rides inside its own glossy gradient capsule, finishing as a three-capsule poster.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/oval-type/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } 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: sans } = loadFont(\"normal\", {\n  weights: [\"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"italic\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface OvalTypeProps {\n  /** The poster row: one word per capsule, in reading order. */\n  tagline?: string[];\n  /** Words that ride the capsules past the frame before the tagline lands. */\n  words?: string[];\n  /** Index of the tagline word set in Instrument Serif italic; -1 for none. */\n  accentWord?: number;\n  /** Frames between two conveyor steps. */\n  beat?: number;\n  /** Kept for the shared draft API; the capsules take the \"mesh\" field. */\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\n/**\n * Beat sheet (30fps, speed 1, beat 9, six conveyor words):\n *   0–6    three capsules hold, each with a word; a fourth peeks in.\n *   6–58   the row steps one capsule left per beat. Capsules stretch wide\n *          while they travel, squash and bounce tall when they land; the\n *          leading capsule moves first, so the gaps breathe.\n *  58–78   the tagline lands: the capsules stretch up into tall poster ovals,\n *          the words grow, the gaps close.\n *  78–     poster hold; the blurred field keeps drifting inside the ovals.\n */\nconst FIRST_STEP = 6;\nconst MOVE = 7;\nconst LAND_LEN = 18;\n\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\nconst STEP = Easing.bezier(0.6, 0, 0.2, 1);\nconst GROW = Easing.bezier(0.25, 1.15, 0.4, 1);\n\n/** Instrument Sans 500 advance widths (per mille) for ASCII 32–126. */\nconst SANS = [\n  197, 284, 417, 721, 623, 786, 761, 244, 423, 423, 419, 538, 265, 499, 265,\n  444, 671, 389, 551, 577, 607, 578, 607, 546, 590, 615, 265, 265, 538, 538,\n  538, 574, 860, 731, 640, 744, 753, 634, 598, 764, 731, 254, 441, 702, 589,\n  902, 731, 790, 661, 796, 660, 623, 656, 708, 731, 1088, 697, 688, 627, 423,\n  444, 423, 538, 446, 354, 546, 613, 543, 613, 567, 362, 613, 606, 250, 250,\n  546, 250, 931, 606, 591, 613, 613, 383, 483, 392, 596, 530, 783, 569, 530,\n  505, 423, 238, 423, 538,\n];\n/** Instrument Serif italic 400 advance widths (per mille) for ASCII 32–126. */\nconst SERIF_IT = [\n  170, 275, 374, 629, 424, 591, 525, 235, 346, 346, 449, 531, 219, 426, 219,\n  258, 461, 248, 404, 373, 394, 387, 411, 365, 437, 414, 299, 301, 531, 531,\n  531, 358, 665, 457, 479, 475, 536, 454, 411, 514, 545, 250, 251, 499, 414,\n  665, 542, 540, 469, 541, 525, 424, 461, 537, 458, 653, 545, 478, 427, 326,\n  256, 326, 431, 372, 355, 474, 431, 351, 474, 348, 273, 401, 476, 288, 266,\n  447, 240, 745, 521, 414, 450, 425, 368, 302, 272, 508, 412, 609, 450, 418,\n  385, 320, 252, 320, 531,\n];\nconst TRACK = -0.03;\n/** The serif italic runs small next to the grotesk; it gets a size bump. */\nconst SERIF_BUMP = 1.22;\n\nfunction measure(word: string, table: number[], track: number) {\n  let w = 0;\n  for (const ch of word) {\n    const c = ch.charCodeAt(0);\n    w += (c >= 32 && c < 127 ? table[c - 32] : 600) / 1000 + track;\n  }\n  return w - track;\n}\n\n// gradients.sh \"mesh\" (bg #0a0d24, [#ecdcf2, #8796ee, #2e45b7, #0e1645]) on\n// dark: a pale lilac field with blue blooms at the capsule ends, so the ink\n// word in the middle keeps its contrast. On light: \"pixel-glass\" blooms over\n// a deep blue field with white words.\nconst THEMES = {\n  dark: {\n    stage: \"#0A0A0B\",\n    word: \"#0B0B10\",\n    base: \"#ecdcf2\",\n    blooms: [\"#2e45b7\", \"#8796ee\"],\n    mid: \"#c9c3f4\",\n    grain: 0.06,\n  },\n  light: {\n    stage: \"#F5F4F0\",\n    word: \"#FFFFFF\",\n    base: \"#3d5bff\",\n    blooms: [\"#ff4fb8\", \"#7a3cff\"],\n    mid: \"#3d6bff\",\n    grain: 0.04,\n  },\n} as const;\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={17}\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 OvalType({\n  tagline = [\"Make\", \"it\", \"move\"],\n  words = [\"Plan\", \"Cut\", \"Edit\", \"Loop\", \"Sync\", \"Draft\"],\n  accentWord = 1,\n  beat = 9,\n  theme = \"dark\",\n  speed = 1,\n}: OvalTypeProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H } = useVideoConfig();\n  const c = THEMES[theme];\n\n  const row = tagline.length > 0 ? tagline : [\"Make\"];\n  const n = row.length;\n  const items = [...words, ...row];\n  const m = words.length;\n  const isAccent = (k: number) => k - m === accentWord;\n\n  const gapLoose = 0.026 * W;\n  const gapTight = 0.014 * W;\n  const ow = (W * 0.88 - (n - 1) * gapLoose) / n;\n  const pitch = ow + gapLoose;\n  const rowLeft = (W - (n * ow + (n - 1) * gapLoose)) / 2;\n\n  // One type size for every word, set by the widest one.\n  const widest = Math.max(\n    ...items.map((w, k) =>\n      isAccent(k)\n        ? measure(w, SERIF_IT, 0) * SERIF_BUMP\n        : measure(w, SANS, TRACK),\n    ),\n  );\n  const fsPoster = Math.min((ow * 0.78) / widest, H * 0.2);\n\n  const lastEnd = FIRST_STEP + (m - 1) * beat + MOVE;\n  const posterAt = lastEnd + 2;\n\n  // Conveyor position in slots, with a per-capsule lag: the leader moves first.\n  const offsetAt = (time: number) => {\n    let s = 0;\n    for (let j = 0; j < m; j++) {\n      const at = FIRST_STEP + j * beat;\n      s += STEP(interpolate(time, [at, at + MOVE], [0, 1], CLAMP));\n    }\n    return s;\n  };\n  const s0 = offsetAt(t);\n\n  const cy = H / 2;\n  const fieldShift = t * 0.6;\n  const field = [\n    `radial-gradient(46% 26% at ${22 + fieldShift * 0.25}% ${6 + 3 * Math.sin(t * 0.05)}%, ${c.blooms[0]} 0%, transparent 74%)`,\n    `radial-gradient(40% 24% at ${78 - fieldShift * 0.22}% ${95 - 3 * Math.sin(t * 0.045)}%, ${c.blooms[0]} 0%, transparent 74%)`,\n    `radial-gradient(55% 48% at ${64 - fieldShift * 0.2}% ${26 + 4 * Math.cos(t * 0.04)}%, ${c.blooms[1]} 0%, transparent 70%)`,\n    `radial-gradient(52% 46% at ${30 + fieldShift * 0.2}% ${82 + 3 * Math.cos(t * 0.05)}%, ${c.blooms[1]} 0%, transparent 70%)`,\n    `radial-gradient(40% 40% at ${50 + 20 * Math.sin(t * 0.03)}% 50%, ${c.mid} 0%, transparent 75%)`,\n    c.base,\n  ].join(\", \");\n\n  const capsules = items.map((word, k) => {\n    // Lag by where the capsule sits in the row: leftmost leads.\n    const slotNow = k - s0;\n    const lag = Math.max(0, Math.min(n, slotNow)) * 1.2;\n    const s = offsetAt(t - lag);\n    const v = (offsetAt(t - lag + 0.5) - offsetAt(t - lag - 0.5)) / 1;\n    const slot = k - s;\n\n    // Landing bounce after each step: tall, then settle.\n    let bump = 0;\n    for (let j = 0; j < m; j++) {\n      const end = FIRST_STEP + j * beat + MOVE + lag;\n      const dt = t - end;\n      if (dt > 0 && dt < 12) {\n        bump += Math.sin((dt / 12) * Math.PI * 2) * Math.exp(-dt / 4);\n      }\n    }\n\n    const fi = k - m;\n    const growAt = posterAt + Math.max(0, fi) * 2;\n    const grow =\n      fi >= 0\n        ? GROW(interpolate(t, [growAt, growAt + LAND_LEN], [0, 1], CLAMP))\n        : 0;\n    const gap = gapLoose + (gapTight - gapLoose) * grow;\n    const pitchNow = ow + gap;\n    const rowW = n * ow + (n - 1) * gap;\n    const left = (W - rowW) / 2;\n\n    // A tall / short / mid rhythm along the conveyor; the poster evens it out.\n    const rhythm = [1, 0.8, 0.9][k % 3];\n    const baseH = H * (0.7 * (rhythm + (1 - rhythm) * grow) + 0.18 * grow);\n    const wStretch = 1 + 0.55 * v + 0.05 * bump;\n    const hStretch = 1 - 0.3 * v - 0.06 * bump;\n    const w = ow * wStretch;\n    const h = baseH * hStretch;\n    // Slots keep their conveyor pitch until the poster closes the gaps.\n    const x =\n      (grow > 0 ? left + slot * pitchNow : rowLeft + slot * pitch) + ow / 2;\n    // Capsules that already passed slide out as the poster row closes up.\n    const clear =\n      fi < 0\n        ? GROW(interpolate(t, [posterAt, posterAt + LAND_LEN], [0, 1], CLAMP)) *\n          (ow + 0.1 * W)\n        : 0;\n    const fs = fsPoster * (0.84 + 0.16 * grow) * (isAccent(k) ? SERIF_BUMP : 1);\n    return { key: `${k}-${word}`, word, k, x: x - clear, w, h, fs };\n  });\n\n  return (\n    <AbsoluteFill style={{ background: c.stage, overflow: \"hidden\" }}>\n      {capsules.map((q) => {\n        if (q.x + q.w / 2 < -20 || q.x - q.w / 2 > W + 20) return null;\n        const left = q.x - q.w / 2;\n        const top = cy - q.h / 2;\n        return (\n          <div\n            key={q.key}\n            style={{\n              position: \"absolute\",\n              left,\n              top,\n              width: q.w,\n              height: q.h,\n              borderRadius: Math.min(q.w, q.h) / 2,\n              overflow: \"hidden\",\n            }}\n          >\n            <div\n              style={{\n                position: \"absolute\",\n                left: -left,\n                top: -top,\n                width: W,\n                height: H,\n                background: field,\n              }}\n            />\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                display: \"flex\",\n                alignItems: \"center\",\n                justifyContent: \"center\",\n                color: c.word,\n                fontFamily: isAccent(q.k) ? serif : sans,\n                fontStyle: isAccent(q.k) ? \"italic\" : \"normal\",\n                fontWeight: isAccent(q.k) ? 400 : 500,\n                fontSize: q.fs,\n                letterSpacing: isAccent(q.k) ? \"0em\" : `${TRACK}em`,\n                lineHeight: 1,\n                whiteSpace: \"nowrap\",\n                fontKerning: \"normal\",\n              }}\n            >\n              {q.word}\n            </div>\n          </div>\n        );\n      })}\n      <Grain opacity={c.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/oval-type.tsx"
    }
  ],
  "type": "registry:component"
}
