{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "speedrun-timer",
  "title": "Speedrun Timer",
  "description": "A speedrun split table fills row by row and ends on a gold 'WORLD RECORD' time.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "registryDependencies": ["@remocn/gsh"],
  "files": [
    {
      "path": "registry/remocn/speedrun-timer/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Geist\";\nimport { loadFont as loadMono } from \"@remotion/google-fonts/GeistMono\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport type React from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\nimport type { GradientParams } from \"@/lib/remocn/gsh/gradient-engine\";\nimport { GshGround } from \"@/lib/remocn/gsh/gsh-ground\";\nimport type { GshPresetId } from \"@/lib/remocn/gsh/presets\";\n\nconst { fontFamily: geist } = loadFont(\"normal\", {\n  weights: [\"300\", \"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: mono } = loadMono(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst manrope = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nconst REMOCN_R_PATH =\n  \"M 0.01 0.81 C 0.01 1.73, 0.36 2.79, 1.09 4.13 C 4.91 11.04, 13.45 17.16, 21.7 18.9 C 22.94 19.16, 23.18 19.16, 51.39 19.27 C 76.07 19.36, 80.02 19.4, 81.32 19.57 C 89.89 20.69, 96.2 24.68, 99.38 31.01 C 103.19 38.56, 102.53 50.1, 97.91 57.07 C 94.66 61.96, 89.68 64.99, 83.26 66 C 82.81 66.07, 70.18 66.83, 55.2 67.69 C 24.82 69.43, 27.03 69.23, 24.18 70.4 C 19.9 72.15, 14.84 75.7, 10.65 79.89 C 4.86 85.68, 1.3 91.91, 0.25 98.13 C 0.12 98.85, 0.08 103.13, 0.04 116.66 L 0 134.26 9.5 134.26 L 19 134.26 19.05 119.41 C 19.1 103.27, 19.08 103.82, 19.82 101.04 C 21.79 93.65, 27.86 88.75, 36.45 87.63 C 37.23 87.53, 39.41 87.5, 43.57 87.53 C 50.12 87.59, 50.75 87.65, 53.22 88.45 C 56.61 89.56, 59.67 91.86, 62.02 95.07 C 62.52 95.76, 69.35 103.85, 77.2 113.07 C 85.04 122.28, 91.63 130.04, 91.85 130.32 C 92.07 130.59, 92.5 131.34, 92.82 131.97 C 93.52 133.35, 94.11 133.93, 95.06 134.13 C 95.5 134.23, 98.97 134.26, 106.36 134.23 L 117.01 134.19 100.82 113.07 C 91.91 101.45, 84.52 91.78, 84.39 91.57 C 83.36 89.89, 83.66 87.53, 85.09 86 C 85.79 85.25, 86.36 84.94, 88.07 84.38 C 96.18 81.72, 104.15 76.62, 109.97 70.36 C 120.59 58.93, 124.06 44.32, 119.44 30.43 C 114.59 15.81, 101.93 4.02, 87.64 0.83 C 83.93 0.01, 88.09 0.08, 41.85 0.04 L 0.01 0 0.01 0.81 Z\";\n\nfunction RemocnLogo({ fontSize, color }: { fontSize: number; color: string }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        alignItems: \"baseline\",\n        color,\n        fontFamily: manrope.fontFamily,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n      }}\n    >\n      <svg\n        viewBox=\"0 0 124.06 134.26\"\n        style={{ height: \"0.76em\", width: \"auto\" }}\n        aria-hidden\n      >\n        <path fill=\"currentColor\" d={REMOCN_R_PATH} />\n      </svg>\n      <span style={{ marginLeft: \"0.04em\" }}>emocn</span>\n    </div>\n  );\n}\n\nexport interface SpeedrunSplit {\n  name: string;\n  /** Cumulative run time at this split, in seconds. */\n  time: number;\n  /** Cumulative personal-best time at this split, in seconds. */\n  pb: number;\n}\n\nexport interface SpeedrunTimerProps {\n  /** Category line in the header, after the logo. */\n  category?: string;\n  /** The splits, in order. The last one is the finish and goes gold. */\n  splits?: SpeedrunSplit[];\n  /** The banner that lands when the run finishes. */\n  banner?: string;\n  /** Colour of the current-split marker. */\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  /** gradients.sh preset id for the ground. */\n  ground?: GshPresetId;\n  /** Overrides for the ground shader. */\n  groundParams?: Partial<GradientParams>;\n  speed?: number;\n}\n\nconst DEFAULT_SPLITS: SpeedrunSplit[] = [\n  { name: \"Script\", time: 521, pb: 563 },\n  { name: \"Edit\", time: 1635, pb: 1703 },\n  { name: \"Captions\", time: 2162, pb: 2143 },\n  { name: \"Render\", time: 2910, pb: 3041 },\n  { name: \"Ship\", time: 3552, pb: 3737 },\n];\n\nconst BEATS = {\n  rowIn: 0,\n  rowGap: 3,\n  run: 12,\n  splits: [30, 50, 66, 84, 102],\n  banner: 106,\n  rise: 102,\n} as const;\n\nconst LIGHT = Easing.bezier(0.22, 1, 0.36, 1);\nconst ENTER = Easing.bezier(0.32, 0.72, 0, 1);\nconst HEAVY = { mass: 1, damping: 26, stiffness: 120 };\nconst CARD = { mass: 0.8, damping: 18, stiffness: 160 };\nconst CHIP = { mass: 0.5, damping: 12, stiffness: 200 };\nconst CLAMP = {\n  extrapolateLeft: \"clamp\",\n  extrapolateRight: \"clamp\",\n} as const;\n\nconst THEMES = {\n  dark: {\n    stage: \"#070302\",\n    ink: \"#F6F1EA\",\n    dim: \"#F6F1EA80\",\n    faint: \"#F6F1EA52\",\n    rule: \"#FFFFFF12\",\n    band: \"#FFFFFF0F\",\n    panel: \"rgba(14, 9, 7, 0.62)\",\n    panelShadow:\n      \"inset 0 1px 0 rgba(255,255,255,0.07), 0 0 0 1px rgba(255,255,255,0.08), 0 2px 6px rgba(0,0,0,0.3), 0 24px 60px -18px rgba(0,0,0,0.65)\",\n    ahead: \"#7DDBA3\",\n    behind: \"#FF8A7A\",\n    gold: \"#FFCF7A\",\n    flash: \"#FFB36B\",\n    flashBlend: \"screen\",\n    flashAlpha: 0.55,\n    preset: \"kiln\" as GshPresetId,\n    params: {\n      colors: [\"#4A0802\", \"#C92E0C\", \"#F2802E\", \"#FFB25E\"],\n    } as Partial<GradientParams>,\n    veilFrom: 0.72,\n    veilTo: 0.42,\n  },\n  light: {\n    stage: \"#F3EEE7\",\n    ink: \"#1A120C\",\n    dim: \"#1A120C8C\",\n    faint: \"#1A120C5C\",\n    rule: \"#0000000F\",\n    band: \"#0000000A\",\n    panel: \"rgba(255, 252, 248, 0.72)\",\n    panelShadow:\n      \"0 0 0 1px rgba(0,0,0,0.06), 0 1px 3px rgba(0,0,0,0.05), 0 20px 50px -20px rgba(60,30,10,0.25)\",\n    ahead: \"#16884A\",\n    behind: \"#C93A2B\",\n    gold: \"#A86A00\",\n    flash: \"#FFC07A\",\n    flashBlend: \"multiply\",\n    flashAlpha: 0.45,\n    preset: \"paper-crease\" as GshPresetId,\n    params: {\n      colors: [\"#FF9A4A\", \"#FFC89A\", \"#F7E4D2\", \"#6A2A10\"],\n      background: \"#F3EEE7\",\n    } as Partial<GradientParams>,\n    veilFrom: 0.6,\n    veilTo: 0.15,\n  },\n} as const;\n\nfunction ramp(t: number, from: number, len: number) {\n  return interpolate(t, [from, from + len], [0, 1], CLAMP);\n}\n\nfunction kick(d: number) {\n  if (d < 0) return 0;\n  if (d < 2) return d / 2;\n  const k = Math.max(0, 1 - (d - 2) / 8);\n  return k * k;\n}\n\nfunction shake(d: number, amp: number, seed: number) {\n  if (d < 0 || d > 9) return { x: 0, y: 0 };\n  const k = (1 - d / 9) ** 2;\n  return {\n    x: amp * k * Math.sin(d * 2.7 + seed),\n    y: amp * 0.7 * k * Math.cos(d * 3.3 + seed * 1.7),\n  };\n}\n\nfunction clockParts(sec: number) {\n  const s = Math.max(0, Math.floor(sec));\n  const h = Math.floor(s / 3600);\n  const m = Math.floor((s % 3600) / 60);\n  const r = s % 60;\n  return {\n    head: `${h}:${String(m).padStart(2, \"0\")}`,\n    tail: `:${String(r).padStart(2, \"0\")}`,\n  };\n}\n\nfunction clock(sec: number) {\n  const p = clockParts(sec);\n  return p.head + p.tail;\n}\n\nfunction delta(sec: number) {\n  const a = Math.abs(Math.round(sec));\n  const sign = sec < 0 ? \"−\" : \"+\";\n  return `${sign}${Math.floor(a / 60)}:${String(a % 60).padStart(2, \"0\")}`;\n}\n\nfunction Tabular({ text }: { text: string }) {\n  return (\n    <>\n      {text.split(\"\").map((ch, i) => (\n        <span\n          key={i}\n          style={{\n            display: \"inline-block\",\n            width: ch === \":\" ? \"0.3em\" : \"0.6em\",\n            textAlign: \"center\",\n          }}\n        >\n          {ch}\n        </span>\n      ))}\n    </>\n  );\n}\n\nexport function SpeedrunTimer({\n  category = \"Launch video · Any%\",\n  splits = DEFAULT_SPLITS,\n  banner = \"World record\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  ground,\n  groundParams,\n  speed = 1,\n}: SpeedrunTimerProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const c = THEMES[theme];\n  const tall = H > W;\n  const u = Math.min(W / 1280, H / 720);\n\n  const n = splits.length;\n  const last = BEATS.splits[BEATS.splits.length - 1];\n  const frames = splits.map((_, i) => BEATS.splits[i] ?? last + 18 * (i - 4));\n  const finishAt = frames[n - 1];\n  const done = t >= finishAt;\n\n  const runTime = interpolate(\n    t,\n    [BEATS.run, ...frames],\n    [0, ...splits.map((s) => s.time)],\n    CLAMP,\n  );\n\n  let bump = 0;\n  frames.forEach((f, i) => {\n    bump = Math.max(bump, (i === n - 1 ? 1.3 : 0.3) * kick(t - f));\n  });\n  const hit = shake(t - finishAt, 9 * u, 1.3);\n  const camScale = 1 + 0.025 * bump;\n  const flash = done ? Math.max(0, 1 - (t - finishAt) / 4) ** 2 : 0;\n\n  let tint = 0;\n  let tintColor: string = c.ink;\n  frames.forEach((f, i) => {\n    const d = t - f;\n    if (d >= 0 && d < 10 && i < n - 1) {\n      tint = (1 - d / 10) ** 2;\n      tintColor = splits[i].time <= splits[i].pb ? c.ahead : c.behind;\n    }\n  });\n  const timerColor = done ? c.gold : c.ink;\n\n  const wr = spring({ frame: t - BEATS.banner, fps, config: CARD });\n  const blinkOff =\n    (t >= BEATS.banner + 6 && t < BEATS.banner + 8) ||\n    (t >= BEATS.banner + 11 && t < BEATS.banner + 13);\n  const wrOpacity =\n    t < BEATS.banner ? 0 : blinkOff ? 0 : ramp(t, BEATS.banner, 2);\n\n  const rise = LIGHT(ramp(t, BEATS.rise, 28));\n  const veil = c.veilFrom + (c.veilTo - c.veilFrom) * rise;\n  const groundShift = ((tall ? 0.1 : 0.08) + (1 - rise) * 0.12) * H;\n\n  const L = tall\n    ? {\n        pad: 0.07 * W,\n        headerTop: 0.07 * H,\n        panelTop: 0.135 * H,\n        panelW: 0.86 * W,\n        rowH: 0.052 * H,\n        rowFs: 0.024 * H,\n        deltaFs: 0.019 * H,\n        labelFs: 0.016 * H,\n        logoFs: 0.022 * H,\n        timerFs: 0.215 * W,\n        timerTop: 0.555 * H,\n        bannerFs: 0.072 * W,\n        bannerTop: 0.49 * H,\n        timeColW: 0.22 * W,\n        deltaColW: 0.17 * W,\n      }\n    : {\n        pad: 0.0625 * W,\n        headerTop: 0.07 * H,\n        panelTop: 0.15 * H,\n        panelW: 0.56 * W,\n        rowH: 0.074 * H,\n        rowFs: 0.036 * H,\n        deltaFs: 0.029 * H,\n        labelFs: 0.024 * H,\n        logoFs: 0.036 * H,\n        timerFs: 0.3 * H,\n        timerTop: 0.7 * H,\n        bannerFs: 0.07 * H,\n        bannerTop: 0.58 * H,\n        timeColW: 0.12 * W,\n        deltaColW: 0.1 * W,\n      };\n  const inset = 8 * u;\n  const radius = 18 * u;\n\n  let bandIndex = 0;\n  frames.forEach((f, i) => {\n    if (i < n - 1) bandIndex += spring({ frame: t - f, fps, config: CARD });\n  });\n  const bandOpacity =\n    ramp(t, BEATS.run - 4, 6) * (1 - LIGHT(ramp(t, finishAt, 6)));\n\n  const monoLabel: React.CSSProperties = {\n    fontFamily: mono,\n    fontWeight: 400,\n    fontSize: L.labelFs,\n    letterSpacing: \"0.04em\",\n    textTransform: \"uppercase\",\n    color: c.dim,\n    lineHeight: 1,\n  };\n\n  const panelIn = spring({ frame: t - BEATS.rowIn, fps, config: HEAVY });\n  const pbParts = clockParts(splits[n - 1]?.pb ?? 0);\n  const pbPop = spring({ frame: t - finishAt - 2, fps, config: CHIP });\n  const timerText = clock(runTime);\n\n  return (\n    <AbsoluteFill style={{ background: c.stage, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          top: -0.3 * H,\n          height: 1.55 * H,\n          transform: `translateY(${groundShift - 0.25 * H}px) scale(1.06)`,\n        }}\n      >\n        <GshGround\n          preset={ground ?? c.preset}\n          params={groundParams ?? c.params}\n          timeOffset={5}\n          timeScale={0.4}\n        />\n      </AbsoluteFill>\n      <AbsoluteFill style={{ background: c.stage, opacity: veil }} />\n\n      <div\n        style={{\n          position: \"absolute\",\n          inset: 0,\n          transform: `translate(${hit.x}px, ${hit.y}px) scale(${camScale})`,\n          transformOrigin: \"60% 70%\",\n        }}\n      >\n        <div\n          style={{\n            position: \"absolute\",\n            left: L.pad,\n            right: L.pad,\n            top: L.headerTop,\n            display: \"flex\",\n            alignItems: \"center\",\n            justifyContent: \"space-between\",\n            opacity: ENTER(ramp(t, 0, 10)),\n          }}\n        >\n          <div style={{ display: \"flex\", alignItems: \"center\", gap: 16 * u }}>\n            <RemocnLogo fontSize={L.logoFs} color={c.ink} />\n            <span style={monoLabel}>{category}</span>\n          </div>\n        </div>\n\n        {tall ? null : (\n          <div\n            style={{\n              position: \"absolute\",\n              right: L.pad,\n              top: L.panelTop + inset + 0.012 * H,\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"flex-end\",\n              gap: 0.022 * H,\n              opacity: ENTER(ramp(t, 4, 12)),\n              transform: `translateY(${(1 - panelIn) * 0.04 * H}px)`,\n            }}\n          >\n            <span style={monoLabel}>Personal best</span>\n            <span\n              style={{\n                fontFamily: geist,\n                fontWeight: 300,\n                fontSize: 0.1 * H,\n                lineHeight: 0.8,\n                letterSpacing: \"-0.03em\",\n                fontVariantNumeric: \"tabular-nums\",\n                color: done ? c.faint : c.ink,\n                textDecoration: done ? \"line-through\" : undefined,\n                textDecorationThickness: 2 * u,\n              }}\n            >\n              {pbParts.head}\n              {pbParts.tail}\n            </span>\n            <span\n              style={{\n                fontFamily: mono,\n                fontWeight: 400,\n                fontSize: 0.036 * H,\n                lineHeight: 1,\n                fontVariantNumeric: \"tabular-nums\",\n                color: c.gold,\n                opacity: done ? ramp(t, finishAt + 2, 3) : 0,\n                transform: `scale(${1.3 - 0.3 * pbPop})`,\n                transformOrigin: \"100% 50%\",\n              }}\n            >\n              {delta((splits[n - 1]?.time ?? 0) - (splits[n - 1]?.pb ?? 0))}\n            </span>\n          </div>\n        )}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: L.pad,\n            top: L.panelTop,\n            width: L.panelW,\n            padding: inset,\n            borderRadius: radius,\n            background: c.panel,\n            boxShadow: c.panelShadow,\n            backdropFilter: \"blur(18px)\",\n            WebkitBackdropFilter: \"blur(18px)\",\n            opacity: ramp(t, 0, 4),\n            transform: `translateY(${(1 - panelIn) * 0.05 * H}px)`,\n          }}\n        >\n          <div style={{ position: \"relative\", height: n * L.rowH }}>\n            <div\n              style={{\n                position: \"absolute\",\n                left: 0,\n                right: 0,\n                top: bandIndex * L.rowH,\n                height: L.rowH,\n                borderRadius: radius - inset,\n                background: c.band,\n                opacity: bandOpacity,\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 9 * u,\n                  top: \"28%\",\n                  bottom: \"28%\",\n                  width: 4 * u,\n                  borderRadius: 2 * u,\n                  background: accent,\n                }}\n              />\n            </div>\n            {splits.map((s, i) => {\n              const at = frames[i];\n              const isLast = i === n - 1;\n              const completed = t >= at;\n              const inK = spring({\n                frame: t - BEATS.rowIn - 2 - i * BEATS.rowGap,\n                fps,\n                config: CARD,\n              });\n              const pop = spring({ frame: t - at, fps, config: CHIP });\n              const ahead = s.time <= s.pb;\n              const deltaColor = isLast ? c.gold : ahead ? c.ahead : c.behind;\n              const rowFlash = completed\n                ? Math.max(0, 1 - (t - at) / 12) ** 2\n                : 0;\n              const rowInk = isLast && completed ? c.gold : c.ink;\n              const parts = clockParts(completed ? s.time : s.pb);\n              return (\n                <div\n                  key={s.name}\n                  style={{\n                    position: \"absolute\",\n                    left: 0,\n                    right: 0,\n                    top: i * L.rowH,\n                    height: L.rowH,\n                    display: \"flex\",\n                    alignItems: \"center\",\n                    padding: `0 ${22 * u}px 0 ${26 * u}px`,\n                    borderTop: i === 0 ? undefined : `1px solid ${c.rule}`,\n                    opacity: ramp(t, BEATS.rowIn + 2 + i * BEATS.rowGap, 4),\n                    transform: `translateY(${(1 - inK) * 0.4 * L.rowH}px)`,\n                  }}\n                >\n                  <div\n                    style={{\n                      position: \"absolute\",\n                      inset: 0,\n                      borderRadius: radius - inset,\n                      background: deltaColor,\n                      opacity: 0.16 * rowFlash,\n                    }}\n                  />\n                  <div\n                    style={{\n                      flex: 1,\n                      fontFamily: geist,\n                      fontWeight: 400,\n                      fontSize: L.rowFs,\n                      letterSpacing: \"-0.01em\",\n                      color: rowInk,\n                      lineHeight: 1,\n                    }}\n                  >\n                    {s.name}\n                  </div>\n                  <div\n                    style={{\n                      width: L.deltaColW,\n                      textAlign: \"right\",\n                      fontFamily: mono,\n                      fontWeight: 400,\n                      fontSize: L.deltaFs,\n                      fontVariantNumeric: \"tabular-nums\",\n                      color: deltaColor,\n                      lineHeight: 1,\n                      opacity: completed ? ramp(t, at, 2) : 0,\n                      transform: `scale(${1.3 - 0.3 * pop})`,\n                      transformOrigin: \"100% 50%\",\n                    }}\n                  >\n                    {delta(s.time - s.pb)}\n                  </div>\n                  <div\n                    style={{\n                      width: L.timeColW,\n                      textAlign: \"right\",\n                      fontFamily: geist,\n                      fontWeight: 400,\n                      fontSize: L.rowFs,\n                      fontVariantNumeric: \"tabular-nums\",\n                      color: completed ? rowInk : c.faint,\n                      lineHeight: 1,\n                    }}\n                  >\n                    {parts.head}\n                    <span style={{ opacity: completed ? 0.55 : 1 }}>\n                      {parts.tail}\n                    </span>\n                  </div>\n                </div>\n              );\n            })}\n          </div>\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            right: L.pad,\n            top: L.bannerTop,\n            fontFamily: geist,\n            fontWeight: 300,\n            fontSize: L.bannerFs,\n            lineHeight: 1,\n            letterSpacing: \"0.01em\",\n            textTransform: \"uppercase\",\n            whiteSpace: \"nowrap\",\n            color: c.gold,\n            opacity: wrOpacity,\n            transform: `scale(${1.35 - 0.35 * wr})`,\n            transformOrigin: \"100% 100%\",\n            filter:\n              wr < 0.97 ? `blur(${Math.max(0, 1 - wr) * 10 * u}px)` : undefined,\n          }}\n        >\n          {banner}\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            right: L.pad - 0.02 * L.timerFs,\n            top: L.timerTop,\n            fontFamily: geist,\n            fontWeight: 300,\n            fontSize: L.timerFs,\n            lineHeight: 0.8,\n            letterSpacing: \"-0.03em\",\n            color: t < BEATS.run ? c.dim : timerColor,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          <Tabular text={timerText} />\n          {tint > 0.01 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                inset: 0,\n                color: tintColor,\n                opacity: tint,\n              }}\n            >\n              <Tabular text={timerText} />\n            </div>\n          ) : null}\n        </div>\n      </div>\n\n      {flash > 0.01 ? (\n        <AbsoluteFill\n          style={{\n            background: c.flash,\n            mixBlendMode: c.flashBlend,\n            opacity: c.flashAlpha * flash,\n          }}\n        />\n      ) : null}\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/speedrun-timer.tsx"
    }
  ],
  "type": "registry:component"
}
