{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dumbbell-shift",
  "title": "Dumbbell Shift",
  "description": "Dumbbell rows slide their before and after dots apart, with a +16 summary.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/dumbbell-shift/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadManrope } from \"@remotion/google-fonts/Manrope\";\nimport { loadFont } from \"@remotion/google-fonts/SchibstedGrotesk\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  interpolateColors,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: manrope } = loadManrope(\"normal\", {\n  weights: [\"600\"],\n  subsets: [\"latin\"],\n});\n\nexport interface DumbbellRow {\n  label: string;\n  before: number;\n  after: number;\n}\n\nexport interface DumbbellShiftProps {\n  rows?: DumbbellRow[];\n  beforeLabel?: string;\n  afterLabel?: string;\n  summaryCaption?: string;\n  max?: number;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_ROWS: DumbbellRow[] = [\n  { label: \"Launch videos\", before: 2, after: 9 },\n  { label: \"Changelogs\", before: 4, after: 7 },\n  { label: \"Social clips\", before: 3, after: 19 },\n  { label: \"Tutorials\", before: 5, after: 11 },\n  { label: \"Onboarding\", before: 1, after: 6 },\n  { label: \"Ads\", before: 6, after: 8 },\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 EASE_SLIDE = Easing.bezier(0.45, 0, 0.15, 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    dot: \"#6A696F\",\n    guide: \"rgba(255,255,255,0.08)\",\n    grain: 0.06,\n    // gradients.sh \"fluted\": bg #050507, [#1d2a8f, #2f7bff, #ff7a3d, #ffe2c4]\n    fluted: [\"#1d2a8f\", \"#2f7bff\", \"#ff7a3d\", \"#ffe2c4\"],\n    heroText: \"linear-gradient(100deg, #4f8dff 0%, #ff7a3d 64%, #ffe2c4 100%)\",\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    dot: \"#A9A7A1\",\n    guide: \"rgba(18,18,20,0.09)\",\n    grain: 0.045,\n    fluted: [\"#1d2a8f\", \"#2f7bff\", \"#ff7a3d\", \"#f2552c\"],\n    heroText:\n      \"linear-gradient(100deg, #1d2a8f 0%, #2f7bff 34%, #ff7a3d 76%, #f2552c 100%)\",\n  },\n} as const;\n\nconst ENTER_STAGGER = 3;\nconst SLIDE_START = 18;\nconst SLIDE_STAGGER = 5;\nconst SLIDE_DUR = 26;\nconst SORT = [78, 108] as const;\nconst HERO = 110;\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 niceMax(v: number) {\n  const steps = [5, 10, 20, 25, 50, 100, 200, 250, 500, 1000];\n  for (const s of steps) if (v <= s) return s;\n  return Math.ceil(v / 1000) * 1000;\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={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 DumbbellShift({\n  rows = DEFAULT_ROWS,\n  beforeLabel = \"Before\",\n  afterLabel = \"With remocn\",\n  summaryCaption,\n  max,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: DumbbellShiftProps) {\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\n  const list = rows.slice(0, 7);\n  const n = list.length;\n  const deltas = list.map((r) => r.after - r.before);\n  const order = list\n    .map((_, i) => i)\n    .sort((a, b) => deltas[b] - deltas[a] || a - b);\n  const rank = list.map((_, i) => order.indexOf(i));\n  const winner = order[0] ?? 0;\n  const top = Math.max(1, ...list.map((r) => Math.max(r.before, r.after)));\n  const scaleMax = max ?? niceMax(top * 1.04);\n\n  const m = width * 0.08;\n  const rowsTop = height * 0.12;\n  const rowsBottom = height * 0.9;\n  const rowH = (rowsBottom - rowsTop) / Math.max(1, n);\n  const ax0 = width * 0.27;\n  const ax1 = width * 0.705;\n  const heroX = width * 0.74;\n  const r = Math.min(15 * u, rowH * 0.17);\n  const barH = r * 0.78;\n  const gid = `db${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const xOf = (v: number) =>\n    ax0 + (Math.max(0, Math.min(v, scaleMax)) / scaleMax) * (ax1 - ax0);\n\n  const sortK = interpolate(t, [SORT[0], SORT[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const dim = interpolate(t, [HERO, HERO + 16], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const grow = interpolate(t, [HERO, HERO + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const capIn = interpolate(t, [HERO + 12, HERO + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const noteOut = interpolate(t, [SORT[0] - 8, SORT[0] + 2], [1, 0], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const push = interpolate(t, [0, HERO + 40], [1.025, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  const items = list.map((row, i) => {\n    const enter = interpolate(\n      t,\n      [2 + i * ENTER_STAGGER, 20 + i * ENTER_STAGGER],\n      [0, 1],\n      { ...CLAMP, easing: EASE_OUT },\n    );\n    const s0 = SLIDE_START + i * SLIDE_STAGGER;\n    const slide = interpolate(t, [s0, s0 + SLIDE_DUR], [0, 1], {\n      ...CLAMP,\n      easing: EASE_SLIDE,\n    });\n    const deltaIn = interpolate(\n      t,\n      [s0 + SLIDE_DUR - 8, s0 + SLIDE_DUR + 8],\n      [0, 1],\n      {\n        ...CLAMP,\n        easing: EASE_OUT,\n      },\n    );\n    const pos = i + (rank[i] - i) * sortK;\n    const y = rowsTop + rowH * (pos + 0.5);\n    const xb = xOf(row.before);\n    const xa = xOf(row.after);\n    const x = xb + (xa - xb) * slide;\n    const isWin = i === winner;\n    const fade = isWin ? 1 : 1 - 0.4 * dim;\n    const tone = isWin\n      ? p.fluted[3]\n      : interpolateColors(dim, [0, 1], [p.fluted[3], p.muted]);\n    return { row, i, enter, slide, deltaIn, y, xb, xa, x, isWin, fade, tone };\n  });\n  const duck = items.map((it) => {\n    let d = Number.POSITIVE_INFINITY;\n    for (const o of items) if (o !== it) d = Math.min(d, Math.abs(o.y - it.y));\n    return it.isWin\n      ? 1\n      : interpolate(d, [rowH * 0.3, rowH * 0.65], [0, 1], CLAMP);\n  });\n\n  const win = items[winner];\n  const heroText = win\n    ? `${deltas[winner] >= 0 ? \"+\" : \"−\"}${Math.abs(Math.round(deltas[winner] * win.slide))}`\n    : \"\";\n  const deltaText = (v: number) => `${v >= 0 ? \"+\" : \"−\"}${Math.abs(v)}`;\n  const winCaption =\n    summaryCaption ?? (win ? `${win.row.label.toLowerCase()} a month` : \"\");\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 id={gid} x1={0} y1={0} x2={1} y2={0}>\n              <stop offset={0} stopColor={p.fluted[0]} />\n              <stop offset={0.38} stopColor={p.fluted[1]} />\n              <stop offset={0.78} stopColor={p.fluted[2]} />\n              <stop offset={1} stopColor={p.fluted[3]} />\n            </linearGradient>\n          </defs>\n          {items.map((it) => (\n            <g key={it.row.label} opacity={it.fade}>\n              <line\n                x1={ax0}\n                x2={ax0 + (ax1 - ax0) * it.enter}\n                y1={it.y}\n                y2={it.y}\n                stroke={p.guide}\n                strokeWidth={Math.max(1, u)}\n              />\n              {it.slide > 0 && Math.abs(it.x - it.xb) > 0.5 ? (\n                <>\n                  <rect\n                    x={Math.min(it.xb, it.x)}\n                    y={it.y - (barH * (it.isWin ? 1 + 0.35 * dim : 1)) / 2}\n                    width={Math.abs(it.x - it.xb)}\n                    height={barH * (it.isWin ? 1 + 0.35 * dim : 1)}\n                    rx={(barH * (it.isWin ? 1 + 0.35 * dim : 1)) / 2}\n                    fill={`url(#${gid})`}\n                    opacity={it.isWin ? 1 : 1 - dim}\n                  />\n                  {it.isWin ? null : (\n                    <rect\n                      x={Math.min(it.xb, it.x)}\n                      y={it.y - barH / 2}\n                      width={Math.abs(it.x - it.xb)}\n                      height={barH}\n                      rx={barH / 2}\n                      fill={p.muted}\n                      opacity={0.42 * dim}\n                    />\n                  )}\n                </>\n              ) : null}\n              {it.slide > 0 ? (\n                <circle\n                  cx={it.x}\n                  cy={it.y}\n                  r={r}\n                  fill={it.tone}\n                  opacity={interpolate(it.slide, [0, 0.12], [0, 1], CLAMP)}\n                />\n              ) : null}\n              {it.isWin ? (\n                <circle\n                  cx={it.x}\n                  cy={it.y}\n                  r={r + (4 + 6 * dim) * u}\n                  fill=\"none\"\n                  stroke={accent}\n                  strokeWidth={1.5 * u}\n                  opacity={dim * 0.9}\n                />\n              ) : null}\n              <circle\n                cx={it.xb}\n                cy={it.y}\n                r={r * (0.6 + 0.4 * it.enter)}\n                fill={p.dot}\n                opacity={it.enter}\n              />\n            </g>\n          ))}\n        </svg>\n\n        {items.map((it) => (\n          <div\n            key={it.row.label}\n            style={{\n              position: \"absolute\",\n              left: m,\n              top: it.y,\n              transform: `translateY(-50%) translateX(${(1 - it.enter) * -10 * u}px)`,\n              fontFamily: sans,\n              fontWeight: it.isWin && dim > 0.5 ? 500 : 400,\n              fontSize: 24 * u,\n              letterSpacing: \"-0.015em\",\n              color: p.fg,\n              opacity: it.enter * it.fade * duck[it.i],\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {it.row.label}\n          </div>\n        ))}\n\n        {items.map((it) => {\n          if (it.isWin) return null;\n          const v = Math.round(deltas[it.i] * it.slide);\n          return (\n            <div\n              key={it.row.label}\n              style={{\n                position: \"absolute\",\n                left: it.xa + r + 14 * u,\n                top: it.y,\n                transform: `translateY(-50%) translateX(${(1 - it.deltaIn) * -6 * u}px)`,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 20 * u,\n                color: p.muted,\n                fontVariantNumeric: \"tabular-nums\",\n                opacity: it.deltaIn * (1 - dim) * duck[it.i],\n                whiteSpace: \"nowrap\",\n              }}\n            >\n              {deltaText(v)}\n            </div>\n          );\n        })}\n\n        {win\n          ? [\n              { key: \"b\", x: win.xb, text: beforeLabel, a: win.enter },\n              { key: \"a\", x: win.xa, text: afterLabel, a: win.deltaIn },\n            ].map((nt) => (\n              <div\n                key={nt.key}\n                style={{\n                  position: \"absolute\",\n                  left: nt.x,\n                  top: win.y - r - 10 * u,\n                  transform: `translate(-50%, -100%) translateY(${(1 - nt.a) * 6 * u}px)`,\n                  display: \"flex\",\n                  alignItems: \"baseline\",\n                  gap: \"0.3em\",\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: 17 * u,\n                  color: nt.key === \"a\" ? p.fg : p.muted,\n                  opacity: nt.a * noteOut,\n                  whiteSpace: \"nowrap\",\n                }}\n              >\n                {nt.key === \"a\" && /remocn$/i.test(nt.text) ? (\n                  <>\n                    {nt.text.replace(/remocn$/i, \"\").trim()}\n                    <RemocnLogo fontSize={18 * u} color={p.fg} />\n                  </>\n                ) : (\n                  nt.text\n                )}\n              </div>\n            ))\n          : null}\n\n        {win ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left:\n                win.xa + r + 14 * u + (heroX - (win.xa + r + 14 * u)) * grow,\n              top: win.y,\n              transform: \"translateY(-50%)\",\n              opacity: win.deltaIn,\n            }}\n          >\n            <div\n              style={{\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 132 * u,\n                lineHeight: 1,\n                letterSpacing: `${interpolate(grow, [0, 1], [0, -0.05])}em`,\n                color: interpolateColors(grow, [0, 1], [p.muted, p.fg]),\n                fontVariantNumeric: \"tabular-nums\",\n                whiteSpace: \"nowrap\",\n                transformOrigin: \"0% 50%\",\n                transform: `scale(${interpolate(grow, [0, 1], [20 / 132, 1])})`,\n              }}\n            >\n              <span style={{ position: \"relative\", display: \"inline-block\" }}>\n                {heroText}\n                <span\n                  aria-hidden\n                  style={{\n                    position: \"absolute\",\n                    inset: 0,\n                    backgroundImage: p.heroText,\n                    backgroundSize: \"100% 100%\",\n                    WebkitBackgroundClip: \"text\",\n                    backgroundClip: \"text\",\n                    color: \"transparent\",\n                    opacity: grow,\n                  }}\n                >\n                  {heroText}\n                </span>\n              </span>\n            </div>\n            <div\n              style={{\n                position: \"absolute\",\n                left: 4 * u,\n                top: \"100%\",\n                marginTop: 6 * u,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 21 * u,\n                color: p.muted,\n                whiteSpace: \"nowrap\",\n                opacity: capIn,\n                transform: `translateY(${(1 - capIn) * 6 * u}px)`,\n              }}\n            >\n              {winCaption}\n            </div>\n          </div>\n        ) : null}\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/dumbbell-shift.tsx"
    }
  ],
  "type": "registry:component"
}
