{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "waterfall-chart",
  "title": "Waterfall Chart",
  "description": "A revenue waterfall steps through gains and losses into a gradient total of $84k MRR.",
  "dependencies": ["remotion", "@remotion/google-fonts"],
  "files": [
    {
      "path": "registry/remocn/waterfall-chart/index.tsx",
      "content": "\"use client\";\n\nimport { loadFont } from \"@remotion/google-fonts/InstrumentSans\";\nimport { useId } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nconst { fontFamily: sans } = loadFont(\"normal\", {\n  weights: [\"400\", \"500\"],\n  subsets: [\"latin\"],\n});\n\n// gradients.sh \"mesh\" palette (bg #0a0d24): #ecdcf2, #8796ee, #2e45b7, #0e1645.\nconst MESH = [\"#ecdcf2\", \"#8796ee\", \"#2e45b7\", \"#0e1645\"] as const;\n\n/** Mesh layers sized for a tall bar: soft wide blobs over a vertical ramp. */\nfunction mesh(drift: number, base: string) {\n  return [\n    `radial-gradient(130% 34% at ${28 + 14 * drift}% 6%, ${MESH[0]} 0%, transparent 72%)`,\n    `radial-gradient(150% 40% at ${74 - 10 * drift}% 34%, color-mix(in oklab, ${MESH[1]} 70%, transparent) 0%, transparent 70%)`,\n    `radial-gradient(160% 46% at ${30 + 12 * drift}% 86%, color-mix(in oklab, ${MESH[2]} 80%, transparent) 0%, transparent 72%)`,\n    base,\n  ].join(\", \");\n}\n\nexport interface WaterfallStep {\n  label: string;\n  value: number;\n}\n\nexport interface WaterfallChartProps {\n  start?: WaterfallStep;\n  steps?: WaterfallStep[];\n  totalLabel?: string;\n  title?: string;\n  format?: (value: number) => string;\n  accent?: string;\n  theme?: \"dark\" | \"light\";\n  speed?: number;\n}\n\nconst DEFAULT_START: WaterfallStep = { label: \"Start\", value: 52 };\n\nconst DEFAULT_STEPS: WaterfallStep[] = [\n  { label: \"Churn\", value: -7 },\n  { label: \"Contraction\", value: -3 },\n  { label: \"New\", value: 24 },\n  { label: \"Expansion\", value: 12 },\n  { label: \"Reactivation\", value: 6 },\n];\n\nconst defaultFormat = (v: number) => `$${Math.round(v)}k`;\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_BAR = Easing.bezier(0.3, 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    axis: \"rgba(255,255,255,0.2)\",\n    hair: \"rgba(255,255,255,0.32)\",\n    base: \"#6A696F\",\n    up: \"#C9C7C2\",\n    down: \"#2E2D33\",\n    downEdge: \"rgba(255,255,255,0.16)\",\n    grain: 0.06,\n  },\n  light: {\n    bg: \"#F5F4F0\",\n    fg: \"#121214\",\n    muted: \"#6E6C67\",\n    axis: \"rgba(18,18,20,0.2)\",\n    hair: \"rgba(18,18,20,0.32)\",\n    base: \"#9C9A95\",\n    up: \"#3F3E3B\",\n    down: \"#E3E1DB\",\n    downEdge: \"rgba(18,18,20,0.24)\",\n    grain: 0.045,\n  },\n} as const;\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\ninterface Column {\n  key: string;\n  label: string;\n  kind: \"start\" | \"up\" | \"down\" | \"total\";\n  from: number;\n  to: number;\n  delta: number;\n  start: number;\n  dur: number;\n}\n\nexport function WaterfallChart({\n  start = DEFAULT_START,\n  steps = DEFAULT_STEPS,\n  totalLabel = \"MRR\",\n  title = \"Net revenue, Q3\",\n  format = defaultFormat,\n  accent = \"#3B5BFD\",\n  theme = \"dark\",\n  speed = 1,\n}: WaterfallChartProps) {\n  const t = useCurrentFrame() * speed;\n  const { width, height, fps } = useVideoConfig();\n  const u = Math.min(width / 1280, height / 720);\n  const p = THEMES[theme];\n  const list = steps.slice(0, 8);\n\n  const cols: Column[] = [];\n  cols.push({\n    key: \"start\",\n    label: start.label,\n    kind: \"start\",\n    from: 0,\n    to: start.value,\n    delta: start.value,\n    start: 8,\n    dur: 22,\n  });\n  let level = start.value;\n  const STAGGER = list.length > 5 ? 7 : 9;\n  list.forEach((s, i) => {\n    const next = level + s.value;\n    cols.push({\n      key: `s${i}`,\n      label: s.label,\n      kind: s.value < 0 ? \"down\" : \"up\",\n      from: level,\n      to: next,\n      delta: s.value,\n      start: 26 + i * STAGGER,\n      dur: 15,\n    });\n    level = next;\n  });\n  const total = level;\n  const TOTAL_AT = 26 + list.length * STAGGER + 6;\n  cols.push({\n    key: \"total\",\n    label: totalLabel,\n    kind: \"total\",\n    from: 0,\n    to: total,\n    delta: total,\n    start: TOTAL_AT,\n    dur: 26,\n  });\n\n  const maxLevel = Math.max(\n    1,\n    ...cols.map((c) => Math.max(c.from, c.to)),\n    total,\n  );\n\n  const plotL = width * 0.1;\n  const plotR = width * 0.9;\n  const baseY = height * 0.82;\n  const topY = height * 0.34;\n  const yOf = (v: number) => baseY - ((baseY - topY) * v) / maxLevel;\n  const slot = (plotR - plotL) / cols.length;\n  const colW = Math.min(slot * 0.58, 92 * u);\n  const xOf = (i: number) => plotL + slot * (i + 0.5);\n\n  const axisIn = interpolate(t, [0, 18], [0, 1], { ...CLAMP, easing: EASE_IO });\n  const titleIn = interpolate(t, [4, 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n\n  const totalRise = spring({\n    frame: t - TOTAL_AT,\n    fps,\n    config: { damping: 15, stiffness: 120, mass: 0.8 },\n  });\n  const count = interpolate(t, [TOTAL_AT, TOTAL_AT + 24], [0, 1], {\n    ...CLAMP,\n    easing: Easing.bezier(0.22, 1, 0.36, 1),\n  });\n  const land = interpolate(t, [TOTAL_AT + 2, TOTAL_AT + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const unitIn = interpolate(t, [TOTAL_AT + 12, TOTAL_AT + 30], [0, 1], {\n    ...CLAMP,\n    easing: EASE_OUT,\n  });\n  const focus = interpolate(t, [TOTAL_AT + 14, TOTAL_AT + 34], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const push = interpolate(t, [0, TOTAL_AT + 60], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const scale = 0.975 + 0.025 * push;\n  const settle = interpolate(t, [TOTAL_AT - 14, TOTAL_AT + 22], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const shift = (1 - settle) * -0.075 * height;\n\n  const progress = (c: Column) =>\n    c.kind === \"total\"\n      ? Math.max(0, totalRise)\n      : interpolate(t, [c.start, c.start + c.dur], [0, 1], {\n          ...CLAMP,\n          easing: EASE_BAR,\n        });\n\n  const sw = Math.max(1, u);\n  const radius = 2 * u;\n  const totalK = Math.max(0, totalRise);\n  const barFull = baseY - yOf(total);\n  const drift = interpolate(t, [0, TOTAL_AT + 70], [0, 1], {\n    ...CLAMP,\n    easing: EASE_IO,\n  });\n  const barBase = `linear-gradient(180deg, ${MESH[0]} 0%, ${MESH[1]} 34%, ${MESH[2]} 82%, #283da6 100%)`;\n  const labelSize = 16 * u;\n  const valueSize = 17 * u;\n\n  return (\n    <AbsoluteFill style={{ background: p.bg, overflow: \"hidden\" }}>\n      <AbsoluteFill\n        style={{ transform: `scale(${scale})`, transformOrigin: \"50% 55%\" }}\n      >\n        <AbsoluteFill style={{ transform: `translateY(${shift}px)` }}>\n          <svg\n            width={width}\n            height={height}\n            style={{ position: \"absolute\", inset: 0 }}\n            aria-hidden\n          >\n            <line\n              x1={plotL}\n              x2={plotL + (plotR - plotL) * axisIn}\n              y1={baseY + sw / 2}\n              y2={baseY + sw / 2}\n              stroke={p.axis}\n              strokeWidth={sw}\n            />\n            {cols.map((c, i) => {\n              if (i === 0) return null;\n              const prev = cols[i - 1];\n              const y = yOf(prev.to);\n              const x0 = xOf(i - 1) + colW / 2;\n              const x1 = xOf(i) - colW / 2;\n              const k = interpolate(t, [c.start - 7, c.start + 1], [0, 1], {\n                ...CLAMP,\n                easing: EASE_IO,\n              });\n              if (k <= 0) return null;\n              const dim = c.kind === \"total\" ? 1 : 1 - 0.45 * focus;\n              return (\n                <line\n                  key={`c${c.key}`}\n                  x1={x0}\n                  x2={x0 + (x1 - x0) * k}\n                  y1={y}\n                  y2={y}\n                  stroke={p.hair}\n                  strokeWidth={sw}\n                  opacity={dim}\n                />\n              );\n            })}\n            {cols.map((c, i) => {\n              const k = progress(c);\n              if (k <= 0 || c.kind === \"total\") return null;\n              const anchor = c.from;\n              const end = anchor + (c.to - anchor) * k;\n              const lo = Math.min(anchor, end);\n              const hi = Math.max(anchor, end);\n              const y0 = yOf(hi);\n              const h = Math.max(sw, yOf(lo) - y0);\n              const fill =\n                c.kind === \"start\" ? p.base : c.kind === \"up\" ? p.up : p.down;\n              const op = 1 - 0.4 * focus;\n              return (\n                <rect\n                  key={c.key}\n                  x={xOf(i) - colW / 2}\n                  y={y0}\n                  width={colW}\n                  height={h}\n                  rx={radius}\n                  fill={fill}\n                  stroke={c.kind === \"down\" ? p.downEdge : \"none\"}\n                  strokeWidth={c.kind === \"down\" ? sw : 0}\n                  opacity={op}\n                />\n              );\n            })}\n          </svg>\n\n          {totalK > 0 ? (\n            <div\n              style={{\n                position: \"absolute\",\n                left: xOf(cols.length - 1) - colW / 2,\n                top: yOf(total * totalK),\n                width: colW,\n                height: baseY - yOf(total * totalK),\n                borderRadius: radius,\n                overflow: \"hidden\",\n                background: MESH[0],\n              }}\n            >\n              <div\n                style={{\n                  position: \"absolute\",\n                  left: 0,\n                  right: 0,\n                  bottom: 0,\n                  height: barFull,\n                  background: mesh(drift, barBase),\n                }}\n              />\n            </div>\n          ) : null}\n\n          {cols.map((c, i) => {\n            const a = interpolate(t, [c.start, c.start + 14], [0, 1], {\n              ...CLAMP,\n              easing: EASE_OUT,\n            });\n            const dim = c.kind === \"total\" ? 1 : 1 - 0.35 * focus;\n            return (\n              <div\n                key={`l${c.key}`}\n                style={{\n                  position: \"absolute\",\n                  left: xOf(i),\n                  top: baseY + 14 * u,\n                  transform: `translate(-50%, ${(1 - a) * 6 * u}px)`,\n                  fontFamily: sans,\n                  fontWeight: 400,\n                  fontSize: labelSize,\n                  color: c.kind === \"total\" ? p.fg : p.muted,\n                  whiteSpace: \"nowrap\",\n                  opacity: a * dim,\n                }}\n              >\n                {c.label}\n              </div>\n            );\n          })}\n\n          {cols.map((c, i) => {\n            if (c.kind === \"total\") return null;\n            const a = interpolate(\n              t,\n              [c.start + c.dur - 8, c.start + c.dur + 6],\n              [0, 1],\n              { ...CLAMP, easing: EASE_OUT },\n            );\n            const top = yOf(Math.max(c.from, c.to));\n            const text =\n              c.kind === \"start\"\n                ? format(c.delta)\n                : `${c.delta < 0 ? \"−\" : \"+\"}${format(Math.abs(c.delta))}`;\n            return (\n              <div\n                key={`v${c.key}`}\n                style={{\n                  position: \"absolute\",\n                  left: xOf(i),\n                  top: top - 10 * u,\n                  transform: `translate(-50%, ${-100 + (1 - a) * 18}%)`,\n                  fontFamily: sans,\n                  fontWeight: 500,\n                  fontSize: valueSize,\n                  letterSpacing: \"-0.005em\",\n                  fontVariantNumeric: \"tabular-nums\",\n                  color: c.kind === \"down\" ? p.muted : p.fg,\n                  whiteSpace: \"nowrap\",\n                  opacity: a * (1 - 0.4 * focus),\n                }}\n              >\n                {text}\n              </div>\n            );\n          })}\n\n          <div\n            style={{\n              position: \"absolute\",\n              left: plotL,\n              top: height * 0.255,\n              transform: `translateY(-100%) translateY(${(1 - titleIn) * 8 * u}px)`,\n              fontFamily: sans,\n              fontWeight: 400,\n              fontSize: 20 * u,\n              letterSpacing: \"-0.01em\",\n              color: p.muted,\n              whiteSpace: \"nowrap\",\n              opacity: titleIn,\n            }}\n          >\n            {title}\n          </div>\n        </AbsoluteFill>\n\n        <div\n          style={{\n            position: \"absolute\",\n            right: width - plotR,\n            top: height * 0.255,\n            transform: \"translateY(-100%)\",\n            display: \"flex\",\n            alignItems: \"baseline\",\n            gap: 14 * u,\n            whiteSpace: \"nowrap\",\n          }}\n        >\n          <span\n            style={{\n              fontFamily: sans,\n              fontWeight: 400,\n              fontSize: 136 * u,\n              lineHeight: 0.86,\n              letterSpacing: \"-0.045em\",\n              paddingRight: \"0.05em\",\n              color: \"transparent\",\n              backgroundImage: `linear-gradient(100deg, ${theme === \"dark\" ? MESH[0] : MESH[2]} ${4 + 10 * drift}%, ${MESH[1]} 48%, ${theme === \"dark\" ? MESH[2] : MESH[3]} 100%)`,\n              WebkitBackgroundClip: \"text\",\n              backgroundClip: \"text\",\n              fontVariantNumeric: \"tabular-nums\",\n              display: \"inline-block\",\n              opacity: land,\n              filter: land < 0.99 ? `blur(${(1 - land) * 8 * u}px)` : undefined,\n              transform: `translateY(${(1 - land) * 20 * u}px)`,\n            }}\n          >\n            {format(total * count)}\n          </span>\n          <span\n            style={{\n              display: \"inline-flex\",\n              alignItems: \"center\",\n              gap: 10 * u,\n              fontFamily: sans,\n              fontWeight: 400,\n              fontSize: 30 * u,\n              letterSpacing: \"-0.015em\",\n              color: p.muted,\n              opacity: unitIn,\n              transform: `translateY(${(1 - unitIn) * 8 * u}px)`,\n            }}\n          >\n            <span\n              style={{\n                width: 10 * u,\n                height: 10 * u,\n                borderRadius: \"50%\",\n                background: accent,\n              }}\n            />\n            {totalLabel}\n          </span>\n        </div>\n      </AbsoluteFill>\n\n      <AbsoluteFill\n        style={{\n          background: `radial-gradient(ellipse 85% 85% at 50% 50%, transparent 64%, ${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/waterfall-chart.tsx"
    }
  ],
  "type": "registry:component"
}
