{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "slope-chart",
  "title": "Slope Chart",
  "description": "A slope chart draws before/after lines, the hero line rising to a 6.8x stat.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/slope-chart/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont as loadSans } from \"@remotion/google-fonts/InstrumentSans\";\nimport { loadFont as loadSerif } from \"@remotion/google-fonts/InstrumentSerif\";\nimport type React from \"react\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadSans(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\nconst { fontFamily: serif } = loadSerif(\"normal\", {\n  weights: [\"400\"],\n  subsets: [\"latin\"],\n});\n\nexport interface SlopeLine {\n  before: number;\n  after: number;\n}\n\nexport interface SlopeChartProps {\n  lines?: SlopeLine[];\n  highlight?: SlopeLine;\n  value?: string;\n  label?: string;\n  beforeLabel?: string;\n  afterLabel?: string;\n  accent?: string;\n  speed?: number;\n  theme?: \"dark\" | \"light\";\n}\n\nconst DEFAULT_LINES: SlopeLine[] = [\n  { before: 14, after: 16 },\n  { before: 20, after: 18 },\n  { before: 10, after: 13 },\n  { before: 24, after: 22 },\n  { before: 17, after: 20 },\n  { before: 3, after: 5 },\n  { before: 28, after: 27 },\n];\n\nconst DEFAULT_HIGHLIGHT: SlopeLine = { before: 6, after: 41 };\n\nconst EASE_IO = Easing.bezier(0.65, 0, 0.35, 1);\nconst EASE_OUT = Easing.bezier(0.22, 1, 0.36, 1);\nconst DRAW_EASE = Easing.bezier(0.45, 0, 0.2, 1);\nconst CLIMB_EASE = Easing.bezier(0.55, 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    line: \"#9C9BA1\",\n    axis: \"rgba(255,255,255,0.22)\",\n    tick: \"rgba(255,255,255,0.08)\",\n    // gradients.sh \"limb\": brightens as the line climbs\n    limb: [\"accent\", \"#8a8bff\", \"#e9ecff\"],\n    wedge: 0.34,\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    line: \"#8F8D87\",\n    axis: \"rgba(18,18,20,0.24)\",\n    tick: \"rgba(18,18,20,0.08)\",\n    limb: [\"#8a8bff\", \"accent\", \"#4b2bd6\"],\n    wedge: 0.18,\n    grain: 0.045,\n  },\n} as const;\n\nconst AXES = [0, 16] as const;\nconst GREY_AT = 12;\nconst GREY_STAGGER = 4;\nconst GREY_DRAW = 24;\nconst CLIMB = [60, 100] as const;\nconst HIT = 98;\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\nfunction spread(ys: number[], minGap: number) {\n  const order = ys.map((y, i) => ({ y, i })).sort((a, b) => a.y - b.y);\n  for (let pass = 0; pass < 8; pass++) {\n    for (let k = 1; k < order.length; k++) {\n      const gap = order[k].y - order[k - 1].y;\n      if (gap < minGap) {\n        const push = (minGap - gap) / 2;\n        order[k - 1].y -= push;\n        order[k].y += push;\n      }\n    }\n  }\n  const out = [...ys];\n  for (const o of order) out[o.i] = o.y;\n  return out;\n}\n\nfunction taper(\n  x0: number,\n  y0: number,\n  x1: number,\n  y1: number,\n  w0: number,\n  w1: number,\n) {\n  const dx = x1 - x0;\n  const dy = y1 - y0;\n  const len = Math.hypot(dx, dy) || 1;\n  const nx = -dy / len;\n  const ny = dx / len;\n  const a = [x0 + (nx * w0) / 2, y0 + (ny * w0) / 2];\n  const b = [x1 + (nx * w1) / 2, y1 + (ny * w1) / 2];\n  const c = [x1 - (nx * w1) / 2, y1 - (ny * w1) / 2];\n  const d = [x0 - (nx * w0) / 2, y0 - (ny * w0) / 2];\n  return `M${a[0]} ${a[1]} L${b[0]} ${b[1]} L${c[0]} ${c[1]} L${d[0]} ${d[1]} Z`;\n}\n\nconst fmt = (n: number) =>\n  Number.isInteger(n) ? String(n) : n.toFixed(1).replace(/\\.0$/, \"\");\n\nexport function SlopeChart({\n  lines = DEFAULT_LINES,\n  highlight = DEFAULT_HIGHLIGHT,\n  value = \"6.8×\",\n  label = \"more launch videos shipped\",\n  beforeLabel = \"Before\",\n  afterLabel = \"After\",\n  accent = \"#3B5BFD\",\n  speed = 1,\n  theme = \"dark\",\n}: SlopeChartProps) {\n  const t = useCurrentFrame() * speed;\n  const { width: W, height: H, fps } = useVideoConfig();\n  const u = Math.min(W / 1280, H / 720);\n  const p = THEMES[theme];\n\n  const greys = lines.slice(0, 8);\n  const all = [...greys, highlight];\n  const max = Math.max(1, ...all.flatMap((l) => [l.before, l.after]));\n  const min = Math.min(0, ...all.flatMap((l) => [l.before, l.after]));\n\n  const xL = W * 0.2;\n  const xR = W * 0.58;\n  const yTop = H * 0.235;\n  const yBot = H * 0.8;\n  const yOf = (v: number) => yBot - ((v - min) / (max - min)) * (yBot - yTop);\n\n  const axisIn = interpolate(t, AXES, [0, 1], { ...CLAMP, easing: EASE_IO });\n  const axisLabelIn = interpolate(t, [6, 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const push = interpolate(t, [0, HIT + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const camScale = 0.975 + 0.025 * push;\n  // The chart sits centred alone; the frame eases over as the number lands.\n  const slide = interpolate(t, [CLIMB[0] + 8, HIT + 14], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const camX = W * 0.11 * (1 - slide);\n\n  const climb = interpolate(t, CLIMB, [0, 1], {\n    ...CLAMP,\n    easing: CLIMB_EASE,\n  });\n  const dim = interpolate(t, [CLIMB[0] - 2, CLIMB[0] + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  const leftYs = spread(\n    all.map((l) => yOf(l.before)),\n    20 * u,\n  );\n  const rightYs = spread(\n    greys.map((l) => yOf(l.after)),\n    20 * u,\n  );\n\n  const hx0 = xL;\n  const hy0 = yOf(highlight.before);\n  const hx1 = xR;\n  const hy1 = yOf(highlight.after);\n  const tipX = hx0 + (hx1 - hx0) * climb;\n  const tipY = hy0 + (hy1 - hy0) * climb;\n  const wStart = 2.2 * u;\n  const wTip = (2.2 + 5.4 * climb) * u;\n  const startDot = spring({\n    frame: t - CLIMB[0] + 6,\n    fps,\n    config: { damping: 14, stiffness: 180, mass: 0.6 },\n  });\n\n  const hit = spring({\n    frame: t - HIT,\n    fps,\n    config: { damping: 15, stiffness: 170, mass: 0.7 },\n  });\n  const hitO = interpolate(t, [HIT, HIT + 6], [0, 1], CLAMP);\n  const ring = interpolate(t, [HIT, HIT + 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const lab = interpolate(t, [HIT + 8, HIT + 26], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const valueSize = 156 * u;\n  const gradId = `${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}-limb`;\n  const tipColor = p.limb[p.limb.length - 1];\n  const stroke = Math.max(1, u);\n\n  const greyEls: React.ReactNode[] = [];\n  const greyDots: React.ReactNode[] = [];\n  greys.forEach((l, i) => {\n    const start = GREY_AT + i * GREY_STAGGER;\n    const prog = interpolate(t, [start, start + GREY_DRAW], [0, 1], {\n      ...CLAMP,\n      easing: DRAW_EASE,\n    });\n    if (prog <= 0) return;\n    const y0 = yOf(l.before);\n    const y1 = yOf(l.after);\n    const x = xL + (xR - xL) * prog;\n    const y = y0 + (y1 - y0) * prog;\n    const o = 0.95 - 0.4 * dim;\n    greyEls.push(\n      <line\n        key={`l${i}`}\n        x1={xL}\n        y1={y0}\n        x2={x}\n        y2={y}\n        stroke={p.line}\n        strokeWidth={1.7 * u}\n        strokeLinecap=\"round\"\n        opacity={o}\n      />,\n    );\n    const a = interpolate(t, [start, start + 6], [0, 1], CLAMP);\n    const b = interpolate(\n      t,\n      [start + GREY_DRAW - 4, start + GREY_DRAW + 4],\n      [0, 1],\n      CLAMP,\n    );\n    greyDots.push(\n      <circle\n        key={`a${i}`}\n        cx={xL}\n        cy={y0}\n        r={3.6 * u * (0.6 + 0.4 * a)}\n        fill={p.line}\n        opacity={a * o}\n      />,\n      <circle\n        key={`b${i}`}\n        cx={xR}\n        cy={y1}\n        r={3.6 * u * (0.6 + 0.4 * b)}\n        fill={p.line}\n        opacity={b * o}\n      />,\n    );\n  });\n\n  const gridVals = [0.25, 0.5, 0.75, 1].map((k) => min + (max - min) * k);\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{\n          transform: `translateX(${camX}px) scale(${camScale})`,\n          transformOrigin: \"50% 50%\",\n        }}\n      >\n        <svg\n          width={W}\n          height={H}\n          style={{ position: \"absolute\", inset: 0 }}\n          aria-hidden\n        >\n          <defs>\n            <linearGradient\n              id={gradId}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={hx0}\n              y1={hy0}\n              x2={hx1}\n              y2={hy1}\n            >\n              {p.limb.map((c, i) => (\n                <stop\n                  key={c}\n                  offset={i / (p.limb.length - 1)}\n                  stopColor={c === \"accent\" ? accent : c}\n                />\n              ))}\n            </linearGradient>\n            <linearGradient\n              id={`${gradId}-w`}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={0}\n              y1={hy1}\n              x2={0}\n              y2={hy0}\n            >\n              <stop\n                offset={0}\n                stopColor={p.limb[1] === \"accent\" ? accent : p.limb[1]}\n                stopOpacity={p.wedge}\n              />\n              <stop offset={1} stopColor={accent} stopOpacity={0} />\n            </linearGradient>\n          </defs>\n          {gridVals.map((v) => (\n            <line\n              key={v}\n              x1={xL}\n              x2={xL + (xR - xL) * axisIn}\n              y1={yOf(v)}\n              y2={yOf(v)}\n              stroke={p.tick}\n              strokeWidth={stroke}\n              strokeDasharray={`${2 * u} ${6 * u}`}\n            />\n          ))}\n          {[xL, xR].map((x) => (\n            <line\n              key={x}\n              x1={x}\n              x2={x}\n              y1={yBot + 14 * u}\n              y2={yBot + 14 * u - (yBot - yTop + 28 * u) * axisIn}\n              stroke={p.axis}\n              strokeWidth={stroke}\n            />\n          ))}\n          {climb > 0 && (\n            <path\n              d={`M${hx0} ${hy0} L${tipX} ${tipY} L${tipX} ${hy0} Z`}\n              fill={`url(#${gradId}-w)`}\n            />\n          )}\n          {greyEls}\n          {greyDots}\n          {climb > 0 && (\n            <>\n              <path\n                d={taper(hx0, hy0, tipX, tipY, wStart, wTip)}\n                fill={`url(#${gradId})`}\n              />\n              <circle\n                cx={tipX}\n                cy={tipY}\n                r={wTip / 2}\n                fill={`url(#${gradId})`}\n              />\n            </>\n          )}\n          <circle\n            cx={hx0}\n            cy={hy0}\n            r={5 * u * (0.6 + 0.4 * startDot)}\n            fill={accent}\n            opacity={Math.min(1, startDot * 1.4)}\n          />\n          {t >= HIT && (\n            <circle\n              cx={hx1}\n              cy={hy1}\n              r={(8 + 34 * ring) * u}\n              fill=\"none\"\n              stroke={tipColor}\n              strokeWidth={1.5 * u}\n              opacity={(1 - ring) * 0.7}\n            />\n          )}\n          {climb > 0 && (\n            <circle\n              cx={tipX}\n              cy={tipY}\n              r={(6.5 + 2 * hit) * u}\n              fill={`url(#${gradId})`}\n            />\n          )}\n          {climb > 0 && <circle cx={tipX} cy={tipY} r={2.4 * u} fill={p.bg} />}\n        </svg>\n\n        {all.map((l, i) => {\n          const isHl = i === all.length - 1;\n          const start = isHl ? CLIMB[0] - 6 : GREY_AT + i * GREY_STAGGER;\n          const a = interpolate(t, [start + 2, start + 14], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          const o = isHl ? 1 : 0.9 - 0.45 * dim;\n          return (\n            <div\n              key={`v${i}`}\n              style={{\n                position: \"absolute\",\n                left: xL - 120 * u - 18 * u,\n                top: leftYs[i] - 10 * u,\n                width: 120 * u,\n                textAlign: \"right\",\n                fontFamily: sans,\n                fontWeight: isHl ? 500 : 400,\n                fontSize: 16 * u,\n                lineHeight: `${20 * u}px`,\n                color: isHl ? accent : p.muted,\n                fontVariantNumeric: \"tabular-nums\",\n                opacity: a * o,\n                transform: `translateX(${(1 - a) * 8 * u}px)`,\n              }}\n            >\n              {fmt(l.before)}\n            </div>\n          );\n        })}\n\n        {greys.map((l, i) => {\n          const end = GREY_AT + i * GREY_STAGGER + GREY_DRAW;\n          const a = interpolate(t, [end - 4, end + 10], [0, 1], {\n            ...CLAMP,\n            easing: EASE_OUT,\n          });\n          return (\n            <div\n              key={`r${i}`}\n              style={{\n                position: \"absolute\",\n                left: xR + 18 * u,\n                top: rightYs[i] - 10 * u,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 16 * u,\n                lineHeight: `${20 * u}px`,\n                color: p.muted,\n                fontVariantNumeric: \"tabular-nums\",\n                opacity: a * (0.9 - 0.45 * dim),\n                transform: `translateX(${(1 - a) * -8 * u}px)`,\n              }}\n            >\n              {fmt(l.after)}\n            </div>\n          );\n        })}\n\n        {[\n          { x: xL, text: beforeLabel },\n          { x: xR, text: afterLabel },\n        ].map((ax) => (\n          <div\n            key={ax.text}\n            style={{\n              position: \"absolute\",\n              left: ax.x - 100 * u,\n              width: 200 * u,\n              top: yBot + 30 * u,\n              textAlign: \"center\",\n              fontFamily: sans,\n              fontWeight: 500,\n              fontSize: 18 * u,\n              letterSpacing: \"-0.01em\",\n              color: p.fg,\n              opacity: axisLabelIn,\n              transform: `translateY(${(1 - axisLabelIn) * 8 * u}px)`,\n            }}\n          >\n            {ax.text}\n          </div>\n        ))}\n\n        <div\n          style={{\n            position: \"absolute\",\n            left: xR + 44 * u,\n            top: hy1 - valueSize * 0.5,\n            opacity: hitO,\n            transform: `translateX(${(1 - hit) * -18 * u}px)`,\n          }}\n        >\n          <div\n            style={{\n              fontFamily: serif,\n              fontWeight: 400,\n              fontSize: valueSize,\n              lineHeight: 1,\n              letterSpacing: \"-0.02em\",\n              color: p.fg,\n              fontVariantNumeric: \"tabular-nums\",\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {value}\n          </div>\n          <div\n            style={{\n              marginTop: 10 * u,\n              fontFamily: sans,\n              fontWeight: 500,\n              fontSize: 20 * u,\n              letterSpacing: \"-0.01em\",\n              color: p.muted,\n              opacity: lab,\n              transform: `translateY(${(1 - lab) * 8 * u}px)`,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {label}\n          </div>\n        </div>\n      </AbsoluteFill>\n      <Grain opacity={p.grain} />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/slope-chart.tsx"
    }
  ],
  "type": "registry:component"
}
