{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-morph",
  "title": "Chart Morph",
  "description": "A line chart with an area fill morphs between points up to a 46k value.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/chart-morph/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/Manrope\";\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});\n\nexport interface ChartMorphDatum {\n  label: string;\n  value: number;\n}\n\nexport interface ChartMorphProps {\n  data?: ChartMorphDatum[];\n  suffix?: string;\n  caption?: string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_DATA: ChartMorphDatum[] = [\n  { label: \"Jan\", value: 12 },\n  { label: \"Feb\", value: 18 },\n  { label: \"Mar\", value: 23 },\n  { label: \"Apr\", value: 31 },\n  { label: \"May\", value: 46 },\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_DRAW = Easing.bezier(0.55, 0, 0.25, 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: \"#E9E8E4\",\n    hair: \"rgba(255,255,255,0.14)\",\n    grays: [\"#5C5B61\", \"#75747A\", \"#929197\", \"#B4B2AE\"],\n    grain: 0.06,\n    // gradients.sh \"limb\": bg #030308, [#e9ecff, #8a8bff, #4b2bd6, #160c3a]\n    limb: [\"#4b2bd6\", \"#8a8bff\", \"#d4d7ff\"],\n    heroDot: \"#d4d7ff\",\n    area: 1,\n    areaStops: [\"#4b2bd6\", \"#4b2bd6\", \"#8a8bff\"],\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    line: \"#1D1D20\",\n    hair: \"rgba(18,18,20,0.16)\",\n    grays: [\"#C9C7C1\", \"#AEACA6\", \"#8F8D88\", \"#6C6A66\"],\n    grain: 0.045,\n    limb: [\"#160c3a\", \"#4b2bd6\", \"#8a8bff\"],\n    heroDot: \"#4b2bd6\",\n    area: 0.4,\n    areaStops: [\"#8a8bff\", \"#4b2bd6\", \"#8a8bff\"],\n  },\n} as const;\n\nconst DRAW = [0, 24] as const;\nconst SPIN = [0, 30] as const;\nconst UNROLL = [30, 60] as const;\nconst TIP_START = 64;\nconst TIP_STAGGER = 3;\nconst TIP_DUR = 24;\nconst COLLAPSE_START = 100;\nconst COLLAPSE_STAGGER = 2;\nconst COLLAPSE_DUR = 22;\nconst LABEL_IN = 131;\nconst TAU = Math.PI * 2;\nconst SAMPLES = 40;\n\ntype Pt = { x: number; y: number };\n\nfunction lerp(a: number, b: number, k: number) {\n  return a + (b - a) * k;\n}\n\nfunction lerpPt(a: Pt, b: Pt, k: number): Pt {\n  return { x: lerp(a.x, b.x, k), y: lerp(a.y, b.y, k) };\n}\n\nfunction toPath(pts: Pt[]) {\n  return `${pts\n    .map((q, i) => `${i === 0 ? \"M\" : \"L\"} ${q.x.toFixed(2)} ${q.y.toFixed(2)}`)\n    .join(\" \")} Z`;\n}\n\nfunction rotateAbout(q: Pt, c: Pt, a: number): Pt {\n  const cs = Math.cos(a);\n  const sn = Math.sin(a);\n  const dx = q.x - c.x;\n  const dy = q.y - c.y;\n  return { x: c.x + cs * dx - sn * dy, y: c.y + sn * dx + cs * dy };\n}\n\nfunction unrollPoint(\n  s: number,\n  o: number,\n  k: number,\n  ax: number,\n  ay: number,\n): Pt {\n  if (k < 1e-6) return { x: ax + s, y: ay + o };\n  const f = s * k;\n  const sn = Math.sin(f);\n  const cs = Math.cos(f);\n  return {\n    x: ax + sn / k + o * sn,\n    y: ay - (1 - cs) / k + o * cs,\n  };\n}\n\nfunction bandPath(\n  s0: number,\n  s1: number,\n  thick: number,\n  k: number,\n  ax: number,\n  ay: number,\n  spin: number,\n  center: Pt,\n) {\n  const top: Pt[] = [];\n  const bot: Pt[] = [];\n  for (let j = 0; j <= SAMPLES; j++) {\n    const s = lerp(s0, s1, j / SAMPLES);\n    top.push(unrollPoint(s, -thick / 2, k, ax, ay));\n    bot.push(unrollPoint(s, thick / 2, k, ax, ay));\n  }\n  const pts = [...top, ...bot.reverse()];\n  return toPath(spin ? pts.map((q) => rotateAbout(q, center, spin)) : pts);\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={41}\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 ChartMorph({\n  data = DEFAULT_DATA,\n  suffix = \"k\",\n  caption,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: ChartMorphProps) {\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 gid = `cm${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n\n  const list = data.slice(0, 5).map((d) => ({\n    ...d,\n    value: Math.max(0, d.value),\n  }));\n  const n = list.length;\n  const total = list.reduce((s, d) => s + d.value, 0) || 1;\n  let hl = 0;\n  list.forEach((d, i) => {\n    if (d.value > list[hl].value) hl = i;\n  });\n  const last = list[n - 1];\n  const endCaption = caption ?? `renders in ${last?.label ?? \"\"}`.trim();\n\n  const cx = width / 2;\n  const cy = height / 2;\n  const center = { x: cx, y: cy };\n  const ringR = height * 0.26;\n  const ringT = height * 0.074;\n  const barT = height * 0.084;\n  const stackW = width * 0.74;\n  const gap = 6 * u;\n  const L0 = TAU * ringR;\n\n  const heroFill = `url(#${gid}-hero)`;\n  const colors = (() => {\n    let g = 0;\n    return list.map((_, i) =>\n      i === hl ? heroFill : p.grays[Math.min(g++, p.grays.length - 1)],\n    );\n  })();\n\n  let acc = 0;\n  const fracs = list.map((d) => {\n    const a = acc / total;\n    acc += d.value;\n    return { a, b: acc / total };\n  });\n\n  const segAt = (i: number, L: number) => ({\n    s0: (fracs[i].a - 0.5) * L + gap / 2,\n    s1: (fracs[i].b - 0.5) * L - gap / 2,\n  });\n\n  const stack = list.map((_, i) => {\n    const { s0, s1 } = segAt(i, stackW);\n    return { x0: cx + s0, x1: cx + s1, w: s1 - s0 };\n  });\n  const maxW = Math.max(...stack.map((s) => s.w), 1);\n  const chartTop = (height - (maxW + 44 * u)) / 2 + 16 * u;\n  const yB = chartTop + maxW;\n  const spacing = width * 0.15;\n  const colA = list.map((_, i) => cx + (i - (n - 1) / 2) * spacing);\n  const fx0 = width * 0.12;\n  const fx1 = width * 0.69;\n  const colB = list.map((_, i) =>\n    n > 1 ? lerp(fx0, fx1, i / (n - 1)) : (fx0 + fx1) / 2,\n  );\n  const P = list.map((_, i) => ({ x: colB[i], y: yB - stack[i].w }));\n\n  const drawK = interpolate(t, [DRAW[0], DRAW[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_DRAW,\n  });\n  const spin = interpolate(t, [SPIN[0], SPIN[1]], [-0.42, 0], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const un = interpolate(t, [UNROLL[0], UNROLL[1]], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const stageIn = interpolate(t, [0, 8], [0, 1], CLAMP);\n  const drift = interpolate(t, [0, 182], [-1, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n\n  const tipK = list.map((_, i) =>\n    interpolate(\n      t,\n      [TIP_START + i * TIP_STAGGER, TIP_START + i * TIP_STAGGER + TIP_DUR],\n      [0, 1],\n      { ...CLAMP, easing: EASE_IO },\n    ),\n  );\n  const colK = list.map((_, i) =>\n    interpolate(\n      t,\n      [\n        COLLAPSE_START + i * COLLAPSE_STAGGER,\n        COLLAPSE_START + i * COLLAPSE_STAGGER + COLLAPSE_DUR,\n      ],\n      [0, 1],\n      { ...CLAMP, easing: EASE_IO },\n    ),\n  );\n  const shift = interpolate(\n    t,\n    [\n      COLLAPSE_START,\n      COLLAPSE_START + COLLAPSE_DUR + (n - 1) * COLLAPSE_STAGGER,\n    ],\n    [0, 1],\n    { ...CLAMP, easing: EASE_IO },\n  );\n  const colX = list.map((_, i) => lerp(colA[i], colB[i], shift));\n\n  const lw = 5 * u;\n  const chevron = (i: number) => {\n    const Pi = P[i];\n    const prev = i > 0 ? P[i - 1] : null;\n    const next = i < n - 1 ? P[i + 1] : null;\n    const dirTo = (a: Pt, b: Pt) => {\n      const dx = b.x - a.x;\n      const dy = b.y - a.y;\n      const l = Math.hypot(dx, dy) || 1;\n      return { x: dx / l, y: dy / l };\n    };\n    const dL = prev ? dirTo(prev, Pi) : next ? dirTo(Pi, next) : { x: 1, y: 0 };\n    const dR = next ? dirTo(Pi, next) : dL;\n    const nL = { x: -dL.y, y: dL.x };\n    const nR = { x: -dR.y, y: dR.x };\n    const ov = 0.8 * u;\n    const mL = prev\n      ? {\n          x: (prev.x + Pi.x) / 2 - dL.x * ov,\n          y: (prev.y + Pi.y) / 2 - dL.y * ov,\n        }\n      : Pi;\n    const mR = next\n      ? {\n          x: (Pi.x + next.x) / 2 + dR.x * ov,\n          y: (Pi.y + next.y) / 2 + dR.y * ov,\n        }\n      : Pi;\n    const sum = { x: nL.x + nR.x, y: nL.y + nR.y };\n    const sl = Math.hypot(sum.x, sum.y) || 1;\n    const m = { x: sum.x / sl, y: sum.y / sl };\n    const ml = Math.min(2, 1 / Math.max(0.3, m.x * nL.x + m.y * nL.y));\n    const h = lw / 2;\n    return [\n      { x: mL.x + nL.x * h, y: mL.y + nL.y * h },\n      { x: mL.x - nL.x * h, y: mL.y - nL.y * h },\n      { x: Pi.x - m.x * h * ml, y: Pi.y - m.y * h * ml },\n      { x: mR.x - nR.x * h, y: mR.y - nR.y * h },\n      { x: mR.x + nR.x * h, y: mR.y + nR.y * h },\n      { x: Pi.x + m.x * h * ml, y: Pi.y + m.y * h * ml },\n    ];\n  };\n\n  const barPoly = (i: number) => {\n    const xc = colA[i];\n    const xl = xc - barT / 2;\n    const xr = xc + barT / 2;\n    const yT = yB - stack[i].w;\n    return [\n      { x: xl, y: yB },\n      { x: xl, y: yT },\n      { x: xc, y: yT },\n      { x: xr, y: yT },\n      { x: xr, y: yB },\n      { x: xc, y: yB },\n    ];\n  };\n\n  const shapes = list.map((d, i) => {\n    if (t < TIP_START) {\n      const L = lerp(L0, stackW, un);\n      const k = (TAU / L) * (1 - un);\n      const thick = lerp(ringT, barT, un);\n      const ay = lerp(cy + ringR, cy, un);\n      const seg = segAt(i, L);\n      const revealFrom = L / 2 - drawK * L;\n      const s0 = Math.max(seg.s0, revealFrom);\n      if (s0 >= seg.s1)\n        return { key: d.label, d: \"\", color: colors[i], split: undefined };\n      return {\n        key: d.label,\n        d: bandPath(s0, seg.s1, thick, k, cx, ay, spin, center),\n        color: colors[i],\n        split: undefined,\n      };\n    }\n    if (t < COLLAPSE_START) {\n      const e = tipK[i];\n      const s = stack[i];\n      const pivot0 = { x: s.x0, y: cy + barT / 2 };\n      const pivot1 = { x: colA[i] + barT / 2, y: yB };\n      const drop = interpolate(e, [0.25, 1], [0, 1], {\n        ...CLAMP,\n        easing: EASE_IO,\n      });\n      const pvl = {\n        x: lerp(pivot0.x, pivot1.x, e),\n        y: lerp(pivot0.y, pivot1.y, drop),\n      };\n      const a = (-Math.PI / 2) * e;\n      const corners = [\n        { x: 0, y: 0 },\n        { x: 0, y: -barT },\n        { x: s.w, y: -barT },\n        { x: s.w, y: 0 },\n      ].map((q) => rotateAbout({ x: pvl.x + q.x, y: pvl.y + q.y }, pvl, a));\n      return {\n        key: d.label,\n        d: toPath(corners),\n        color: colors[i],\n        split: undefined,\n      };\n    }\n    const e = colK[i];\n    const from = barPoly(i);\n    const to = chevron(i);\n    const pts = from.map((q, j) => lerpPt(q, to[j], e));\n    const color =\n      i === hl ? heroFill : interpolateColors(e, [0, 1], [colors[i], p.line]);\n    const toward =\n      i === hl\n        ? heroFill\n        : interpolateColors(\n            e,\n            [0, 1],\n            [colors[i], i < hl ? p.limb[0] : p.limb[2]],\n          );\n    const split =\n      i === hl - 1\n        ? { x: pts[2].x, left: color, right: toward }\n        : i === hl + 1\n          ? { x: pts[2].x, left: toward, right: color }\n          : undefined;\n    return { key: d.label, d: toPath(pts), color, split };\n  });\n\n  const labelsIn = interpolate(t, [TIP_START + 22, TIP_START + 40], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const baseIn = interpolate(t, [TIP_START + 14, TIP_START + 38], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const baseL = lerp(colA[0] - barT, colB[0], shift);\n  const baseR = lerp(colA[n - 1] + barT, colB[n - 1], shift);\n\n  const dotsIn = interpolate(t, [LABEL_IN - 6, LABEL_IN + 10], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const labIn = interpolate(t, [LABEL_IN, LABEL_IN + 20], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const capIn = interpolate(t, [LABEL_IN + 6, LABEL_IN + 24], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const push = interpolate(t, [0, LABEL_IN + 40], [1.03, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const end = P[n - 1];\n  const areaIn = interpolate(t, [LABEL_IN - 6, LABEL_IN + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const areaRise = interpolate(areaIn, [0, 1], [0.4, 1]);\n  const endColor = hl === n - 1 ? p.heroDot : p.line;\n  const valueText = `${Math.round(last?.value ?? 0)}${suffix}`;\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, opacity: stageIn }}\n          aria-hidden\n        >\n          <line\n            x1={baseL}\n            x2={lerp(baseL, baseR, baseIn)}\n            y1={yB + 0.5}\n            y2={yB + 0.5}\n            stroke={p.hair}\n            strokeWidth={Math.max(1, u)}\n            opacity={baseIn > 0 ? 1 : 0}\n          />\n          <defs>\n            <linearGradient id={`${gid}-hero`} x1={0} y1={1} x2={1} y2={0}>\n              <stop offset={0} stopColor={p.limb[0]} />\n              <stop offset={0.55 + 0.1 * drift} stopColor={p.limb[1]} />\n              <stop offset={1} stopColor={p.limb[2]} />\n            </linearGradient>\n            <linearGradient\n              id={`${gid}-area`}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={P[0]?.x ?? 0}\n              y1={yB}\n              x2={end?.x ?? width}\n              y2={end?.y ?? 0}\n            >\n              <stop offset={0} stopColor={p.areaStops[0]} stopOpacity={0.2} />\n              <stop\n                offset={0.6}\n                stopColor={p.areaStops[1]}\n                stopOpacity={0.55}\n              />\n              <stop offset={1} stopColor={p.areaStops[2]} stopOpacity={0.9} />\n            </linearGradient>\n            <linearGradient\n              id={`${gid}-fade`}\n              gradientUnits=\"userSpaceOnUse\"\n              x1={0}\n              y1={end?.y ?? 0}\n              x2={0}\n              y2={yB}\n            >\n              <stop offset={0} stopColor=\"#fff\" stopOpacity={1} />\n              <stop offset={1} stopColor=\"#fff\" stopOpacity={0} />\n            </linearGradient>\n            <mask id={`${gid}-mask`}>\n              <rect width={width} height={height} fill={`url(#${gid}-fade)`} />\n            </mask>\n            {shapes.map((s, i) =>\n              s.split ? (\n                <linearGradient\n                  key={s.key}\n                  id={`${gid}-${i}`}\n                  gradientUnits=\"userSpaceOnUse\"\n                  x1={s.split.x - 0.5}\n                  x2={s.split.x + 0.5}\n                  y1={0}\n                  y2={0}\n                >\n                  <stop offset=\"0\" stopColor={s.split.left} />\n                  <stop offset=\"0.5\" stopColor={s.split.left} />\n                  <stop offset=\"0.5\" stopColor={s.split.right} />\n                  <stop offset=\"1\" stopColor={s.split.right} />\n                </linearGradient>\n              ) : null,\n            )}\n          </defs>\n          {areaIn > 0 && n > 1 ? (\n            <path\n              d={`${P.map((q, i) => `${i ? \"L\" : \"M\"} ${q.x.toFixed(2)} ${(yB - (yB - q.y) * areaRise).toFixed(2)}`).join(\" \")} L ${P[n - 1].x} ${yB} L ${P[0].x} ${yB} Z`}\n              fill={`url(#${gid}-area)`}\n              mask={`url(#${gid}-mask)`}\n              opacity={areaIn * p.area}\n            />\n          ) : null}\n          {shapes.map((s, i) =>\n            s.d ? (\n              <path\n                key={s.key}\n                d={s.d}\n                fill={s.split ? `url(#${gid}-${i})` : s.color}\n              />\n            ) : null,\n          )}\n          {P.slice(0, n - 1).map((q, i) => (\n            <circle\n              key={list[i].label}\n              cx={q.x}\n              cy={q.y}\n              r={(4 + 2 * dotsIn) * u}\n              fill={p.bg}\n              stroke={p.line}\n              strokeWidth={2.5 * u}\n              opacity={dotsIn}\n            />\n          ))}\n          {end ? (\n            <>\n              <circle\n                cx={end.x}\n                cy={end.y}\n                r={(10 + 6 * dotsIn) * u}\n                fill=\"none\"\n                stroke={accent}\n                strokeWidth={1.5 * u}\n                opacity={0.7 * dotsIn}\n              />\n              <circle\n                cx={end.x}\n                cy={end.y}\n                r={(5 + 3 * dotsIn) * u}\n                fill={endColor}\n                opacity={dotsIn}\n              />\n            </>\n          ) : null}\n        </svg>\n\n        {list.map((d, i) => (\n          <div\n            key={d.label}\n            style={{\n              position: \"absolute\",\n              left: colX[i],\n              top: yB + 18 * u,\n              transform: `translateX(-50%) translateY(${(1 - labelsIn) * 8 * u}px)`,\n              fontFamily: sans,\n              fontWeight: 500,\n              fontSize: 16 * u,\n              letterSpacing: \"0.01em\",\n              color: i === hl ? p.fg : p.muted,\n              opacity: labelsIn,\n              whiteSpace: \"nowrap\",\n            }}\n          >\n            {d.label}\n          </div>\n        ))}\n\n        {end ? (\n          <div\n            style={{\n              position: \"absolute\",\n              left: end.x + 34 * u,\n              top: end.y,\n              transform: \"translateY(-50%)\",\n              display: \"flex\",\n              flexDirection: \"column\",\n              alignItems: \"flex-start\",\n            }}\n          >\n            <div\n              style={{\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 116 * u,\n                lineHeight: 0.92,\n                letterSpacing: \"-0.05em\",\n                color: p.fg,\n                fontVariantNumeric: \"tabular-nums\",\n                whiteSpace: \"nowrap\",\n                opacity: labIn,\n                transform: `translateX(${(1 - labIn) * -18 * u}px)`,\n                filter:\n                  labIn < 0.99 ? `blur(${(1 - labIn) * 6 * u}px)` : undefined,\n              }}\n            >\n              {valueText}\n            </div>\n            <div\n              style={{\n                marginTop: 10 * u,\n                fontFamily: sans,\n                fontWeight: 400,\n                fontSize: 21 * u,\n                letterSpacing: \"-0.01em\",\n                color: p.muted,\n                whiteSpace: \"nowrap\",\n                opacity: capIn,\n                transform: `translateY(${(1 - capIn) * 6 * u}px)`,\n              }}\n            >\n              {endCaption}\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/chart-morph.tsx"
    }
  ],
  "type": "registry:component"
}
