{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "growth-chart",
  "type": "registry:component",
  "title": "Growth Chart",
  "description": "A record-month moment: a little ball hops up a staircase of bars, each one springing up under it as the total ticks over, and plants a flag on the peak.",
  "categories": [
    "illustration",
    "Celebrations"
  ],
  "dependencies": [
    "lucide-react",
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/growth-chart.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { TrendingUpIcon } from \"lucide-react\";\nimport { animate, motion, useInView, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type GrowthChartHue = \"sky\" | \"emerald\" | \"amber\" | \"violet\" | \"rose\" | \"orange\" | \"indigo\" | \"teal\";\n\nconst hues: Record<GrowthChartHue, string> = {\n  sky: \"[--hue-hi:var(--color-sky-400)] [--hue:var(--color-sky-500)] [--hue-deep:var(--color-sky-600)]\",\n  emerald: \"[--hue-hi:var(--color-emerald-400)] [--hue:var(--color-emerald-500)] [--hue-deep:var(--color-emerald-600)]\",\n  amber: \"[--hue-hi:var(--color-amber-400)] [--hue:var(--color-amber-500)] [--hue-deep:var(--color-amber-600)]\",\n  violet: \"[--hue-hi:var(--color-violet-400)] [--hue:var(--color-violet-500)] [--hue-deep:var(--color-violet-600)]\",\n  rose: \"[--hue-hi:var(--color-rose-400)] [--hue:var(--color-rose-500)] [--hue-deep:var(--color-rose-600)]\",\n  orange: \"[--hue-hi:var(--color-orange-400)] [--hue:var(--color-orange-500)] [--hue-deep:var(--color-orange-600)]\",\n  indigo: \"[--hue-hi:var(--color-indigo-400)] [--hue:var(--color-indigo-500)] [--hue-deep:var(--color-indigo-600)]\",\n  teal: \"[--hue-hi:var(--color-teal-400)] [--hue:var(--color-teal-500)] [--hue-deep:var(--color-teal-600)]\",\n};\n\nexport interface GrowthChartPoint {\n  label: string;\n  value: number;\n}\n\nexport interface GrowthChartProps {\n  data?: GrowthChartPoint[];\n  title?: string;\n  format?: (value: number) => string;\n  hue?: GrowthChartHue;\n  loop?: boolean;\n  className?: string;\n}\n\nconst defaultData: GrowthChartPoint[] = [\n  { label: \"Jan\", value: 18400 },\n  { label: \"Feb\", value: 22900 },\n  { label: \"Mar\", value: 21300 },\n  { label: \"Apr\", value: 29800 },\n  { label: \"May\", value: 34900 },\n  { label: \"Jun\", value: 48200 },\n];\n\nconst W = 400;\nconst H = 210;\nconst BASE = 180;\nconst TOP = 46;\nconst LEFT = 28;\nconst RIGHT = 372;\n\nconst grow = { type: \"spring\", stiffness: 220, damping: 20 } as const;\nconst R = 7;\n\nfunction useCalmMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction flagPath(x: number, y: number, phase: 0 | 1) {\n  const top = y - 40;\n  const k = phase ? 1 : -1;\n  return `M${x} ${top} C ${x + 10} ${top - 3 * k} ${x + 18} ${top + 3 * k} ${x + 28} ${top + 1} L ${x + 26} ${top + 11} L ${x + 28} ${top + 21} C ${x + 18} ${top + 23 + 3 * k} ${x + 10} ${top + 17 - 3 * k} ${x} ${top + 20} Z`;\n}\n\nexport function GrowthChart({\n  data = defaultData,\n  title = \"Record month\",\n  format = (v) => `$${Math.round(v).toLocaleString(\"en-US\")}`,\n  hue = \"emerald\",\n  loop = true,\n  className,\n}: GrowthChartProps) {\n  const uid = useId().replace(/:/g, \"\");\n  const reduce = useCalmMotion();\n  const root = useRef<HTMLDivElement>(null);\n  const inView = useInView(root);\n  const [grown, setGrown] = useState(0);\n  const [planted, setPlanted] = useState(false);\n\n  const max = Math.max(...data.map((d) => d.value));\n  const step = (RIGHT - LEFT) / data.length;\n  const barW = Math.min(34, step * 0.56);\n  const pts = data.map((d, i) => ({\n    x: LEFT + step * i + step / 2,\n    y: BASE - (d.value / max) * (BASE - TOP),\n  }));\n  const last = pts[pts.length - 1];\n  const prev = data[data.length - 2]?.value ?? data[0].value;\n  const change = Math.round(((data[data.length - 1].value - prev) / prev) * 100);\n\n  const dataKey = data.map((d) => `${d.label}:${d.value}`).join(\"|\");\n  const figure = useMotionValue(0);\n  const figureText = useTransform(figure, (v) => format(v));\n  const bx = useMotionValue(pts[0].x);\n  const by = useMotionValue(TOP - 24);\n  const ballOpacity = useMotionValue(0);\n  const squash = useMotionValue(1);\n  const stretch = useTransform(squash, (v) => 2 - v);\n\n  useEffect(() => {\n    const finish = () => {\n      setGrown(data.length);\n      setPlanted(true);\n      figure.set(data[data.length - 1].value);\n      bx.set(last.x);\n      by.set(last.y - R);\n      ballOpacity.set(1);\n    };\n    if (reduce) {\n      finish();\n      return;\n    }\n    if (!inView) return;\n    let alive = true;\n    const timers: ReturnType<typeof setTimeout>[] = [];\n    const stops: { stop: () => void }[] = [];\n    const wait = (ms: number) => new Promise<void>((r) => timers.push(setTimeout(r, ms)));\n    const run = <T extends { stop: () => void }>(c: T) => {\n      stops.push(c);\n      return c;\n    };\n    const hop = (x0: number, y0: number, x1: number, y1: number, duration: number) => {\n      const c = 2 * (Math.min(y0, y1) - 26) - (y0 + y1) / 2;\n      return run(\n        animate(0, 1, {\n          duration,\n          ease: \"linear\",\n          onUpdate: (t) => {\n            bx.set(x0 + (x1 - x0) * t);\n            by.set((1 - t) ** 2 * y0 + 2 * (1 - t) * t * c + t ** 2 * y1);\n          },\n        }),\n      );\n    };\n\n    (async () => {\n      while (alive) {\n        setGrown(0);\n        setPlanted(false);\n        figure.set(0);\n        bx.set(pts[0].x);\n        by.set(TOP - 30);\n        squash.set(1);\n        run(animate(ballOpacity, 1, { duration: 0.25 }));\n        await wait(450);\n        if (!alive) return;\n        for (let i = 0; i < pts.length; i++) {\n          const from = i === 0 ? { x: pts[0].x, y: TOP - 30 } : { x: pts[i - 1].x, y: pts[i - 1].y - R };\n          setGrown(i + 1);\n          await hop(from.x, from.y, pts[i].x, pts[i].y - R, i === 0 ? 0.5 : 0.42);\n          if (!alive) return;\n          run(animate(squash, [1, 0.72, 1], { duration: 0.24, ease: [0.23, 1, 0.32, 1] }));\n          run(animate(figure, data[i].value, { duration: 0.35, ease: [0.23, 1, 0.32, 1] }));\n          await wait(i === pts.length - 1 ? 120 : 60);\n          if (!alive) return;\n        }\n        setPlanted(true);\n        if (!loop) return;\n        await wait(4400);\n        if (!alive) return;\n        setPlanted(false);\n        setGrown(0);\n        run(animate(ballOpacity, 0, { duration: 0.25 }));\n        await wait(700);\n      }\n    })();\n\n    return () => {\n      alive = false;\n      for (const t of timers) clearTimeout(t);\n      for (const x of stops) x.stop();\n    };\n  }, [reduce, inView, loop, dataKey]);\n\n  return (\n    <div\n      ref={root}\n      className={cn(\n        \"relative w-full max-w-[440px] overflow-hidden rounded-2xl border bg-card p-4 shadow-xs/5 sm:p-6\",\n        hues[hue],\n        className,\n      )}\n    >\n      <div\n        aria-hidden\n        className=\"dot-grid pointer-events-none absolute inset-0 opacity-40 [mask-image:radial-gradient(closest-side,black,transparent)]\"\n      />\n      <div className=\"relative flex items-start justify-between gap-3\">\n        <div>\n          <p className=\"text-muted-foreground text-xs\">{title}</p>\n          <motion.p className=\"mt-0.5 font-semibold text-2xl text-foreground tabular-nums tracking-tight\">{figureText}</motion.p>\n        </div>\n        <motion.span\n          animate={{ opacity: planted ? 1 : 0, y: planted ? 0 : 4, filter: planted ? \"blur(0px)\" : \"blur(2px)\" }}\n          className=\"mt-1 inline-flex items-center gap-1 rounded-full bg-success/12 px-2 py-0.5 font-medium text-success-foreground text-xs tabular-nums\"\n          initial={false}\n          transition={{ duration: reduce ? 0 : 0.25, delay: planted && !reduce ? 0.3 : 0 }}\n        >\n          <TrendingUpIcon aria-hidden className=\"size-3.5\" />+{change}%\n        </motion.span>\n      </div>\n\n      <div className=\"relative mx-auto mt-2 aspect-[40/21] w-full\">\n        <svg\n          aria-label={`${title}: ${data.map((d) => `${d.label} ${format(d.value)}`).join(\", \")}`}\n          className=\"absolute inset-0 size-full overflow-visible\"\n          role=\"img\"\n          viewBox={`0 0 ${W} ${H}`}\n        >\n          <defs>\n            <linearGradient id={`${uid}-bar`} x1=\"0\" x2=\"0\" y1=\"0\" y2=\"1\">\n              <stop offset=\"0\" stopColor=\"var(--hue-hi)\" />\n              <stop offset=\"1\" stopColor=\"var(--hue-deep)\" />\n            </linearGradient>\n          </defs>\n\n          {[0.25, 0.5, 0.75].map((f) => (\n            <line\n              className=\"stroke-foreground/8\"\n              key={f}\n              strokeDasharray=\"3 5\"\n              x1={LEFT}\n              x2={RIGHT}\n              y1={BASE - f * (BASE - TOP)}\n              y2={BASE - f * (BASE - TOP)}\n            />\n          ))}\n          <line className=\"stroke-foreground/15\" strokeLinecap=\"round\" strokeWidth={1.5} x1={LEFT} x2={RIGHT} y1={BASE} y2={BASE} />\n\n          {pts.map((p, i) => {\n            const on = i < grown;\n            return (\n              <motion.rect\n                animate={{ scaleY: on ? 1 : 0.06 }}\n                fill={`url(#${uid}-bar)`}\n                height={BASE - p.y}\n                initial={false}\n                key={data[i].label}\n                opacity={i === pts.length - 1 ? 1 : 0.28 + (i / (pts.length - 1)) * 0.42}\n                rx={6}\n                style={{ originY: 1 }}\n                transition={reduce ? { duration: 0 } : on ? grow : { duration: 0.35, ease: [0.65, 0, 0.35, 1] }}\n                width={barW}\n                x={p.x - barW / 2}\n                y={p.y}\n              />\n            );\n          })}\n\n          <motion.line\n            animate={{ pathLength: planted ? 1 : 0, opacity: planted ? 1 : 0 }}\n            className=\"stroke-foreground/70\"\n            initial={false}\n            strokeLinecap=\"round\"\n            strokeWidth={2}\n            transition={reduce ? { duration: 0 } : { duration: 0.25, ease: [0.23, 1, 0.32, 1] }}\n            x1={last.x}\n            x2={last.x}\n            y1={last.y - 2 * R}\n            y2={last.y - 40}\n          />\n          <motion.g\n            animate={{ scaleX: planted ? 1 : 0.2, opacity: planted ? 1 : 0 }}\n            initial={false}\n            style={{ originX: 0 }}\n            transition={reduce ? { duration: 0 } : planted ? { delay: 0.18, type: \"spring\", stiffness: 380, damping: 20 } : { duration: 0.2 }}\n          >\n            <motion.path\n              animate={reduce || !planted || !inView ? undefined : { d: [flagPath(last.x, last.y, 0), flagPath(last.x, last.y, 1), flagPath(last.x, last.y, 0)] }}\n              className=\"fill-(--hue)\"\n              d={flagPath(last.x, last.y, 0)}\n              transition={{ delay: 0.5, duration: 2.2, ease: \"easeInOut\", repeat: Number.POSITIVE_INFINITY }}\n            />\n          </motion.g>\n          {!reduce &&\n            [0, 1, 2, 3, 4, 5].map((n) => {\n              const a = (-150 + n * 24) * (Math.PI / 180);\n              return (\n                <motion.circle\n                  animate={planted ? { opacity: [0, 1, 0], cx: [last.x, last.x + Math.cos(a) * 26], cy: [last.y - 38, last.y - 38 + Math.sin(a) * 22] } : { opacity: 0 }}\n                  className={n % 2 ? \"fill-amber-400\" : \"fill-(--hue)\"}\n                  cx={last.x}\n                  cy={last.y - 38}\n                  initial={false}\n                  key={n}\n                  r={2.5}\n                  transition={planted ? { delay: 0.2, duration: 0.6, ease: \"easeOut\" } : { duration: 0.1 }}\n                />\n              );\n            })}\n\n          <motion.g style={{ opacity: ballOpacity, x: bx, y: by }}>\n            <motion.circle\n              className=\"fill-foreground\"\n              cx={0}\n              cy={0}\n              r={R}\n              style={{ originY: `${R}px`, scaleX: stretch, scaleY: squash }}\n            />\n            <circle className=\"fill-background/40\" cx={-2.2} cy={-2.4} r={2} />\n          </motion.g>\n\n          {pts.map((p, i) => (\n            <text\n              className={cn(\"fill-muted-foreground\", i === pts.length - 1 && \"fill-foreground font-medium\")}\n              fontSize={11}\n              key={data[i].label}\n              textAnchor=\"middle\"\n              x={p.x}\n              y={BASE + 18}\n            >\n              {data[i].label}\n            </text>\n          ))}\n        </svg>\n      </div>\n    </div>\n  );\n}\n"
    }
  ]
}