{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","categories":["data","minimal"],"dependencies":["@tanstack/charts"],"description":"A four-card editorial chart set — rung bars, a hairline dot-line, a tick-ring dial, and a dot waffle — drawn ink-on-paper by TanStack Charts, with spring-staggered entrances and quarterly data that morphs on a loop.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { defineChart, dot } from \"@tanstack/charts\";\nimport { motion } from \"@tanstack/charts/motion\";\nimport { Chart } from \"@tanstack/charts/react/core\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { useMemo } from \"react\";\n\nimport type { MixRow } from \"../lib/data\";\nimport { SPRING } from \"../lib/tokens\";\n\nconst renderer = motion({ initial: \"always\", transition: SPRING });\n\ninterface Cell {\n  id: string;\n  col: number;\n  row: number;\n  plan: string;\n}\n\n// A hundred dots, one per percent. Cells fill left to right, top to bottom;\n// keys carry the plan so a shifted percent pops out and back in.\nexport const DotWaffle = ({\n  mix,\n  reveal,\n  size,\n  ladder,\n}: {\n  mix: MixRow[];\n  /** 0 → 1 entrance progress; the grid inks in dot by dot. */\n  reveal: number;\n  size: number;\n  ladder: readonly string[];\n}) => {\n  const revealed = Math.min(100, Math.floor(reveal * 101));\n\n  const definition = useMemo(() => {\n    const bounds: number[] = [];\n    let acc = 0;\n    for (const m of mix) {\n      acc += m.share;\n      bounds.push(acc);\n    }\n    const cells: Cell[] = Array.from({ length: revealed }, (_, i) => {\n      const slot = bounds.findIndex((b) => i < b);\n      const plan = mix[slot === -1 ? mix.length - 1 : slot]?.plan ?? \"\";\n      // Fill left → right, top → bottom, so the darkest plan starts where the\n      // legend starts reading.\n      return { col: i % 10, id: `${i}:${plan}`, plan, row: 9 - Math.floor(i / 10) };\n    });\n    return defineChart({\n      color: { domain: mix.map((m) => m.plan), range: [...ladder] },\n      guides: false,\n      margin: 0,\n      marks: [\n        dot(cells, {\n          color: \"plan\",\n          key: \"id\",\n          r: size / 30,\n          x: \"col\",\n          y: \"row\",\n        }),\n      ],\n      scales: {\n        x: { scale: scaleLinear().domain([-0.65, 9.65]) },\n        y: { scale: scaleLinear().domain([-0.65, 9.65]) },\n      },\n    });\n  }, [mix, revealed, size, ladder]);\n\n  return (\n    <Chart\n      definition={definition}\n      renderer={renderer}\n      width={size}\n      height={size}\n      ariaLabel=\"New accounts by plan, one dot per percent\"\n    />\n  );\n};\n","path":"/charts/dot-waffle.tsx","target":"uicapsule/editorial-charts/charts/dot-waffle.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { defineChart, dot, lineY, text, tickX } from \"@tanstack/charts\";\nimport { motion } from \"@tanstack/charts/motion\";\nimport { Chart } from \"@tanstack/charts/react/core\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { useMemo } from \"react\";\n\nimport type { DayRow } from \"../lib/data\";\nimport { FAINT, INK, MUTED, PAPER, SPRING } from \"../lib/tokens\";\n\nconst renderer = motion({ initial: \"always\", transition: SPRING });\n\n// One dot = one day, hollow = weekend, barcode floor keeps the calendar\n// honest. Days land dot by dot; the hairline threads them once the month is\n// complete.\nexport const HairlineLine = ({\n  days,\n  reveal,\n  yMax,\n  height,\n  initialWidth,\n}: {\n  days: DayRow[];\n  /** 0 → 1 entrance progress; days land left to right as it advances. */\n  reveal: number;\n  yMax: number;\n  height: number;\n  initialWidth: number;\n}) => {\n  const revealed = Math.min(days.length, Math.floor(reveal * (days.length + 1)));\n\n  const definition = useMemo(() => {\n    const shown = days.slice(0, revealed);\n    const complete = revealed >= days.length;\n    const weekdays = shown.filter((d) => !d.weekend);\n    const weekends = shown.filter((d) => d.weekend);\n    let [peak] = days;\n    for (const d of days) {\n      if (peak === undefined || d.value > peak.value) {\n        peak = d;\n      }\n    }\n    const last = days.at(-1);\n    // Two callouts crowd each other when the peak already sits near day 30.\n    const callouts: DayRow[] = [];\n    if (complete && peak !== undefined) {\n      callouts.push(peak);\n      if (last !== undefined && last.day !== peak.day && peak.day < 26) {\n        callouts.push(last);\n      }\n    }\n    return defineChart({\n      guides: false,\n      margin: 0,\n      marks: [\n        tickX(shown, {\n          key: \"day\",\n          length: 8,\n          stroke: FAINT,\n          strokeWidth: 1,\n          x: \"day\",\n          y: () => 0,\n        }),\n        ...(complete\n          ? [\n              lineY(days, {\n                key: \"day\",\n                stroke: INK,\n                strokeWidth: 1.25,\n                x: \"day\",\n                y: \"value\",\n              }),\n            ]\n          : []),\n        dot(weekdays, {\n          fill: INK,\n          id: \"weekday-dots\",\n          key: \"day\",\n          r: 2.4,\n          x: \"day\",\n          y: \"value\",\n        }),\n        dot(weekends, {\n          fill: PAPER,\n          id: \"weekend-dots\",\n          key: \"day\",\n          r: 2.6,\n          stroke: INK,\n          strokeWidth: 1.1,\n          x: \"day\",\n          y: \"value\",\n        }),\n        text(callouts, {\n          anchor: (d) => (d.day >= 27 ? \"end\" : \"middle\"),\n          dx: (d) => (d.day >= 27 ? 3 : 0),\n          dy: -13,\n          fill: INK,\n          fontSize: 11.5,\n          fontWeight: 800,\n          key: \"day\",\n          text: (d) => `${d.value}`,\n          x: \"day\",\n          y: \"value\",\n        }),\n      ],\n      motion: { path: \"morph\" },\n      scales: {\n        x: { scale: scaleLinear().domain([1, 30]) },\n        y: { scale: scaleLinear().domain([0, yMax * 1.2]) },\n      },\n    });\n  }, [days, revealed, yMax]);\n\n  return (\n    <div>\n      <Chart\n        definition={definition}\n        renderer={renderer}\n        height={height}\n        initialWidth={initialWidth}\n        ariaLabel=\"Daily sign-ups over thirty days\"\n      />\n      <div\n        className=\"relative h-5 pt-1.5 text-[9px] font-semibold tracking-[0.09em]\"\n        style={{ color: MUTED }}\n      >\n        <span className=\"absolute left-0\">DAY 1</span>\n        <span className=\"absolute left-[48.3%] -translate-x-1/2\">DAY 15</span>\n        <span className=\"absolute right-0\">DAY 30</span>\n      </div>\n    </div>\n  );\n};\n","path":"/charts/hairline-line.tsx","target":"uicapsule/editorial-charts/charts/hairline-line.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { defineChart, text, tickY } from \"@tanstack/charts\";\nimport { motion } from \"@tanstack/charts/motion\";\nimport { Chart } from \"@tanstack/charts/react/core\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { scalePoint } from \"@tanstack/charts/scales/point\";\nimport { useMemo } from \"react\";\n\nimport type { PlanRow } from \"../lib/data\";\nimport { INK, MUTED, SPRING } from \"../lib/tokens\";\n\nconst renderer = motion({ initial: \"always\", transition: SPRING });\n\n// One rung = $2k of MRR. The bar is a ladder you can count.\nconst RUNG = 2;\n\ninterface Rung {\n  id: string;\n  plan: string;\n  y: number;\n  fifth: boolean;\n}\n\nexport const RungBars = ({\n  plans,\n  reveal,\n  height,\n  initialWidth,\n}: {\n  plans: PlanRow[];\n  /** 0 → 1 entrance progress; rungs climb in as it advances. */\n  reveal: number;\n  height: number;\n  initialWidth: number;\n}) => {\n  const totalRungs = plans.reduce((sum, p) => sum + Math.round(p.value / RUNG), 0);\n  const revealed = Math.min(totalRungs, Math.floor(reveal * (totalRungs + 1)));\n\n  const definition = useMemo(() => {\n    const rungs: Rung[] = plans.flatMap((p) =>\n      Array.from({ length: Math.round(p.value / RUNG) }, (_, i) => ({\n        fifth: (i + 1) % 5 === 0,\n        id: `${p.plan}-${i}`,\n        plan: p.plan,\n        y: (i + 1) * RUNG,\n      })),\n    );\n    // The ladder climbs column by column, rung by rung.\n    const shown = rungs.slice(0, revealed);\n    const done = new Map<string, number>();\n    for (const r of shown) {\n      done.set(r.plan, (done.get(r.plan) ?? 0) + 1);\n    }\n    const labeled = plans.filter((p) => (done.get(p.plan) ?? 0) >= Math.round(p.value / RUNG));\n    const top = Math.max(...plans.map((p) => p.value)) * 1.36;\n    return defineChart({\n      guides: false,\n      margin: 0,\n      marks: [\n        tickY(shown, {\n          key: \"id\",\n          span: 0.5,\n          stroke: (d) => (d.fifth ? INK : \"rgba(27,27,25,0.55)\"),\n          strokeWidth: 1.2,\n          x: \"plan\",\n          y: \"y\",\n        }),\n        text(labeled, {\n          dy: -13,\n          fill: INK,\n          fontSize: 12,\n          fontWeight: 800,\n          key: \"plan\",\n          text: (d) => `${d.value}`,\n          x: \"plan\",\n          y: \"value\",\n        }),\n      ],\n      scales: {\n        // Fixed domains keep columns and headroom still while marks enter.\n        x: {\n          scale: scalePoint<string>()\n            .domain(plans.map((p) => p.plan))\n            .padding(0.5),\n        },\n        y: { scale: scaleLinear().domain([0, top]) },\n      },\n    });\n  }, [plans, revealed]);\n\n  return (\n    <div>\n      <Chart\n        definition={definition}\n        renderer={renderer}\n        height={height}\n        initialWidth={initialWidth}\n        ariaLabel=\"Monthly recurring revenue by plan, one rung per two thousand dollars\"\n      />\n      <div className=\"flex\" style={{ color: MUTED }}>\n        {plans.map((p) => (\n          <span\n            key={p.plan}\n            className=\"flex-1 pt-1.5 text-center text-[9px] font-semibold tracking-[0.09em]\"\n          >\n            {p.plan}\n          </span>\n        ))}\n      </div>\n    </div>\n  );\n};\n","path":"/charts/rung-bars.tsx","target":"uicapsule/editorial-charts/charts/rung-bars.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { defineChart, vector } from \"@tanstack/charts\";\nimport { motion } from \"@tanstack/charts/motion\";\nimport { Chart } from \"@tanstack/charts/react/core\";\nimport { scaleLinear } from \"@tanstack/charts/scales/linear\";\nimport { useMemo } from \"react\";\n\nimport type { ChannelRow } from \"../lib/data\";\nimport { SPRING } from \"../lib/tokens\";\n\nconst renderer = motion({ initial: \"always\", transition: SPRING });\n\ninterface RingTick {\n  id: number;\n  x: number;\n  y: number;\n  rotate: number;\n  channel: string;\n  major: boolean;\n}\n\n// 100 radial dashes, one per percent, read clockwise from noon. The reveal\n// sweeps the dial once; after that each scene twists it a few degrees so the\n// update itself is visible motion.\nexport const TickRing = ({\n  channels,\n  reveal,\n  size,\n  twist,\n  ladder,\n}: {\n  channels: ChannelRow[];\n  /** 0 → 1 entrance progress; the dial sweeps clockwise as it advances. */\n  reveal: number;\n  size: number;\n  twist: number;\n  ladder: readonly string[];\n}) => {\n  const revealed = Math.min(100, Math.floor(reveal * 101));\n\n  const definition = useMemo(() => {\n    const bounds: number[] = [];\n    let acc = 0;\n    for (const c of channels) {\n      acc += c.share;\n      bounds.push(acc);\n    }\n    const shade = new Map(channels.map((c, i) => [c.channel, ladder[i % ladder.length]]));\n    const ticks: RingTick[] = Array.from({ length: revealed }, (_, i) => {\n      const deg = i * 3.6 + twist;\n      const rad = (deg * Math.PI) / 180;\n      const slot = bounds.findIndex((b) => i < b);\n      const row = channels[slot === -1 ? channels.length - 1 : slot];\n      return {\n        channel: row?.channel ?? \"\",\n        id: i,\n        major: i % 10 === 0,\n        rotate: deg + 180,\n        x: Math.sin(rad),\n        y: Math.cos(rad),\n      };\n    });\n    return defineChart({\n      guides: false,\n      margin: 0,\n      marks: [\n        vector(ticks, {\n          anchor: \"start\",\n          headLength: 0,\n          key: \"id\",\n          length: (d) => (d.major ? 26 : 18),\n          rotate: \"rotate\",\n          stroke: (d) => shade.get(d.channel) ?? ladder[0] ?? \"#f1f0ec\",\n          strokeWidth: 2.4,\n          x: \"x\",\n          y: \"y\",\n        }),\n      ],\n      scales: {\n        x: { scale: scaleLinear().domain([-1, 1]) },\n        y: { scale: scaleLinear().domain([-1, 1]) },\n      },\n    });\n  }, [channels, revealed, twist, ladder]);\n\n  return (\n    <Chart\n      definition={definition}\n      renderer={renderer}\n      width={size}\n      height={size}\n      ariaLabel=\"Sessions by acquisition channel, one tick per percent\"\n    />\n  );\n};\n","path":"/charts/tick-ring.tsx","target":"uicapsule/editorial-charts/charts/tick-ring.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useEffect, useRef, useState } from \"react\";\n\nimport { DotWaffle } from \"./charts/dot-waffle\";\nimport { HairlineLine } from \"./charts/hairline-line\";\nimport { RungBars } from \"./charts/rung-bars\";\nimport { TickRing } from \"./charts/tick-ring\";\nimport { DAY_MAX, SCENES } from \"./lib/data\";\nimport { DARK, FAINT, INK, LADDER, MUTED, PAPER } from \"./lib/tokens\";\n\nconst CYCLE_MS = 4600;\n\nconst useCountUp = (target: number, ms = 950) => {\n  // Starts at 0 (matching the not-yet-inked charts around it) and always\n  // resumes from the last painted value, so neither SSR nor a Strict Mode\n  // remount can flash the final count or skip the entrance.\n  const [shown, setShown] = useState(0);\n  const painted = useRef(0);\n  useEffect(() => {\n    const from = painted.current;\n    if (from === target) {\n      return;\n    }\n    let raf = 0;\n    const t0 = performance.now();\n    const step = (t: number) => {\n      const p = Math.min(1, (t - t0) / ms);\n      const eased = 1 - (1 - p) ** 3;\n      const value = Math.round(from + (target - from) * eased);\n      painted.current = value;\n      setShown(value);\n      if (p < 1) {\n        raf = requestAnimationFrame(step);\n      }\n    };\n    raf = requestAnimationFrame(step);\n    return () => cancelAnimationFrame(raf);\n  }, [target, ms]);\n  return shown;\n};\n\n// Entrance clock: 0 → 1 over `ms`, driving how much of each chart is inked.\nconst useReveal = (active: boolean, ms: number) => {\n  const [progress, setProgress] = useState(0);\n  useEffect(() => {\n    if (!active) {\n      return;\n    }\n    let raf = 0;\n    const t0 = performance.now();\n    const step = (t: number) => {\n      const p = Math.min(1, (t - t0) / ms);\n      setProgress(p);\n      if (p < 1) {\n        raf = requestAnimationFrame(step);\n      }\n    };\n    raf = requestAnimationFrame(step);\n    return () => cancelAnimationFrame(raf);\n  }, [active, ms]);\n  return progress;\n};\n\nconst Kicker = ({ children, dark }: { children: string; dark?: boolean }) => (\n  <span\n    className=\"inline-block rounded-full border border-dashed px-2.5 py-0.5 text-[8.5px] font-semibold tracking-[0.14em]\"\n    style={{\n      borderColor: dark ? DARK.faint : FAINT,\n      color: dark ? DARK.muted : MUTED,\n    }}\n  >\n    {children}\n  </span>\n);\n\nconst CardHead = ({\n  kicker,\n  title,\n  sub,\n  dark,\n}: {\n  kicker: string;\n  title: string;\n  sub: string;\n  dark?: boolean;\n}) => (\n  <div>\n    <Kicker dark={dark}>{kicker}</Kicker>\n    <h2\n      className=\"mt-2 text-[16.5px] font-bold tracking-[-0.02em]\"\n      style={{ color: dark ? DARK.ink : INK }}\n    >\n      {title}\n    </h2>\n    <p className=\"mt-0.5 text-[11.5px]\" style={{ color: dark ? DARK.muted : MUTED }}>\n      {sub}\n    </p>\n  </div>\n);\n\nconst SourceRow = ({ children, dark }: { children: string; dark?: boolean }) => (\n  <p\n    className=\"mt-auto pt-3 text-[9px] font-medium tracking-[0.08em]\"\n    style={{ color: dark ? DARK.faint : FAINT }}\n  >\n    {children}\n  </p>\n);\n\nexport const EditorialCharts = () => {\n  const [scene, setScene] = useState(0);\n  // Charts mount after first paint so every visitor gets the full staggered\n  // entrance instead of a server-rendered final state.\n  const [inked, setInked] = useState(false);\n  useEffect(() => {\n    const frame = requestAnimationFrame(() => setInked(true));\n    const t = setInterval(() => setScene((s) => (s + 1) % SCENES.length), CYCLE_MS);\n    return () => {\n      cancelAnimationFrame(frame);\n      clearInterval(t);\n    };\n  }, []);\n\n  const reveal = useReveal(inked, 1500);\n  const s = SCENES[scene];\n  if (!s) {\n    throw new RangeError(\"Editorial chart scene index is out of bounds\");\n  }\n  const sessions = useCountUp(s.sessions);\n\n  return (\n    <div\n      className=\"w-[1080px] rounded-[28px] px-9 pt-7 pb-5 shadow-[0_40px_90px_-30px_rgba(0,0,0,0.55)] [font-family:Inter,ui-sans-serif,system-ui,sans-serif]\"\n      style={{ backgroundColor: PAPER, color: INK }}\n    >\n      {/* Masthead */}\n      <div className=\"flex items-baseline justify-between pb-4\">\n        <div className=\"flex items-baseline gap-3\">\n          <h1 className=\"text-[19px] font-bold tracking-[-0.02em]\">The quarter, drawn in ink</h1>\n          <span className=\"text-[11.5px]\" style={{ color: MUTED }}>\n            four marks, one grammar, one spring\n          </span>\n        </div>\n        <div className=\"flex gap-3 text-[11px] font-bold tracking-[0.06em]\">\n          {SCENES.map((q, i) => (\n            <span\n              key={q.quarter}\n              className=\"transition-colors duration-500\"\n              style={{\n                borderBottom: i === scene ? `2px solid ${INK}` : \"2px solid transparent\",\n                color: i === scene ? INK : FAINT,\n              }}\n            >\n              {q.quarter}\n            </span>\n          ))}\n        </div>\n      </div>\n\n      <div className=\"grid grid-cols-2 gap-x-9 gap-y-5\">\n        {/* Rung bars */}\n        <section className=\"flex min-h-[298px] flex-col\">\n          <CardHead\n            kicker=\"RUNG BARS · BILLING\"\n            title=\"Revenue by plan, rung by rung\"\n            sub=\"one rung = $2k of MRR · the bar is a ladder you can count\"\n          />\n          <div className=\"mt-3 h-[204px]\">\n            {inked && <RungBars plans={s.plans} reveal={reveal} height={182} initialWidth={480} />}\n          </div>\n          <SourceRow>TICK MARKS ON A POINT SCALE · STAGGERED SPRING ENTRANCE</SourceRow>\n        </section>\n\n        {/* Tick ring — the dark card */}\n        <section\n          className=\"flex min-h-[298px] flex-col rounded-[20px] px-6 pt-5 pb-4\"\n          style={{ backgroundColor: DARK.bg }}\n        >\n          <CardHead\n            dark\n            kicker=\"TICK RING · ACQUISITION\"\n            title=\"Where the sessions come from\"\n            sub=\"one tick = 1% · reads clockwise from noon · the dial twists each quarter\"\n          />\n          <div className=\"relative mx-auto mt-1 size-[198px]\">\n            {inked && (\n              <TickRing\n                channels={s.channels}\n                reveal={reveal}\n                size={198}\n                twist={scene * 14}\n                ladder={DARK.ladder}\n              />\n            )}\n            <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\">\n              <span\n                className=\"text-[30px] font-extrabold tracking-[-0.02em] tabular-nums\"\n                style={{ color: DARK.ink }}\n              >\n                {(sessions / 1000).toFixed(1)}k\n              </span>\n              <span\n                className=\"text-[8.5px] font-semibold tracking-[0.14em]\"\n                style={{ color: DARK.muted }}\n              >\n                SESSIONS\n              </span>\n            </div>\n            {s.channels.map((c, i) => {\n              // Clockwise from noon: channel order sweeps right side first.\n              const spots = [\n                \"left-full top-3 ml-3\",\n                \"left-full bottom-3 ml-3\",\n                \"right-full bottom-3 mr-3 text-right\",\n                \"right-full top-3 mr-3 text-right\",\n              ] as const;\n              return (\n                <div\n                  key={c.channel}\n                  className={`absolute w-24 text-[9px] font-semibold tracking-[0.1em] ${spots[i % 4]}`}\n                  style={{ color: DARK.ladder[i % DARK.ladder.length] }}\n                >\n                  {c.channel}\n                  <span className=\"block text-[13px] font-extrabold tabular-nums tracking-normal\">\n                    {c.share}%\n                  </span>\n                </div>\n              );\n            })}\n          </div>\n          <SourceRow dark>100 VECTOR MARKS · ONE SHADE PER CHANNEL · SPRUNG TWIST</SourceRow>\n        </section>\n\n        {/* Hairline line */}\n        <section className=\"flex min-h-[286px] flex-col\">\n          <CardHead\n            kicker=\"HAIRLINE LINE · GROWTH\"\n            title=\"Thirty days of sign-ups\"\n            sub=\"one dot = one day · hollow = weekend · the floor is a barcode of days\"\n          />\n          <div className=\"mt-3 h-[184px]\">\n            {inked && (\n              <HairlineLine\n                days={s.days}\n                reveal={reveal}\n                yMax={DAY_MAX}\n                height={158}\n                initialWidth={480}\n              />\n            )}\n          </div>\n          <SourceRow>LINE + DOT + TEXT MARKS · PATH MORPHS BETWEEN QUARTERS</SourceRow>\n        </section>\n\n        {/* Dot waffle */}\n        <section className=\"flex min-h-[286px] flex-col\">\n          <CardHead\n            kicker=\"DOT WAFFLE · PLAN MIX\"\n            title=\"What the new accounts pick\"\n            sub=\"one dot = 1% of new accounts · a moved percent pops out and back in\"\n          />\n          <div className=\"mt-2 flex items-center gap-8\">\n            <div className=\"size-[182px]\">\n              {inked && <DotWaffle mix={s.mix} reveal={reveal} size={182} ladder={LADDER} />}\n            </div>\n            <div className=\"flex flex-col gap-2.5\">\n              {s.mix.map((m, i) => (\n                <div key={m.plan} className=\"flex items-center gap-2.5\">\n                  <span\n                    className=\"size-2.5 rounded-full\"\n                    style={{ backgroundColor: LADDER[i % LADDER.length] }}\n                  />\n                  <span\n                    className=\"w-16 text-[9px] font-semibold tracking-[0.1em]\"\n                    style={{ color: MUTED }}\n                  >\n                    {m.plan}\n                  </span>\n                  <span className=\"text-[13px] font-extrabold tabular-nums\">{m.share}%</span>\n                </div>\n              ))}\n            </div>\n          </div>\n          <SourceRow>DOT MARKS + ORDINAL COLOR SCALE · KEYS CARRY THE PLAN</SourceRow>\n        </section>\n      </div>\n\n      <p\n        className=\"pt-4 text-center text-[9px] font-medium tracking-[0.08em]\"\n        style={{ color: FAINT }}\n      >\n        DEMO DATA · DRAWN BY TANSTACK CHARTS · SPRINGS AND STAGGER BY ITS MOTION RENDERER\n      </p>\n    </div>\n  );\n};\n","path":"/editorial-charts.tsx","target":"uicapsule/editorial-charts/editorial-charts.tsx","type":"registry:file"},{"content":"// Three quarters of a fictional product, cycled by the set. Deterministic on\n// purpose — a refresh must draw the exact same page.\n\nexport interface PlanRow {\n  plan: string;\n  value: number;\n}\nexport interface DayRow {\n  day: number;\n  value: number;\n  weekend: boolean;\n}\nexport interface ChannelRow {\n  channel: string;\n  share: number;\n}\nexport interface MixRow {\n  plan: string;\n  share: number;\n}\n\nexport interface Scene {\n  quarter: string;\n  /** $k MRR per plan. Keep every value divisible by 2: one rung = $2k, and the\n   * rung-bars ladder must count exactly to its label. */\n  plans: PlanRow[];\n  days: DayRow[];\n  channels: ChannelRow[];\n  sessions: number;\n  mix: MixRow[];\n}\n\n/* oxlint-disable no-bitwise -- integer hash: xorshift-multiply needs unsigned 32-bit wraparound */\nconst hash = (n: number) => {\n  let x = (n + 0x9e_37_79_b9) >>> 0;\n  x = Math.imul(x ^ (x >>> 16), 0x4_5d_9f_3b) >>> 0;\n  x = Math.imul(x ^ (x >>> 16), 0x4_5d_9f_3b) >>> 0;\n  return ((x ^ (x >>> 16)) >>> 0) / 0xff_ff_ff_ff;\n};\n/* oxlint-enable no-bitwise */\n\nconst days = (seed: number, base: number, slope: number): DayRow[] =>\n  Array.from({ length: 30 }, (_, i) => {\n    const weekend = i % 7 === 5 || i % 7 === 6;\n    const wiggle = (hash(seed * 1000 + i) - 0.5) * 16;\n    const value = Math.max(6, Math.round(base + i * slope + wiggle - (weekend ? 11 : 0)));\n    return { day: i + 1, value, weekend };\n  });\n\nexport const SCENES: Scene[] = [\n  {\n    channels: [\n      { channel: \"ORGANIC\", share: 37 },\n      { channel: \"REFERRAL\", share: 21 },\n      { channel: \"SOCIAL\", share: 14 },\n      { channel: \"PAID\", share: 28 },\n    ],\n    days: days(1, 34, 0.9),\n    mix: [\n      { plan: \"FREE\", share: 46 },\n      { plan: \"STARTER\", share: 27 },\n      { plan: \"PRO\", share: 17 },\n      { plan: \"TEAM\", share: 10 },\n    ],\n    plans: [\n      { plan: \"FREE\", value: 38 },\n      { plan: \"STARTER\", value: 28 },\n      { plan: \"PRO\", value: 22 },\n      { plan: \"TEAM\", value: 16 },\n      { plan: \"SCALE\", value: 10 },\n    ],\n    quarter: \"Q1\",\n    sessions: 41_800,\n  },\n  {\n    channels: [\n      { channel: \"ORGANIC\", share: 41 },\n      { channel: \"REFERRAL\", share: 23 },\n      { channel: \"SOCIAL\", share: 12 },\n      { channel: \"PAID\", share: 24 },\n    ],\n    days: days(2, 42, 1.15),\n    mix: [\n      { plan: \"FREE\", share: 41 },\n      { plan: \"STARTER\", share: 29 },\n      { plan: \"PRO\", share: 19 },\n      { plan: \"TEAM\", share: 11 },\n    ],\n    plans: [\n      { plan: \"FREE\", value: 44 },\n      { plan: \"STARTER\", value: 32 },\n      { plan: \"PRO\", value: 26 },\n      { plan: \"TEAM\", value: 20 },\n      { plan: \"SCALE\", value: 14 },\n    ],\n    quarter: \"Q2\",\n    sessions: 48_200,\n  },\n  {\n    channels: [\n      { channel: \"ORGANIC\", share: 46 },\n      { channel: \"REFERRAL\", share: 24 },\n      { channel: \"SOCIAL\", share: 11 },\n      { channel: \"PAID\", share: 19 },\n    ],\n    days: days(3, 52, 1.4),\n    mix: [\n      { plan: \"FREE\", share: 34 },\n      { plan: \"STARTER\", share: 31 },\n      { plan: \"PRO\", share: 22 },\n      { plan: \"TEAM\", share: 13 },\n    ],\n    plans: [\n      { plan: \"FREE\", value: 52 },\n      { plan: \"STARTER\", value: 36 },\n      { plan: \"PRO\", value: 30 },\n      { plan: \"TEAM\", value: 24 },\n      { plan: \"SCALE\", value: 18 },\n    ],\n    quarter: \"Q3\",\n    sessions: 56_400,\n  },\n];\n\n// Shared y ceiling so the line visibly climbs across quarters.\nexport const DAY_MAX = Math.max(...SCENES.flatMap((s) => s.days.map((d) => d.value)));\n","path":"/lib/data.ts","target":"uicapsule/editorial-charts/lib/data.ts","type":"registry:file"},{"content":"// Editorial mono tokens — paper ground, charcoal ink, lightness IS the data.\n// Inspired by print-editorial data graphics; values are our own.\n\nexport const PAPER = \"#f1f0ec\";\nexport const INK = \"#1b1b19\";\nexport const MUTED = \"#8b8a84\";\nexport const FAINT = \"#c2c1bb\";\nexport const HAIR = \"#dbdad3\";\n\n// Ink ladder for multi-series work: most important = darkest.\nexport const LADDER = [\"#1b1b19\", \"#4a4943\", \"#8b8a84\", \"#aeada7\", \"#d0cfc9\"] as const;\n\n// Dark card: ink becomes paper, ladder runs bright → dim.\nexport const DARK = {\n  bg: \"#1b1b19\",\n  faint: \"#4c4b45\",\n  ink: \"#f1f0ec\",\n  ladder: [\"#f1f0ec\", \"#c7c6be\", \"#93928b\", \"#5c5b54\"] as const,\n  muted: \"#8b8a84\",\n};\n\n// One spring for the whole set — quick in, no bounce to speak of.\nexport const SPRING = {\n  damping: 22,\n  mass: 1,\n  stiffness: 170,\n  type: \"spring\",\n} as const;\n","path":"/lib/tokens.ts","target":"uicapsule/editorial-charts/lib/tokens.ts","type":"registry:file"},{"content":"\"use client\";\n\nimport { EditorialCharts } from \"./editorial-charts\";\n\nconst Preview = () => (\n  <main className=\"flex h-dvh items-center justify-center overflow-hidden bg-[radial-gradient(circle_at_50%_25%,#26272b_0%,#141518_50%,#08090b_100%)] px-5\">\n    <div className=\"origin-center scale-[0.94]\">\n      <EditorialCharts />\n    </div>\n  </main>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/editorial-charts/preview.tsx","type":"registry:file"}],"homepage":"https://www.uicapsule.com/ui/editorial-charts","name":"editorial-charts","registryDependencies":[],"title":"Editorial Charts","type":"registry:block"}