{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dashboard-2",
  "title": "Dashboard 2: traffic analytics",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A web analytics page: a live-now pill, date range and compare switch, a visitors line chart against the previous period, traffic sources as a donut, devices, top countries and a top-pages table.",
  "dependencies": [
    "lucide-react",
    "recharts@^3"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/chart.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/status-dot.json",
    "https://ui.ballmac.com/r/switch.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/dashboard-2/dashboard-2.tsx",
      "content": "// Ballmac UI: Dashboard 2. https://ui.ballmac.com/blocks/dashboard-2\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownRight, ArrowUpRight, Globe, Monitor, Smartphone, Tablet } from \"lucide-react\"\nimport { CartesianGrid, Cell, Label, Line, LineChart, Pie, PieChart, XAxis, YAxis } from \"recharts\"\n\nimport { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"@/components/ballmac/chart\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { StatusDot } from \"@/components/ballmac/status-dot\"\nimport { Switch } from \"@/components/ballmac/switch\"\nimport { cn } from \"@/lib/utils\"\nimport { useLocale } from \"@/lib/ballmac/i18n\"\n\ntype Dashboard2Range = \"7d\" | \"30d\" | \"12m\"\n\ntype Dashboard2Page = { path: string; views: number; bounce: number }\ntype Dashboard2Country = { name: string; visitors: number }\n\ntype Dashboard2Props = Omit<React.ComponentProps<\"section\">, \"title\"> & {\n  /** Page heading. */\n  title?: string\n  /** One line under the heading. */\n  description?: string\n  /** Range shown first. */\n  defaultRange?: Dashboard2Range\n  /** Visitors per step for a range and the same steps a period earlier. Defaults to a built-in demo curve. */\n  getSeries?: (range: Dashboard2Range) => { label: string; visitors: number; previous: number }[]\n  /** Visitors by traffic source. The first five are coloured. */\n  sources?: { key: string; label: string; visitors: number }[]\n  /** Share of visitors by device, in percent, summing to about 100. */\n  devices?: { desktop: number; mobile: number; tablet: number }\n  /** Most viewed pages. */\n  pages?: Dashboard2Page[]\n  /** Visitors by country, largest first. */\n  countries?: Dashboard2Country[]\n  /** People on the site right now. Pass null to hide the live pill. */\n  live?: number | null\n  /** Locale for numbers. Fixed by default so server and browser match. */\n  locale?: string\n}\n\nconst STEPS: Record<Dashboard2Range, number> = { \"7d\": 7, \"30d\": 30, \"12m\": 12 }\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\nfunction demoSeries(range: Dashboard2Range) {\n  const n = STEPS[range]\n  const end = Date.UTC(2026, 8, 30)\n  const base = range === \"12m\" ? 52000 : 1800\n  const val = (t: number) => Math.round(base * (1 + t * (range === \"12m\" ? 0.02 : 0.004) + Math.sin(t / 1.9) * 0.12 + Math.cos(t / 4.4) * 0.09))\n  return Array.from({ length: n }, (_, i) => {\n    const d = new Date(end - (n - 1 - i) * (range === \"12m\" ? 30.4 * 86400000 : 86400000))\n    return {\n      label: range === \"12m\" ? MONTHS[d.getUTCMonth()] : `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`,\n      visitors: val(i),\n      previous: Math.round(val(i - n) * 0.96),\n    }\n  })\n}\n\nconst defaultSources = [\n  { key: \"search\", label: \"Search\", visitors: 18420 },\n  { key: \"direct\", label: \"Direct\", visitors: 12310 },\n  { key: \"social\", label: \"Social\", visitors: 6540 },\n  { key: \"email\", label: \"Email\", visitors: 4120 },\n  { key: \"referral\", label: \"Referral\", visitors: 2680 },\n]\n\nconst defaultPages: Dashboard2Page[] = [\n  { path: \"/\", views: 24810, bounce: 38 },\n  { path: \"/pricing\", views: 12940, bounce: 31 },\n  { path: \"/blog/month-end-close\", views: 9120, bounce: 52 },\n  { path: \"/product/invoicing\", views: 7480, bounce: 36 },\n  { path: \"/customers\", views: 4310, bounce: 44 },\n]\n\nconst defaultCountries: Dashboard2Country[] = [\n  { name: \"United States\", visitors: 21400 },\n  { name: \"United Kingdom\", visitors: 8900 },\n  { name: \"Germany\", visitors: 6100 },\n  { name: \"Portugal\", visitors: 4800 },\n  { name: \"Canada\", visitors: 3700 },\n]\n\nconst chartColors = [\"var(--chart-1)\", \"var(--chart-2)\", \"var(--chart-3)\", \"var(--chart-4)\", \"var(--chart-5)\"]\n\nfunction Dashboard2({\n  title = \"Traffic\",\n  description = \"Who visits, where they come from and what they read.\",\n  defaultRange = \"30d\",\n  getSeries = demoSeries,\n  sources = defaultSources,\n  devices = { desktop: 54, mobile: 39, tablet: 7 },\n  pages = defaultPages,\n  countries = defaultCountries,\n  live = 128,\n  locale,\n  className,\n  ...props\n}: Dashboard2Props) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const [range, setRange] = React.useState<Dashboard2Range>(defaultRange)\n  const [compare, setCompare] = React.useState(true)\n  const compareId = React.useId()\n  const series = React.useMemo(() => getSeries(range), [getSeries, range])\n  const int = React.useMemo(() => new Intl.NumberFormat(locale), [locale])\n  const compact = React.useMemo(() => new Intl.NumberFormat(locale, { notation: \"compact\", maximumFractionDigits: 1 }), [locale])\n\n  const total = series.reduce((n, p) => n + p.visitors, 0)\n  const before = series.reduce((n, p) => n + p.previous, 0)\n  const delta = before ? ((total - before) / before) * 100 : 0\n  const Arrow = delta >= 0 ? ArrowUpRight : ArrowDownRight\n  const sourceTotal = sources.reduce((n, s) => n + s.visitors, 0)\n  const maxViews = Math.max(...pages.map((p) => p.views), 1)\n  const maxCountry = Math.max(...countries.map((c) => c.visitors), 1)\n\n  const lineConfig = {\n    visitors: { label: \"Visitors\", color: \"var(--chart-1)\" },\n    previous: { label: \"Previous period\", color: \"var(--muted-foreground)\" },\n  } satisfies ChartConfig\n  const sourceConfig: ChartConfig = { visitors: { label: \"Visitors\" } }\n  sources.slice(0, 5).forEach((s, i) => {\n    sourceConfig[s.key] = { label: s.label, color: chartColors[i] }\n  })\n  const deviceRows = [\n    { key: \"desktop\", label: \"Desktop\", icon: Monitor, value: devices.desktop, tone: \"bg-chart-1\" },\n    { key: \"mobile\", label: \"Mobile\", icon: Smartphone, value: devices.mobile, tone: \"bg-chart-3\" },\n    { key: \"tablet\", label: \"Tablet\", icon: Tablet, value: devices.tablet, tone: \"bg-chart-5\" },\n  ]\n  const tickEvery = Math.max(1, Math.floor(series.length / 6))\n\n  return (\n    <section data-slot=\"dashboard-2\" className={cn(\"mx-auto w-full max-w-6xl px-4 py-8 sm:px-6\", className)} {...props}>\n      <div className=\"flex flex-wrap items-end justify-between gap-4\">\n        <div>\n          <div className=\"flex flex-wrap items-center gap-3\">\n            <h2 className=\"text-2xl font-semibold tracking-[-0.03em]\">{title}</h2>\n            {live !== null && (\n              <span className=\"rounded-full border px-3 py-1\">\n                <StatusDot status=\"online\" pulse label={`${int.format(live)} online now`} className=\"text-xs\" />\n              </span>\n            )}\n          </div>\n          <p className=\"text-muted-foreground mt-1 text-sm\">{description}</p>\n        </div>\n        <div className=\"flex flex-wrap items-center gap-4\">\n          <label htmlFor={compareId} className=\"flex cursor-pointer items-center gap-2 text-sm\">\n            <Switch id={compareId} checked={compare} onCheckedChange={setCompare} />\n            Compare\n          </label>\n          <SegmentedControl aria-label=\"Date range\" value={range} onValueChange={(v) => setRange(v as Dashboard2Range)}>\n            <SegmentedControlItem value=\"7d\">7 days</SegmentedControlItem>\n            <SegmentedControlItem value=\"30d\">30 days</SegmentedControlItem>\n            <SegmentedControlItem value=\"12m\">12 months</SegmentedControlItem>\n          </SegmentedControl>\n        </div>\n      </div>\n\n      <div className=\"bg-card mt-6 rounded-2xl border p-4 sm:p-6\">\n        <div className=\"flex flex-wrap items-end justify-between gap-3\">\n          <div>\n            <p className=\"text-muted-foreground text-sm\">Visitors</p>\n            <p className=\"mt-1 flex items-baseline gap-3\">\n              <span className=\"text-4xl font-semibold tracking-[-0.04em] tabular-nums\">{int.format(total)}</span>\n              <span className=\"inline-flex items-center gap-1 text-sm font-medium\">\n                <Arrow className={cn(\"size-4\", delta >= 0 ? \"text-chart-2\" : \"text-destructive\")} aria-hidden=\"true\" />\n                <span className=\"tabular-nums\">{Math.abs(delta).toFixed(1)}%</span>\n                <span className=\"text-muted-foreground font-normal\">vs previous period</span>\n              </span>\n            </p>\n          </div>\n        </div>\n        <ChartContainer\n          config={lineConfig}\n          label={`Visitors over the last ${range === \"12m\" ? \"12 months\" : STEPS[range] + \" days\"}`}\n          summary={`${int.format(total)} visitors, ${Math.abs(delta).toFixed(1)} percent ${delta >= 0 ? \"more\" : \"fewer\"} than the previous period.`}\n          className=\"mt-5 aspect-[16/6] w-full min-h-52\"\n        >\n          <LineChart data={series} margin={{ left: 0, right: 8, top: 8 }} accessibilityLayer>\n            <CartesianGrid vertical={false} />\n            <XAxis dataKey=\"label\" tickLine={false} axisLine={false} tickMargin={8} interval={tickEvery - 1} />\n            <YAxis tickLine={false} axisLine={false} width={44} tickFormatter={(v: number) => compact.format(v)} />\n            <ChartTooltip cursor={false} content={<ChartTooltipContent indicator=\"line\" />} />\n            {compare && <Line dataKey=\"previous\" type=\"monotone\" stroke=\"var(--color-previous)\" strokeWidth={1.5} strokeDasharray=\"4 4\" dot={false} />}\n            <Line dataKey=\"visitors\" type=\"monotone\" stroke=\"var(--color-visitors)\" strokeWidth={2} dot={false} activeDot={{ r: 4, strokeWidth: 2, stroke: \"var(--card)\" }} />\n            {compare && <ChartLegend content={<ChartLegendContent />} />}\n          </LineChart>\n        </ChartContainer>\n      </div>\n\n      <div className=\"mt-3 grid gap-3 md:grid-cols-2 lg:grid-cols-3\">\n        <div className=\"bg-card rounded-2xl border p-4 sm:p-5\">\n          <p className=\"text-sm font-medium\">Traffic sources</p>\n          <ChartContainer\n            config={sourceConfig}\n            label=\"Visitors by traffic source\"\n            summary={sources.map((s) => `${s.label} ${int.format(s.visitors)}`).join(\", \")}\n            className=\"mx-auto mt-2 aspect-square w-full max-w-[13rem]\"\n          >\n            <PieChart accessibilityLayer>\n              <ChartTooltip content={<ChartTooltipContent hideLabel nameKey=\"key\" />} />\n              <Pie data={sources.slice(0, 5)} dataKey=\"visitors\" nameKey=\"key\" innerRadius=\"64%\" outerRadius=\"92%\" paddingAngle={2} strokeWidth={0}>\n                {sources.slice(0, 5).map((s) => (\n                  <Cell key={s.key} fill={`var(--color-${s.key})`} />\n                ))}\n                <Label\n                  position=\"center\"\n                  content={({ viewBox }) =>\n                    viewBox && \"cx\" in viewBox ? (\n                      <text x={viewBox.cx} y={viewBox.cy} textAnchor=\"middle\" dominantBaseline=\"middle\">\n                        <tspan x={viewBox.cx} y={(viewBox.cy ?? 0) - 6} className=\"fill-foreground text-xl font-semibold\">{compact.format(sourceTotal)}</tspan>\n                        <tspan x={viewBox.cx} y={(viewBox.cy ?? 0) + 14} className=\"fill-muted-foreground text-xs\">visitors</tspan>\n                      </text>\n                    ) : null\n                  }\n                />\n              </Pie>\n            </PieChart>\n          </ChartContainer>\n          <ul className=\"mt-3 grid grid-cols-2 gap-x-4 gap-y-1.5 text-sm\">\n            {sources.slice(0, 5).map((s, i) => (\n              <li key={s.key} className=\"flex items-center gap-2\">\n                <span aria-hidden=\"true\" className=\"size-2.5 rounded-sm\" style={{ background: chartColors[i] }} />\n                <span className=\"flex-1 truncate\">{s.label}</span>\n                <span className=\"text-muted-foreground tabular-nums\">{Math.round((s.visitors / sourceTotal) * 100)}%</span>\n              </li>\n            ))}\n          </ul>\n        </div>\n\n        <div className=\"bg-card rounded-2xl border p-4 sm:p-5\">\n          <p className=\"text-sm font-medium\">Devices</p>\n          <div className=\"mt-5 flex h-3 gap-0.5 overflow-hidden rounded-full\" role=\"img\" aria-label={deviceRows.map((d) => `${d.label} ${d.value} percent`).join(\", \")}>\n            {deviceRows.map((d) => (\n              <span key={d.key} className={cn(\"h-full first:rounded-s-full last:rounded-e-full\", d.tone)} style={{ width: `${d.value}%` }} />\n            ))}\n          </div>\n          <ul className=\"mt-5 space-y-4\">\n            {deviceRows.map((d) => (\n              <li key={d.key} className=\"flex items-center gap-3 text-sm\">\n                <span className=\"bg-muted flex size-9 items-center justify-center rounded-lg\"><d.icon className=\"size-4\" aria-hidden=\"true\" /></span>\n                <span className=\"flex-1\">{d.label}</span>\n                <span className={cn(\"size-2.5 rounded-sm\", d.tone)} aria-hidden=\"true\" />\n                <span className=\"w-10 text-end font-medium tabular-nums\">{d.value}%</span>\n              </li>\n            ))}\n          </ul>\n        </div>\n\n        <div className=\"bg-card rounded-2xl border p-4 sm:p-5 md:col-span-2 lg:col-span-1\">\n          <p className=\"flex items-center gap-2 text-sm font-medium\"><Globe className=\"size-4\" aria-hidden=\"true\" />Top countries</p>\n          <ul className=\"mt-5 space-y-4\">\n            {countries.map((c) => (\n              <li key={c.name}>\n                <div className=\"flex items-baseline justify-between text-sm\">\n                  <span>{c.name}</span>\n                  <span className=\"text-muted-foreground tabular-nums\">{int.format(c.visitors)}</span>\n                </div>\n                <div className=\"bg-muted mt-1.5 h-2 overflow-hidden rounded-full\" role=\"img\" aria-label={`${c.name}: ${int.format(c.visitors)} visitors`}>\n                  <div className=\"bg-chart-1 h-full rounded-full\" style={{ width: `${(c.visitors / maxCountry) * 100}%` }} />\n                </div>\n              </li>\n            ))}\n          </ul>\n        </div>\n      </div>\n\n      <div className=\"bg-card mt-3 overflow-hidden rounded-2xl border\">\n        <p className=\"px-4 py-4 text-sm font-medium sm:px-5\">Top pages</p>\n        <div className=\"overflow-x-auto\" tabIndex={0} role=\"region\" aria-label=\"Top pages\">\n          <table className=\"w-full min-w-[28rem] text-start text-sm\">\n            <thead>\n              <tr className=\"text-muted-foreground border-y text-xs\">\n                <th scope=\"col\" className=\"px-4 py-2.5 font-medium sm:px-5\">Page</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 font-medium\">Views</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium sm:px-5\">Bounce rate</th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y\">\n              {pages.map((p) => (\n                <tr key={p.path} className=\"hover:bg-accent/40 transition-colors\">\n                  <td className=\"px-4 py-3 font-mono text-xs sm:px-5\">{p.path}</td>\n                  <td className=\"px-4 py-3\">\n                    <div className=\"flex items-center gap-3\">\n                      <span className=\"w-14 tabular-nums\">{int.format(p.views)}</span>\n                      <span aria-hidden=\"true\" className=\"bg-muted hidden h-1.5 w-32 overflow-hidden rounded-full sm:block\"><span className=\"bg-chart-1 block h-full rounded-full\" style={{ width: `${(p.views / maxViews) * 100}%` }} /></span>\n                    </div>\n                  </td>\n                  <td className=\"px-4 py-3 text-end tabular-nums sm:px-5\">{p.bounce}%</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Dashboard2, type Dashboard2Props, type Dashboard2Range, type Dashboard2Page, type Dashboard2Country }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/dashboard-2/dashboard-2.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "dashboard",
      "analytics",
      "traffic",
      "line chart",
      "donut",
      "visitors",
      "web"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "A traffic dashboard. Pass getSeries(range) returning { label, visitors, previous }[], plus sources, devices, pages, countries and live (null hides the pill).",
      "whenToUse": [
        "Site and product analytics",
        "Marketing performance pages"
      ],
      "whenNotToUse": [
        "Sales and revenue (use dashboard-1)"
      ],
      "composesWith": [
        "app-shell-1",
        "dashboard-1",
        "settings-1"
      ],
      "a11y": [
        {
          "keys": "Arrow Left / Right",
          "action": "Changes the date range"
        },
        {
          "keys": "Compare switch",
          "action": "Shows or hides the dashed previous-period line; the legend appears with it"
        },
        {
          "keys": "Charts",
          "action": "Each chart is a labelled figure with a text summary; sources, devices and countries are also listed as text with their values"
        }
      ],
      "customization": [
        "getSeries, sources, devices, pages, countries",
        "live: number | null",
        "locale"
      ]
    },
    "examples": [
      "dashboard-2-demo",
      "dashboard-2-week"
    ],
    "url": "https://ui.ballmac.com/components/dashboard-2"
  },
  "categories": [
    "blocks",
    "dashboard"
  ],
  "type": "registry:block"
}