{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "analytics-1",
  "title": "Analytics 1: product analytics",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A product analytics page that reads like a report: four metrics with their change and trend, the visit-to-purchase funnel with the biggest drop flagged, a tabbed ranking of pages, sources and countries, and a device split.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/skeleton.json",
    "https://ui.ballmac.com/r/state-view.json",
    "https://ui.ballmac.com/r/sparkline.json",
    "https://ui.ballmac.com/r/bar-list.json",
    "https://ui.ballmac.com/r/funnel-chart.json",
    "https://ui.ballmac.com/r/split-bar.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/analytics-1/analytics-1.tsx",
      "content": "// Ballmac UI: Analytics 1. https://ui.ballmac.com/blocks/analytics-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownRight, ArrowUpRight, Minus } from \"lucide-react\"\n\nimport { BarList, type BarListItem } from \"@/components/ballmac/bar-list\"\nimport { FunnelChart, type FunnelStep } from \"@/components/ballmac/funnel-chart\"\nimport { Skeleton } from \"@/components/ballmac/skeleton\"\nimport { Sparkline } from \"@/components/ballmac/sparkline\"\nimport { SplitBar, type SplitSegment } from \"@/components/ballmac/split-bar\"\nimport { frameProps, StateView, type StateProps } from \"@/components/ballmac/state-view\"\nimport { cn } from \"@/lib/utils\"\n\ntype Analytics1Metric = { id: string; label: string; value: string; change: number; series: number[] }\ntype Analytics1Period = {\n  id: string\n  label: string\n  metrics: Analytics1Metric[]\n  funnel: FunnelStep[]\n  lists: { id: string; label: string; items: BarListItem[] }[]\n  devices: SplitSegment[]\n}\n\ntype Analytics1Props = Omit<React.ComponentProps<\"section\">, \"title\"> &\n  StateProps & {\n    title?: string\n    description?: string\n    periods?: Analytics1Period[]\n    /** The period shown first (an id). */\n    defaultPeriod?: string\n    onPeriodChange?: (id: string) => void\n  }\n\nconst sparkline = (n: number, trend: number, wobble = 0.18) => Array.from({ length: 14 }, (_, i) => Math.round(n * (1 + trend * (i / 13) + Math.sin(i * 1.7 + n) * wobble * 0.3)))\n\nconst samplePeriods: Analytics1Period[] = [\n  {\n    id: \"7d\",\n    label: \"7 days\",\n    metrics: [\n      { id: \"v\", label: \"Visitors\", value: \"48,210\", change: 8.2, series: sparkline(6800, 0.12) },\n      { id: \"s\", label: \"Signups\", value: \"1,930\", change: 14.6, series: sparkline(270, 0.2) },\n      { id: \"c\", label: \"Conversion\", value: \"4.0%\", change: 0.3, series: sparkline(40, 0.04) },\n      { id: \"r\", label: \"Revenue\", value: \"$18,420\", change: -2.1, series: sparkline(2600, -0.04) },\n    ],\n    funnel: [\n      { id: \"visit\", label: \"Visited the site\", value: 48210, hint: \"Unique visitors\" },\n      { id: \"pricing\", label: \"Viewed pricing\", value: 19800 },\n      { id: \"start\", label: \"Started checkout\", value: 4120 },\n      { id: \"pay\", label: \"Paid\", value: 1385 },\n    ],\n    lists: [\n      { id: \"pages\", label: \"Pages\", items: [{ id: \"p1\", label: \"/pricing\", value: 12840 }, { id: \"p2\", label: \"/docs/getting-started\", value: 9310 }, { id: \"p3\", label: \"/blog/launch-week\", value: 7420 }, { id: \"p4\", label: \"/components\", value: 5180 }, { id: \"p5\", label: \"/changelog\", value: 3960 }, { id: \"p6\", label: \"/about\", value: 1210 }] },\n      { id: \"sources\", label: \"Sources\", items: [{ id: \"s1\", label: \"Google\", value: 18200 }, { id: \"s2\", label: \"Direct\", value: 11400 }, { id: \"s3\", label: \"Hacker News\", value: 8300 }, { id: \"s4\", label: \"X\", value: 5100 }, { id: \"s5\", label: \"Newsletter\", value: 2900 }] },\n      { id: \"countries\", label: \"Countries\", items: [{ id: \"c1\", label: \"United States\", value: 16400 }, { id: \"c2\", label: \"Germany\", value: 6100 }, { id: \"c3\", label: \"United Kingdom\", value: 5900 }, { id: \"c4\", label: \"India\", value: 4800 }, { id: \"c5\", label: \"France\", value: 3200 }] },\n    ],\n    devices: [{ id: \"d\", label: \"Desktop\", value: 27600 }, { id: \"m\", label: \"Mobile\", value: 17900 }, { id: \"t\", label: \"Tablet\", value: 2710 }],\n  },\n  {\n    id: \"30d\",\n    label: \"30 days\",\n    metrics: [\n      { id: \"v\", label: \"Visitors\", value: \"196,480\", change: 11.9, series: sparkline(6500, 0.18) },\n      { id: \"s\", label: \"Signups\", value: \"7,820\", change: 9.4, series: sparkline(260, 0.1) },\n      { id: \"c\", label: \"Conversion\", value: \"3.9%\", change: -0.2, series: sparkline(39, -0.03) },\n      { id: \"r\", label: \"Revenue\", value: \"$74,960\", change: 6.8, series: sparkline(2500, 0.1) },\n    ],\n    funnel: [\n      { id: \"visit\", label: \"Visited the site\", value: 196480, hint: \"Unique visitors\" },\n      { id: \"pricing\", label: \"Viewed pricing\", value: 80300 },\n      { id: \"start\", label: \"Started checkout\", value: 16900 },\n      { id: \"pay\", label: \"Paid\", value: 5610 },\n    ],\n    lists: [\n      { id: \"pages\", label: \"Pages\", items: [{ id: \"p1\", label: \"/pricing\", value: 51200 }, { id: \"p2\", label: \"/docs/getting-started\", value: 38100 }, { id: \"p3\", label: \"/blog/launch-week\", value: 30200 }, { id: \"p4\", label: \"/components\", value: 21400 }, { id: \"p5\", label: \"/changelog\", value: 16100 }, { id: \"p6\", label: \"/about\", value: 5100 }] },\n      { id: \"sources\", label: \"Sources\", items: [{ id: \"s1\", label: \"Google\", value: 74800 }, { id: \"s2\", label: \"Direct\", value: 46100 }, { id: \"s3\", label: \"Hacker News\", value: 33200 }, { id: \"s4\", label: \"X\", value: 20900 }, { id: \"s5\", label: \"Newsletter\", value: 11800 }] },\n      { id: \"countries\", label: \"Countries\", items: [{ id: \"c1\", label: \"United States\", value: 66900 }, { id: \"c2\", label: \"Germany\", value: 24800 }, { id: \"c3\", label: \"United Kingdom\", value: 23700 }, { id: \"c4\", label: \"India\", value: 19600 }, { id: \"c5\", label: \"France\", value: 13100 }] },\n    ],\n    devices: [{ id: \"d\", label: \"Desktop\", value: 112400 }, { id: \"m\", label: \"Mobile\", value: 73200 }, { id: \"t\", label: \"Tablet\", value: 10880 }],\n  },\n]\n\nfunction Skel() {\n  return (\n    <div aria-hidden=\"true\" className=\"grid gap-6\">\n      <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">{[0, 1, 2, 3].map((i) => <Skeleton key={i} className=\"h-28 rounded-xl\" />)}</div>\n      <div className=\"grid gap-6 lg:grid-cols-2\"><Skeleton className=\"h-80 rounded-xl\" /><Skeleton className=\"h-80 rounded-xl\" /></div>\n    </div>\n  )\n}\n\n/**\n * A product analytics page that reads like a report: four metrics with their change and a trend, the funnel from visit to payment with where people drop out,\n * a tabbed ranking of pages, sources or countries, and how traffic splits by device. Switching the period swaps all of it.\n */\nfunction Analytics1({ title = \"Analytics\", description = \"How people find the product, and where they stop.\", periods = samplePeriods, defaultPeriod, onPeriodChange, state = \"ready\", onRetry, emptyState, errorState, className, ...props }: Analytics1Props) {\n  const id = React.useId()\n  const [pid, setPid] = React.useState(defaultPeriod ?? periods[1]?.id ?? periods[0]?.id)\n  const [tab, setTab] = React.useState<string>(\"pages\")\n  const [step, setStep] = React.useState<string | null>(null)\n  const period = periods.find((p) => p.id === pid) ?? periods[0]\n  const view = state === \"ready\" && !period ? \"empty\" : state\n  const list = period?.lists.find((l) => l.id === tab) ?? period?.lists[0]\n  const total = period?.devices.reduce((s, d) => s + d.value, 0) ?? 0\n\n  return (\n    <section data-slot=\"analytics-1\" data-state={state} aria-labelledby={`${id}-title`} className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...frameProps(props)}>\n      <header className=\"mb-6 flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <h1 id={`${id}-title`} className=\"text-2xl font-semibold tracking-[-0.03em] text-balance\">{title}</h1>\n          <p className=\"mt-1 text-sm text-muted-foreground text-pretty\">{description}</p>\n        </div>\n        {period && (\n          <div role=\"radiogroup\" aria-label=\"Period\" className=\"flex gap-0.5 rounded-lg border bg-card p-0.5\">\n            {periods.map((p) => (\n              <label key={p.id} className=\"relative\">\n                <input type=\"radio\" name={`${id}-period`} className=\"peer sr-only\" checked={pid === p.id} onChange={() => { setPid(p.id); onPeriodChange?.(p.id) }} />\n                <span className=\"flex h-8 cursor-pointer items-center rounded-md px-3 text-sm font-medium text-muted-foreground transition-colors peer-checked:bg-foreground peer-checked:text-background peer-focus-visible:ring-[3px] peer-focus-visible:ring-ring/50 motion-reduce:transition-none\">{p.label}</span>\n              </label>\n            ))}\n          </div>\n        )}\n      </header>\n      <StateView state={view} onRetry={onRetry} label=\"Loading analytics\" loading={<Skel />} empty={{ title: \"No data for this period\", description: \"When people visit, their activity appears here.\", ...emptyState }} error={{ title: \"Analytics could not be loaded\", description: \"Nothing was changed. Check your connection and try again.\", ...errorState }}>\n        {period && (\n          <div className=\"grid gap-6\">\n            <dl className=\"grid grid-cols-2 gap-3 lg:grid-cols-4 lg:gap-4\">\n              {period.metrics.map((m) => (\n                <div key={m.id} className=\"grid min-w-0 gap-1 rounded-xl border bg-card p-4\">\n                  <dt className=\"text-xs font-medium text-muted-foreground\">{m.label}</dt>\n                  <dd className=\"text-2xl font-semibold tracking-[-0.03em] tabular-nums\">{m.value}</dd>\n                  <dd className=\"flex flex-wrap items-center gap-x-1 text-xs font-medium whitespace-nowrap tabular-nums\">\n                    {m.change > 0 ? <ArrowUpRight aria-hidden=\"true\" className=\"size-3.5 rtl:-scale-x-100\" /> : m.change < 0 ? <ArrowDownRight aria-hidden=\"true\" className=\"size-3.5 rtl:-scale-x-100\" /> : <Minus aria-hidden=\"true\" className=\"size-3.5\" />}\n                    {m.change === 0 ? \"No change\" : `${m.change > 0 ? \"Up\" : \"Down\"} ${Math.abs(m.change)}%`}\n                    <span className=\"font-normal text-muted-foreground max-sm:sr-only\">vs the period before</span>\n                  </dd>\n                  <dd className=\"mt-1 text-foreground/70\"><Sparkline values={m.series} label={`${m.label} over the period`} height={28} showEnd={false} /></dd>\n                </div>\n              ))}\n            </dl>\n            <div className=\"grid items-start gap-6 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,1fr)]\">\n              <div className=\"grid gap-3 rounded-xl border bg-card p-5\">\n                <h2 className=\"text-sm font-semibold\">Visit to purchase</h2>\n                <FunnelChart steps={period.funnel} selected={step} onSelect={setStep} />\n              </div>\n              <div className=\"grid gap-6\">\n                <div className=\"grid gap-3 rounded-xl border bg-card p-5\">\n                  <div className=\"flex flex-wrap items-center justify-between gap-2\">\n                    <h2 className=\"text-sm font-semibold\">Top {list?.label.toLowerCase()}</h2>\n                    <div role=\"tablist\" aria-label=\"Ranking\" className=\"flex gap-0.5\">\n                      {period.lists.map((l) => (\n                        <button key={l.id} role=\"tab\" type=\"button\" id={`${id}-tab-${l.id}`} aria-selected={list?.id === l.id} aria-controls={`${id}-panel`} tabIndex={list?.id === l.id ? 0 : -1} onClick={() => setTab(l.id)} onKeyDown={(e) => {\n                          const i = period.lists.findIndex((x) => x.id === l.id)\n                          const n = e.key === \"ArrowRight\" ? (i + 1) % period.lists.length : e.key === \"ArrowLeft\" ? (i - 1 + period.lists.length) % period.lists.length : -1\n                          if (n < 0) return\n                          e.preventDefault()\n                          setTab(period.lists[n].id)\n                          document.getElementById(`${id}-tab-${period.lists[n].id}`)?.focus()\n                        }} className=\"h-8 rounded-md px-2.5 text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 aria-selected:bg-accent aria-selected:text-foreground\">{l.label}</button>\n                      ))}\n                    </div>\n                  </div>\n                  <div role=\"tabpanel\" id={`${id}-panel`} aria-labelledby={`${id}-tab-${list?.id}`}>\n                    {list && <BarList items={list.items} limit={5} showShare />}\n                  </div>\n                </div>\n                <div className=\"rounded-xl border bg-card p-5\">\n                  <SplitBar title=\"Devices\" total={total.toLocaleString(\"en-US\")} segments={period.devices} />\n                </div>\n              </div>\n            </div>\n          </div>\n        )}\n      </StateView>\n    </section>\n  )\n}\n\nexport { Analytics1, type Analytics1Metric, type Analytics1Period, type Analytics1Props }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/analytics-1/analytics-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "dashboard",
      "analytics",
      "funnel",
      "metrics",
      "traffic",
      "growth",
      "charts",
      "report"
    ],
    "version": "1.0.0",
    "updated": "2026-10-24",
    "tests": true,
    "ai": {
      "summary": "Product analytics. <Analytics1 title description periods={[{ id, label, metrics, funnel, lists, devices }]} defaultPeriod onPeriodChange={(id) => void} />. Each period carries four metrics ({ label, value, change, series }), funnel steps, ranked lists for the tabs and device segments. Switching the period swaps every part. The period is a radio group; the ranking is a tablist with arrow keys.",
      "whenToUse": [
        "The overview page of an analytics or growth tool",
        "A report a founder reads every morning"
      ],
      "whenNotToUse": [
        "Operational health; use reliability-1",
        "Revenue and billing detail; use cost-1 or dashboard-1"
      ],
      "composesWith": [
        "reliability-1",
        "reviews-2",
        "app-shell-1",
        "dashboard-2"
      ],
      "a11y": [
        {
          "keys": "Period",
          "action": "A labelled radio group; arrow keys change it and the whole page follows"
        },
        {
          "keys": "Metrics",
          "action": "A description list; changes are written as 'Up 8.2%', never only an arrow or a color"
        },
        {
          "keys": "Ranking",
          "action": "A tablist with a tabpanel; Left and Right move between tabs"
        },
        {
          "keys": "Funnel",
          "action": "Drop-off and conversion are text; steps are toggle buttons"
        }
      ],
      "customization": [
        "title and description",
        "periods and defaultPeriod",
        "onPeriodChange"
      ]
    },
    "states": [
      {
        "id": "loading",
        "title": "Loading",
        "example": "analytics-1-loading",
        "trigger": "state=\"loading\"",
        "note": "Skeleton metrics and charts under the real heading, announced as loading."
      },
      {
        "id": "empty",
        "title": "No data for this period",
        "example": "analytics-1-empty",
        "trigger": "state=\"empty\"",
        "note": "Says there is no data for the period; emptyState sets the copy."
      },
      {
        "id": "error",
        "title": "Could not load",
        "example": "analytics-1-error",
        "trigger": "state=\"error\"",
        "note": "An alert saying nothing was changed, with Try again (onRetry)."
      }
    ],
    "examples": [
      "analytics-1-demo",
      "analytics-1-week",
      "analytics-1-loading",
      "analytics-1-empty",
      "analytics-1-error"
    ],
    "url": "https://ui.ballmac.com/components/analytics-1"
  },
  "categories": [
    "blocks",
    "dashboard"
  ],
  "type": "registry:block"
}