{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "funnel-chart",
  "type": "registry:ui",
  "title": "Funnel chart",
  "description": "A funnel chart with stage bars laid out horizontally or vertically, conversion rates and drop-off between stages, an optional comparison series and bars that spring to new values when the data changes.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://ui.minidev.pro/r/segmented-control.json",
    "utils"
  ],
  "files": [
    {
      "path": "registry/ui/funnel-chart.tsx",
      "type": "registry:ui",
      "target": "components/ui/funnel-chart.tsx",
      "content": "\"use client\"\nimport * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/lib/utils\"\nimport { SegmentedControl } from \"@/components/ui/segmented-control\"\n\n/** A funnel chart with stage bars laid out horizontally or vertically, conversion rates and drop-off between stages, an optional comparison series and bars that spring to new values when the data changes. */\ntype FunnelStage = {\n  label: string\n  value: number\n  /** Value from the comparison period, drawn as a dashed outline. */\n  compare?: number\n}\n\ntype FunnelDataset = { label: string; stages: FunnelStage[] }\n\ntype FunnelChartProps = {\n  /** Stages from the top of the funnel down. */\n  stages?: FunnelStage[]\n  /** Datasets to switch between with a segmented control. Ignored when `stages` is set. */\n  datasets?: FunnelDataset[]\n  /** \"auto\" is horizontal when the container is at least 480px wide, vertical below that. */\n  orientation?: \"horizontal\" | \"vertical\" | \"auto\"\n  title?: string\n  /** Legend text for the comparison series. */\n  compareLabel?: string\n  /** Formats values in the bars and the table. */\n  format?: (value: number) => string\n  /** Draw the comparison outline when stages carry `compare`. */\n  showCompare?: boolean\n  className?: string\n}\n\nconst DATASETS: FunnelDataset[] = [\n  {\n    label: \"All\",\n    stages: [\n      { label: \"Visitors\", value: 48200, compare: 44100 },\n      { label: \"Signed up\", value: 12840, compare: 11200 },\n      { label: \"Activated\", value: 6120, compare: 5480 },\n      { label: \"Trial started\", value: 3210, compare: 2900 },\n      { label: \"Paid\", value: 1464, compare: 1210 },\n    ],\n  },\n  {\n    label: \"Web\",\n    stages: [\n      { label: \"Visitors\", value: 31400, compare: 29800 },\n      { label: \"Signed up\", value: 9120, compare: 8300 },\n      { label: \"Activated\", value: 4680, compare: 4120 },\n      { label: \"Trial started\", value: 2510, compare: 2260 },\n      { label: \"Paid\", value: 1182, compare: 980 },\n    ],\n  },\n  {\n    label: \"Mobile\",\n    stages: [\n      { label: \"Visitors\", value: 16800, compare: 14300 },\n      { label: \"Signed up\", value: 3720, compare: 2900 },\n      { label: \"Activated\", value: 1440, compare: 1360 },\n      { label: \"Trial started\", value: 700, compare: 640 },\n      { label: \"Paid\", value: 282, compare: 230 },\n    ],\n  },\n]\n\nconst DATA_SPRING = { type: \"spring\", bounce: 0.12, duration: 0.7 } as const\nconst compact = new Intl.NumberFormat(\"en-US\", { notation: \"compact\", maximumFractionDigits: 1 })\nconst defaultFormat = (n: number) => compact.format(n)\nconst pct = (r: number) => `${(r * 100).toFixed(r < 0.1 ? 1 : 0)}%`\n\n/**\n * A conversion funnel. Each stage is a bar sized against the first stage,\n * with the step conversion and the drop-off written between stages and an\n * overall rate in the header. Switch datasets and every bar springs to its\n * new size. Lays out as columns on wide containers and as rows on narrow ones.\n */\nfunction FunnelChart({\n  stages: stagesProp,\n  datasets = DATASETS,\n  orientation = \"auto\",\n  title = \"Visitors to paid\",\n  compareLabel = \"Last month\",\n  format = defaultFormat,\n  showCompare = true,\n  className,\n}: FunnelChartProps) {\n  const reduce = useReducedMotion()\n  const boxRef = React.useRef<HTMLDivElement>(null)\n  const [narrow, setNarrow] = React.useState(false)\n  const [set, setSet] = React.useState(datasets[0]?.label ?? \"\")\n  const titleId = React.useId()\n\n  React.useEffect(() => {\n    const el = boxRef.current\n    if (!el || typeof ResizeObserver === \"undefined\") return\n    const ro = new ResizeObserver(([e]) => setNarrow(e.contentRect.width < 480))\n    ro.observe(el)\n    return () => ro.disconnect()\n  }, [])\n\n  const stages = stagesProp ?? datasets.find((d) => d.label === set)?.stages ?? datasets[0]?.stages ?? []\n  const vertical = orientation === \"vertical\" || (orientation === \"auto\" && narrow)\n  const max = Math.max(1, ...stages.map((s) => Math.max(s.value, showCompare ? (s.compare ?? 0) : 0)))\n  const hasCompare = showCompare && stages.some((s) => s.compare != null)\n  const first = stages[0]?.value ?? 0\n  const last = stages[stages.length - 1]?.value ?? 0\n  const overall = first > 0 ? last / first : 0\n  const compareOverall = hasCompare && stages[0]?.compare ? (stages[stages.length - 1]?.compare ?? 0) / stages[0].compare! : null\n  const spring = reduce ? { duration: 0 } : DATA_SPRING\n\n  const rows = stages.map((s, i) => {\n    const prev = i > 0 ? stages[i - 1].value : null\n    const rate = prev ? s.value / prev : null\n    const drop = prev != null ? prev - s.value : null\n    return { ...s, i, rate, drop, size: s.value / max, cmp: s.compare != null ? s.compare / max : null }\n  })\n\n  return (\n    <figure\n      data-slot=\"funnel-chart\"\n      aria-labelledby={titleId}\n      className={cn(\"w-full max-w-2xl rounded-xl border border-border bg-surface p-5 shadow-raised\", className)}\n    >\n      <figcaption className=\"flex flex-wrap items-start justify-between gap-x-4 gap-y-3\">\n        <div>\n          <p id={titleId} className=\"text-[0.8125rem] font-medium text-fg\">\n            {title}\n          </p>\n          <p className=\"mt-1 flex items-baseline gap-2\">\n            <span className=\"text-2xl font-medium tracking-[-0.025em] text-fg tabular-nums\">{pct(overall)}</span>\n            <span className=\"text-xs text-fg-muted\">\n              overall\n              {compareOverall != null ? <span className=\"tabular-nums\"> · {pct(compareOverall)} {compareLabel.toLowerCase()}</span> : null}\n            </span>\n          </p>\n        </div>\n        {!stagesProp && datasets.length > 1 ? (\n          <SegmentedControl size=\"sm\" aria-label=\"Dataset\" value={set} onChange={setSet} items={datasets.map((d) => d.label)} />\n        ) : null}\n      </figcaption>\n\n      <div ref={boxRef} className=\"mt-5\" aria-hidden>\n        {vertical ? (\n          <ol className=\"space-y-1\">\n            {rows.map((r) => (\n              <li key={r.label} className=\"space-y-1\">\n                {r.rate != null ? (\n                  <div className=\"grid grid-cols-[88px_1fr_56px] items-center gap-3\">\n                    <span />\n                    <p className=\"truncate text-center text-[11px] text-fg-subtle tabular-nums\">\n                      <span className=\"rounded-md bg-sunken px-1.5 py-0.5 font-medium text-fg\">{pct(r.rate)}</span> −{format(r.drop ?? 0)}\n                    </p>\n                    <span />\n                  </div>\n                ) : null}\n                <div className=\"grid grid-cols-[88px_1fr_56px] items-center gap-3\">\n                  <span className=\"truncate text-xs text-fg-muted\">{r.label}</span>\n                  <div className=\"relative flex h-7 items-center justify-center rounded-md bg-sunken\">\n                    <motion.span\n                      className=\"h-full rounded-md bg-accent\"\n                      initial={false}\n                      animate={{ width: `${Math.max(1, r.size * 100)}%` }}\n                      transition={spring}\n                    />\n                    {hasCompare && r.cmp != null ? (\n                      <motion.span\n                        className=\"pointer-events-none absolute inset-y-0 rounded-md border border-dashed border-fg/45\"\n                        initial={false}\n                        animate={{ width: `${Math.max(1, r.cmp * 100)}%` }}\n                        transition={spring}\n                        style={{ left: \"50%\", x: \"-50%\" }}\n                      />\n                    ) : null}\n                  </div>\n                  <span className=\"text-right text-xs font-medium text-fg tabular-nums\">{format(r.value)}</span>\n                </div>\n              </li>\n            ))}\n          </ol>\n        ) : (\n          <div className=\"flex items-stretch\">\n            {rows.map((r) => (\n              <React.Fragment key={r.label}>\n                {r.rate != null ? (\n                  <div className=\"flex w-16 shrink-0 flex-col items-center justify-center gap-0.5 pb-9\">\n                    <span className=\"rounded-md bg-sunken px-1.5 py-0.5 text-[11px] font-medium text-fg tabular-nums\">{pct(r.rate)}</span>\n                    <span className=\"text-[11px] text-fg-subtle tabular-nums\">−{format(r.drop ?? 0)}</span>\n                  </div>\n                ) : null}\n                <div className=\"flex min-w-0 flex-1 flex-col\">\n                  <p className=\"truncate text-lg font-medium tracking-[-0.02em] text-fg tabular-nums\">{format(r.value)}</p>\n                  {hasCompare ? (\n                    <p className=\"truncate text-[11px] text-fg-subtle tabular-nums\">{r.compare != null ? `vs ${format(r.compare)}` : \" \"}</p>\n                  ) : null}\n                  <div className=\"relative mt-2 h-40 rounded-md bg-sunken\">\n                    <motion.span\n                      className=\"absolute inset-x-0 bottom-0 rounded-md bg-accent\"\n                      initial={false}\n                      animate={{ height: `${Math.max(1, r.size * 100)}%` }}\n                      transition={spring}\n                    />\n                    {hasCompare && r.cmp != null ? (\n                      <motion.span\n                        className=\"pointer-events-none absolute inset-x-0 bottom-0 rounded-md border border-dashed border-fg/45\"\n                        initial={false}\n                        animate={{ height: `${Math.max(1, r.cmp * 100)}%` }}\n                        transition={spring}\n                      />\n                    ) : null}\n                  </div>\n                  <p className=\"mt-2 line-clamp-2 min-h-[2.6em] pr-1 text-xs leading-[1.3] text-fg-muted\">{r.label}</p>\n                </div>\n              </React.Fragment>\n            ))}\n          </div>\n        )}\n        {hasCompare ? (\n          <div className=\"mt-4 flex items-center gap-4 border-t border-border pt-3 text-[11px] text-fg-muted\">\n            <span className=\"inline-flex items-center gap-1.5\">\n              <span className=\"h-2 w-3 rounded-sm bg-accent\" /> {set || \"This month\"}\n            </span>\n            <span className=\"inline-flex items-center gap-1.5\">\n              <span className=\"h-2 w-3 rounded-sm border border-dashed border-fg/45\" /> {compareLabel}\n            </span>\n          </div>\n        ) : null}\n      </div>\n\n      <table className=\"sr-only\">\n        <caption>{title}</caption>\n        <thead>\n          <tr>\n            <th scope=\"col\">Stage</th>\n            <th scope=\"col\">Value</th>\n            <th scope=\"col\">Step conversion</th>\n            <th scope=\"col\">Drop-off</th>\n            {hasCompare ? <th scope=\"col\">{compareLabel}</th> : null}\n          </tr>\n        </thead>\n        <tbody>\n          {rows.map((r) => (\n            <tr key={r.label}>\n              <th scope=\"row\">{r.label}</th>\n              <td>{r.value.toLocaleString(\"en-US\")}</td>\n              <td>{r.rate != null ? pct(r.rate) : \"Top of funnel\"}</td>\n              <td>{r.drop != null ? r.drop.toLocaleString(\"en-US\") : \"0\"}</td>\n              {hasCompare ? <td>{r.compare != null ? r.compare.toLocaleString(\"en-US\") : \"\"}</td> : null}\n            </tr>\n          ))}\n        </tbody>\n        <tfoot>\n          <tr>\n            <th scope=\"row\">Overall conversion</th>\n            <td colSpan={hasCompare ? 4 : 3}>{pct(overall)}</td>\n          </tr>\n        </tfoot>\n      </table>\n    </figure>\n  )\n}\n\nexport { FunnelChart }\nexport type { FunnelChartProps, FunnelStage, FunnelDataset }\n"
    }
  ]
}
