{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stacked-shift",
  "title": "Stacked Shift",
  "description": "A stacked share bar shifts over the years until the logo series reaches 52%.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/stacked-shift/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Figtree\";\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont as loadDisplay } from \"@remotion/google-fonts/Newsreader\";\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: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: display } = loadDisplay(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: manrope } = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nexport interface StackedSeries {\n  label: string;\n  values: number[];\n}\n\nexport interface StackedShiftProps {\n  series?: StackedSeries[];\n  years?: string[];\n  highlight?: number;\n  caption?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_SERIES: StackedSeries[] = [\n  { label: \"remocn\", values: [6, 24, 52] },\n  { label: \"Kitebase\", values: [34, 28, 18] },\n  { label: \"Lumen\", values: [26, 22, 14] },\n  { label: \"Fernway\", values: [18, 14, 9] },\n  { label: \"Other\", values: [16, 12, 7] },\n];\nconst DEFAULT_YEARS = [\"2023\", \"2024\", \"2025\"];\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\nconst THEMES = {\n  dark: {\n    bg: \"#0A0A0B\",\n    fg: \"#F3F2EF\",\n    muted: \"#8B8A90\",\n    hair: \"rgba(255,255,255,0.14)\",\n    grays: [\"#B4B2AE\", \"#8E8D92\", \"#6C6B71\", \"#4C4B51\"],\n    grain: 0.06,\n    // gradients.sh \"grain\": bg #070505, [#ffd8b4, #e0703b, #8a2c1d, #2b1015]\n    ember: [\"#8a2c1d\", \"#e0703b\", \"#ffd8b4\"],\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    hair: \"rgba(18,18,20,0.16)\",\n    grays: [\"#5B5955\", \"#85837E\", \"#AAA8A2\", \"#CBC9C3\"],\n    grain: 0.045,\n    ember: [\"#8a2c1d\", \"#e0703b\", \"#f6b27e\"],\n  },\n} as const;\n\nconst ENTER = [0, 26] as const;\nconst FIRST_SHIFT = 34;\nconst SHIFT_EVERY = 54;\nconst OUT = [0, 12] as const;\nconst MOVE = [10, 36] as const;\nconst IN = [32, 44] as const;\n\ntype Slot = { y0: number; y1: number; share: number };\n\nfunction lerp(a: number, b: number, k: number) {\n  return a + (b - a) * k;\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 }}\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\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\n/** The remocn.dev logo: R mark (0.76em, on the baseline) + \"emocn\" in Manrope 600. */\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,\n        fontWeight: 600,\n        fontSize,\n        letterSpacing: \"-0.025em\",\n        lineHeight: 1,\n        whiteSpace: \"nowrap\",\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\nfunction YearTicker({\n  from,\n  to,\n  k,\n  size,\n  color,\n}: {\n  from: string;\n  to: string;\n  k: number;\n  size: number;\n  color: string;\n}) {\n  const len = Math.max(from.length, to.length);\n  const a = from.padStart(len, \" \");\n  const b = to.padStart(len, \" \");\n  const lh = size * 1.15;\n  const travel = size * 0.42;\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        fontFamily: display,\n        fontWeight: 400,\n        fontSize: size,\n        letterSpacing: \"-0.02em\",\n        color,\n        fontVariantNumeric: \"tabular-nums\",\n        height: lh,\n        lineHeight: `${lh}px`,\n      }}\n    >\n      {a.split(\"\").map((ch, i) => {\n        const key = `c${i}`;\n        if (ch === b[i]) return <span key={key}>{ch}</span>;\n        const outA = interpolate(k, [0, 0.6], [1, 0], CLAMP);\n        const inA = interpolate(k, [0.4, 1], [0, 1], CLAMP);\n        const blur = Math.sin(k * Math.PI) * size * 0.06;\n        return (\n          <span\n            key={key}\n            style={{ position: \"relative\", display: \"inline-block\" }}\n          >\n            <span style={{ visibility: \"hidden\" }}>{b[i]}</span>\n            <span\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                opacity: outA,\n                transform: `translateY(${-k * travel}px)`,\n                filter: blur > 0.2 ? `blur(${blur}px)` : undefined,\n              }}\n            >\n              {ch}\n            </span>\n            <span\n              style={{\n                position: \"absolute\",\n                left: 0,\n                top: 0,\n                opacity: inA,\n                transform: `translateY(${(1 - k) * travel}px)`,\n                filter: blur > 0.2 ? `blur(${blur}px)` : undefined,\n              }}\n            >\n              {b[i]}\n            </span>\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\nexport function StackedShift({\n  series = DEFAULT_SERIES,\n  years = DEFAULT_YEARS,\n  highlight = 0,\n  caption = \"of launch videos\",\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: StackedShiftProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height } = useVideoConfig();\n  const u = Math.min(width / 1280, height / 720);\n  const p = THEMES[theme];\n  const clipId = `ss${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n  const list = series.slice(0, 6);\n  const n = list.length;\n  const Y = Math.max(\n    1,\n    Math.min(years.length, ...list.map((s) => s.values.length)),\n  );\n  const hl = Math.min(Math.max(0, Math.round(highlight)), n - 1);\n\n  const colH = height * 0.7;\n  const colTop = (height - colH) / 2 - 16 * u;\n  const colBottom = colTop + colH;\n  const colW = width * 0.165;\n  const colX = width * 0.365;\n  const colR = colX + colW;\n  const gap = 4 * u;\n  const radius = 5 * u;\n  const laneDx = colW + 18 * u;\n\n  const slots: Slot[][] = Array.from({ length: Y }, (_, k) => {\n    const vals = list.map((s) => Math.max(0, s.values[k] ?? 0));\n    const total = vals.reduce((a, b) => a + b, 0) || 1;\n    const avail = colH - gap * (n - 1);\n    const order = vals\n      .map((_, i) => i)\n      .sort((a, b) => vals[b] - vals[a] || a - b);\n    const out: Slot[] = new Array(n);\n    let y = colBottom;\n    for (const i of order) {\n      const h = (vals[i] / total) * avail;\n      out[i] = { y0: y - h, y1: y, share: (vals[i] / total) * 100 };\n      y = y - h - gap;\n    }\n    return out;\n  });\n\n  let yearK = 0;\n  let laneK = 0;\n  for (let k = 0; k < Y - 1; k++) {\n    const T = FIRST_SHIFT + k * SHIFT_EVERY;\n    const mv = interpolate(t, [T + MOVE[0], T + MOVE[1]], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IO,\n    });\n    yearK += mv;\n    const o = interpolate(t, [T + OUT[0], T + OUT[1]], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IO,\n    });\n    const back = interpolate(t, [T + IN[0], T + IN[1]], [0, 1], {\n      ...CLAMP,\n      easing: EASE_IO,\n    });\n    laneK += o - back;\n  }\n  const kA = Math.min(Math.floor(yearK), Y - 1);\n  const kB = Math.min(kA + 1, Y - 1);\n  const f = yearK - kA;\n  const cur = list.map((_, i) => {\n    const a = slots[kA][i];\n    const b = slots[kB][i];\n    return {\n      y0: lerp(a.y0, b.y0, f),\n      y1: lerp(a.y1, b.y1, f),\n      share: lerp(a.share, b.share, f),\n    };\n  });\n\n  const lastEnd = FIRST_SHIFT + (Y - 2) * SHIFT_EVERY + IN[1];\n  const reveal = interpolate(t, [ENTER[0], ENTER[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const revealTop = colBottom - (colH + 4 * u) * reveal;\n  const finalK = interpolate(t, [lastEnd - 6, lastEnd + 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const capIn = interpolate(t, [lastEnd - 2, lastEnd + 18], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const yearIn = interpolate(t, [8, 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const push = interpolate(t, [0, lastEnd + 30], [1.025, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  const shares = slots.map((s) => s[hl].share);\n  const minS = Math.min(...shares);\n  const maxS = Math.max(...shares);\n  const acc = cur[hl];\n  const heroSize =\n    interpolate(acc.share, [minS, Math.max(maxS, minS + 1)], [30, 150], CLAMP) *\n    u;\n  const nameSize = interpolate(heroSize / u, [30, 150], [18, 24], CLAMP) * u;\n  const offX = laneDx * laneK;\n  const sheen = interpolate(t, [0, lastEnd + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  let gi = 0;\n  const colors = list.map((_, i) =>\n    i === hl ? accent : p.grays[Math.min(gi++, p.grays.length - 1)],\n  );\n\n  const labelIn = (y: number) =>\n    interpolate(y + 20 * u - revealTop, [0, 30 * u], [0, 1], CLAMP);\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{ transform: `scale(${push})`, transformOrigin: \"50% 50%\" }}\n      >\n        <svg\n          width={width}\n          height={height}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={`${clipId}e`}\n              x1={0.15 + 0.1 * sheen}\n              y1={1}\n              x2={0.85 - 0.1 * sheen}\n              y2={0}\n            >\n              <stop offset={0} stopColor={p.ember[0]} />\n              <stop offset={0.55} stopColor={p.ember[1]} />\n              <stop offset={1} stopColor={p.ember[2]} />\n            </linearGradient>\n            <clipPath id={clipId}>\n              <rect\n                x={0}\n                y={revealTop}\n                width={width}\n                height={Math.max(0, colBottom - revealTop + 2 * u)}\n              />\n            </clipPath>\n          </defs>\n          <line\n            x1={colX - 18 * u}\n            x2={colR + 18 * u}\n            y1={colBottom + 0.5}\n            y2={colBottom + 0.5}\n            stroke={p.hair}\n            strokeWidth={Math.max(1, u)}\n            opacity={yearIn}\n          />\n          <g clipPath={`url(#${clipId})`}>\n            {cur.map((s, i) =>\n              i === hl ? null : (\n                <rect\n                  key={list[i].label}\n                  x={colX}\n                  y={s.y0}\n                  width={colW}\n                  height={Math.max(0, s.y1 - s.y0)}\n                  rx={Math.min(radius, (s.y1 - s.y0) / 2)}\n                  fill={colors[i]}\n                />\n              ),\n            )}\n            <rect\n              x={colX + offX}\n              y={acc.y0}\n              width={colW}\n              height={Math.max(0, acc.y1 - acc.y0)}\n              rx={Math.min(radius, (acc.y1 - acc.y0) / 2)}\n              fill={`url(#${clipId}e)`}\n            />\n          </g>\n        </svg>\n\n        {cur.map((s, i) => {\n          if (i === hl) return null;\n          const a = labelIn(s.y0) * (1 - 0.45 * finalK);\n          return (\n            <div\n              key={list[i].label}\n              style={{\n                position: \"absolute\",\n                right: width - colX + 22 * u,\n                top: (s.y0 + s.y1) / 2,\n                transform: \"translateY(-50%)\",\n                display: \"flex\",\n                alignItems: \"baseline\",\n                gap: 12 * u,\n                opacity: a,\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              <span\n                style={{\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 19 * u,\n                  letterSpacing: \"-0.01em\",\n                  color: p.fg,\n                }}\n              >\n                {list[i].label}\n              </span>\n              <span\n                style={{\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 19 * u,\n                  color: p.muted,\n                  fontVariantNumeric: \"tabular-nums\",\n                  minWidth: 42 * u,\n                  textAlign: \"right\",\n                }}\n              >\n                {Math.round(s.share)}%\n              </span>\n            </div>\n          );\n        })}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: colR + offX + 26 * u,\n            top: (acc.y0 + acc.y1) / 2,\n            transform: \"translateY(-50%)\",\n            display: \"flex\",\n            flexDirection: \"column\",\n            alignItems: \"flex-start\",\n            opacity: labelIn(acc.y0),\n          }}\n        >\n          <div\n            style={{\n              fontFamily: display,\n              fontWeight: 400,\n              fontSize: heroSize,\n              lineHeight: 0.92,\n              letterSpacing: `${interpolate(heroSize / u, [30, 150], [-0.01, -0.045], CLAMP)}em`,\n              color: p.fg,\n              fontVariantNumeric: \"tabular-nums\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {Math.round(acc.share)}\n            <span style={{ fontSize: heroSize * 0.56 }}>%</span>\n          </div>\n          <div\n            style={{\n              marginTop: interpolate(heroSize / u, [30, 150], [4, 14]) * u,\n              display: \"flex\",\n              alignItems: \"center\",\n              gap: 10 * u,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {list[hl].label.toLowerCase() === \"remocn\" ? (\n              <RemocnLogo fontSize={nameSize * 1.12} color={p.fg} />\n            ) : (\n              <>\n                <span\n                  style={{\n                    width: 10 * u,\n                    height: 10 * u,\n                    borderRadius: \"50%\",\n                    background: accent,\n                  }}\n                />\n                <span\n                  style={{\n                    fontFamily: sans,\n                    fontWeight: 500,\n                    fontSize: nameSize,\n                    letterSpacing: \"-0.01em\",\n                    color: p.fg,\n                  }}\n                >\n                  {list[hl].label}\n                </span>\n              </>\n            )}\n          </div>\n          {caption ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: 2 * u,\n                top: \"100%\",\n                marginTop: 6 * u,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 20 * u,\n                color: p.muted,\n                whiteSpace: \"nowrap\",\n                opacity: capIn,\n                transform: `translateY(${(1 - capIn) * 6 * u}px)`,\n              }}\n            >\n              {caption}\n            </div>\n          ) : null}\n        </div>\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: colX + colW / 2,\n            top: colBottom + 16 * u,\n            transform: `translateX(-50%) translateY(${(1 - yearIn) * 8 * u}px)`,\n            opacity: yearIn,\n          }}\n        >\n          <YearTicker\n            from={years[kA] ?? \"\"}\n            to={years[kB] ?? \"\"}\n            k={f}\n            size={34 * u}\n            color={p.fg}\n          />\n        </div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 85% 85% at 50% 50%, transparent 62%, ${p.bg} 100%)`,\n          opacity: 0.5,\n        }}\n      />\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/stacked-shift.tsx"
    }
  ],
  "type": "registry:component"
}
