{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dashboard-1",
  "title": "Dashboard 1: sales overview",
  "author": "Ballmac <https://ui.ballmac.com>",
  "description": "A sales dashboard page: a date-range switch that recomputes everything, four KPI cards with change and trend, a revenue area chart against the previous period, revenue by channel and a recent-orders table.",
  "dependencies": [
    "lucide-react",
    "recharts@^3"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.ballmac.com/r/badge.json",
    "https://ui.ballmac.com/r/button.json",
    "https://ui.ballmac.com/r/chart.json",
    "https://ui.ballmac.com/r/segmented-control.json",
    "https://ui.ballmac.com/r/sparkline.json",
    "https://ui.ballmac.com/r/i18n.json"
  ],
  "files": [
    {
      "path": "registry/ballmac/components/blocks/dashboard-1/dashboard-1.tsx",
      "content": "// Ballmac UI: Dashboard 1. https://ui.ballmac.com/blocks/dashboard-1\n\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownRight, ArrowUpRight, Download } from \"lucide-react\"\nimport { Area, AreaChart, CartesianGrid, XAxis, YAxis } from \"recharts\"\n\nimport { Badge } from \"@/components/ballmac/badge\"\nimport { Button } from \"@/components/ballmac/button\"\nimport { ChartContainer, ChartLegend, ChartLegendContent, ChartTooltip, ChartTooltipContent, type ChartConfig } from \"@/components/ballmac/chart\"\nimport { SegmentedControl, SegmentedControlItem } from \"@/components/ballmac/segmented-control\"\nimport { Sparkline } from \"@/components/ballmac/sparkline\"\nimport { cn } from \"@/lib/utils\"\nimport { useLocale } from \"@/lib/ballmac/i18n\"\n\ntype Dashboard1Range = \"7d\" | \"30d\" | \"90d\"\n\ntype Dashboard1Order = {\n  id: string\n  customer: string\n  email: string\n  status: \"paid\" | \"pending\" | \"refunded\"\n  total: number\n  /** ISO date (YYYY-MM-DD), shown in UTC. */\n  date: string\n}\n\ntype Dashboard1Channel = { name: string; share: number }\n\ntype Dashboard1Props = 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?: Dashboard1Range\n  /** Called when the range changes. Pass your own data for each range with `getSeries`. */\n  onRangeChange?: (range: Dashboard1Range) => void\n  /**\n   * Revenue per day for a range, and the same days a period earlier. Defaults to a built-in demo curve so the block\n   * works with no data.\n   */\n  getSeries?: (range: Dashboard1Range) => { label: string; revenue: number; previous: number; orders: number }[]\n  /** Share of revenue by channel, largest first. */\n  channels?: Dashboard1Channel[]\n  /** Recent orders, newest first. */\n  orders?: Dashboard1Order[]\n  /** ISO currency code. */\n  currency?: string\n  /** Locale for numbers and dates. Fixed by default so server and browser match. */\n  locale?: string\n}\n\nconst DAYS: Record<Dashboard1Range, number> = { \"7d\": 7, \"30d\": 30, \"90d\": 90 }\nconst MONTHS = [\"Jan\", \"Feb\", \"Mar\", \"Apr\", \"May\", \"Jun\", \"Jul\", \"Aug\", \"Sep\", \"Oct\", \"Nov\", \"Dec\"]\n\n/** A smooth, deterministic demo curve (no randomness, so the server and browser draw the same chart). */\nfunction demoSeries(range: Dashboard1Range) {\n  const n = DAYS[range]\n  const end = Date.UTC(2026, 8, 30)\n  const point = (i: number, shift: number) => {\n    const t = i + shift\n    return Math.round(2900 + t * 6 + Math.sin(t / 2.3) * 520 + Math.cos(t / 5.1) * 380)\n  }\n  return Array.from({ length: n }, (_, i) => {\n    const d = new Date(end - (n - 1 - i) * 86400000)\n    const revenue = point(i, 0)\n    return {\n      label: `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}`,\n      revenue,\n      previous: Math.round(point(i, -n) * 0.94),\n      orders: Math.round(revenue / 54),\n    }\n  })\n}\n\nconst defaultChannels: Dashboard1Channel[] = [\n  { name: \"Search\", share: 38 },\n  { name: \"Direct\", share: 27 },\n  { name: \"Email\", share: 19 },\n  { name: \"Social\", share: 11 },\n  { name: \"Referral\", share: 5 },\n]\n\nconst defaultOrders: Dashboard1Order[] = [\n  { id: \"ORD-10482\", customer: \"Northwind Studio\", email: \"billing@northwind.co\", status: \"paid\", total: 4200, date: \"2026-09-30\" },\n  { id: \"ORD-10481\", customer: \"Globex Corp\", email: \"ap@globex.com\", status: \"pending\", total: 12800, date: \"2026-09-30\" },\n  { id: \"ORD-10480\", customer: \"Initech\", email: \"finance@initech.io\", status: \"paid\", total: 960, date: \"2026-09-29\" },\n  { id: \"ORD-10479\", customer: \"Umbrella Labs\", email: \"pay@umbrella.dev\", status: \"refunded\", total: 3450, date: \"2026-09-29\" },\n  { id: \"ORD-10478\", customer: \"Stark Supply\", email: \"accounts@stark.supply\", status: \"paid\", total: 7100, date: \"2026-09-28\" },\n  { id: \"ORD-10477\", customer: \"Wayne Freight\", email: \"ops@wayne.co\", status: \"paid\", total: 2280, date: \"2026-09-27\" },\n]\n\nconst statusTone = { paid: \"success\", pending: \"warning\", refunded: \"neutral\" } as const\nconst statusLabel = { paid: \"Paid\", pending: \"Pending\", refunded: \"Refunded\" }\n\nconst config = {\n  revenue: { label: \"This period\", color: \"var(--chart-1)\" },\n  previous: { label: \"Previous period\", color: \"var(--muted-foreground)\" },\n} satisfies ChartConfig\n\n/** A short moving average, so a noisy ratio draws as a calm trend line. */\nfunction smooth(values: number[]) {\n  return values.map((_, i) => {\n    const slice = values.slice(Math.max(0, i - 3), i + 4)\n    return slice.reduce((n, v) => n + v, 0) / slice.length\n  })\n}\n\nfunction pctChange(now: number, before: number) {\n  return before === 0 ? 0 : ((now - before) / before) * 100\n}\n\nfunction Dashboard1({\n  title = \"Overview\",\n  description = \"How the store is doing compared with the previous period.\",\n  defaultRange = \"30d\",\n  onRangeChange,\n  getSeries = demoSeries,\n  channels = defaultChannels,\n  orders = defaultOrders,\n  currency = \"USD\",\n  locale,\n  className,\n  ...props\n}: Dashboard1Props) {\n  const defaultLocale = useLocale()\n  locale ??= defaultLocale\n  const [range, setRange] = React.useState<Dashboard1Range>(defaultRange)\n  const series = React.useMemo(() => getSeries(range), [getSeries, range])\n  const money = React.useMemo(() => new Intl.NumberFormat(locale, { style: \"currency\", currency, maximumFractionDigits: 0 }), [locale, currency])\n  const compact = React.useMemo(() => new Intl.NumberFormat(locale, { style: \"currency\", currency, notation: \"compact\", minimumFractionDigits: 0, maximumFractionDigits: 1 }), [locale, currency])\n  const int = React.useMemo(() => new Intl.NumberFormat(locale), [locale])\n  const dateFmt = React.useMemo(() => new Intl.DateTimeFormat(locale, { month: \"short\", day: \"numeric\", timeZone: \"UTC\" }), [locale])\n\n  const sum = (key: \"revenue\" | \"previous\" | \"orders\") => series.reduce((n, p) => n + p[key], 0)\n  const revenue = sum(\"revenue\")\n  const prevRevenue = sum(\"previous\")\n  const orderCount = sum(\"orders\")\n  const prevOrders = Math.round(prevRevenue / 54)\n  const aov = orderCount ? revenue / orderCount : 0\n  const prevAov = prevOrders ? prevRevenue / prevOrders : 0\n  const kpis = [\n    { label: \"Revenue\", value: compact.format(revenue), delta: pctChange(revenue, prevRevenue), trend: series.map((p) => p.revenue) },\n    { label: \"Orders\", value: int.format(orderCount), delta: pctChange(orderCount, prevOrders), trend: series.map((p) => p.orders) },\n    { label: \"Average order\", value: money.format(aov), delta: pctChange(aov, prevAov), trend: smooth(series.map((p) => p.revenue / Math.max(p.orders, 1))) },\n    { label: \"Refund rate\", value: \"1.8%\", delta: -0.4, invert: true, trend: series.map((p, i) => 2.4 - Math.sin(i / 3) * 0.3 - i * 0.004) },\n  ]\n  const tickEvery = Math.max(1, Math.floor(series.length / 6))\n\n  return (\n    <section data-slot=\"dashboard-1\" 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          <h2 className=\"text-2xl font-semibold tracking-[-0.03em]\">{title}</h2>\n          <p className=\"text-muted-foreground mt-1 text-sm\">{description}</p>\n        </div>\n        <div className=\"flex items-center gap-2\">\n          <SegmentedControl aria-label=\"Date range\" value={range} onValueChange={(v) => { setRange(v as Dashboard1Range); onRangeChange?.(v as Dashboard1Range) }}>\n            <SegmentedControlItem value=\"7d\">7 days</SegmentedControlItem>\n            <SegmentedControlItem value=\"30d\">30 days</SegmentedControlItem>\n            <SegmentedControlItem value=\"90d\">90 days</SegmentedControlItem>\n          </SegmentedControl>\n          <Button variant=\"outline\" size=\"icon\" aria-label=\"Export report\"><Download /></Button>\n        </div>\n      </div>\n\n      <dl className=\"mt-6 grid grid-cols-2 gap-3 lg:grid-cols-4\">\n        {kpis.map((k) => {\n          const good = k.invert ? k.delta <= 0 : k.delta >= 0\n          const Arrow = k.delta >= 0 ? ArrowUpRight : ArrowDownRight\n          return (\n            <div key={k.label} className=\"bg-card rounded-2xl border p-4 sm:p-5\">\n              <dt className=\"text-muted-foreground text-xs sm:text-sm\">{k.label}</dt>\n              <dd className=\"mt-2\">\n                <span className=\"block text-2xl font-semibold tracking-[-0.03em] tabular-nums sm:text-3xl\">{k.value}</span>\n                <span className=\"mt-1 inline-flex items-center gap-1 text-xs font-medium sm:text-sm\">\n                  <Arrow className={cn(\"size-3.5\", good ? \"text-chart-2\" : \"text-destructive\")} aria-hidden=\"true\" />\n                  <span className=\"tabular-nums\">{Math.abs(k.delta).toFixed(1)}%</span>\n                  <span className=\"text-muted-foreground font-normal\">{k.delta >= 0 ? \"up\" : \"down\"} vs previous</span>\n                </span>\n                <Sparkline values={k.trend} label={`${k.label} trend`} height={32} showEnd={false} className=\"text-chart-1 mt-3 w-full\" />\n              </dd>\n            </div>\n          )\n        })}\n      </dl>\n\n      <div className=\"mt-3 grid gap-3 lg:grid-cols-[1fr_20rem]\">\n        <div className=\"bg-card rounded-2xl border p-4 sm:p-5\">\n          <div className=\"flex items-baseline justify-between gap-3\">\n            <p className=\"text-sm font-medium\">Revenue</p>\n            <p className=\"text-muted-foreground text-xs\">Daily, last {DAYS[range]} days</p>\n          </div>\n          <ChartContainer\n            config={config}\n            label={`Daily revenue for the last ${DAYS[range]} days compared with the previous period`}\n            summary={`Revenue totalled ${money.format(revenue)}, ${Math.abs(pctChange(revenue, prevRevenue)).toFixed(1)} percent ${revenue >= prevRevenue ? \"above\" : \"below\"} the previous period.`}\n            className=\"mt-4 aspect-[16/8] w-full\"\n          >\n            <AreaChart data={series} margin={{ left: 0, right: 8, top: 8 }} accessibilityLayer>\n              <defs>\n                <linearGradient id=\"d1-fill\" x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n                  <stop offset=\"5%\" stopColor=\"var(--color-revenue)\" stopOpacity={0.3} />\n                  <stop offset=\"95%\" stopColor=\"var(--color-revenue)\" stopOpacity={0.02} />\n                </linearGradient>\n              </defs>\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) => `$${(v / 1000).toFixed(1)}k`} />\n              <ChartTooltip cursor={false} content={<ChartTooltipContent indicator=\"line\" formatter={(v) => money.format(Number(v))} />} />\n              <Area dataKey=\"previous\" type=\"monotone\" fill=\"none\" stroke=\"var(--color-previous)\" strokeWidth={1.5} strokeDasharray=\"4 4\" dot={false} />\n              <Area dataKey=\"revenue\" type=\"monotone\" fill=\"url(#d1-fill)\" stroke=\"var(--color-revenue)\" strokeWidth={2} dot={false} />\n              <ChartLegend content={<ChartLegendContent />} />\n            </AreaChart>\n          </ChartContainer>\n        </div>\n\n        <div className=\"bg-card rounded-2xl border p-4 sm:p-5\">\n          <p className=\"text-sm font-medium\">Revenue by channel</p>\n          <ul className=\"mt-4 space-y-4\">\n            {channels.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\">{c.share}%</span>\n                </div>\n                <div className=\"bg-muted mt-1.5 h-2 overflow-hidden rounded-full\" role=\"img\" aria-label={`${c.name}: ${c.share} percent of revenue`}>\n                  <div className=\"bg-chart-1 h-full rounded-full\" style={{ width: `${c.share}%` }} />\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        <div className=\"flex items-center justify-between px-4 py-4 sm:px-5\">\n          <p className=\"text-sm font-medium\">Recent orders</p>\n          <a href=\"#orders\" className=\"text-muted-foreground hover:text-foreground focus-visible:ring-ring/50 rounded-sm text-sm underline-offset-4 outline-none hover:underline focus-visible:ring-[3px]\">View all</a>\n        </div>\n        <div className=\"overflow-x-auto\" tabIndex={0} role=\"region\" aria-label=\"Recent orders\">\n          <table className=\"w-full min-w-[34rem] 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\">Order</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 font-medium\">Customer</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 font-medium\">Status</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 font-medium\">Date</th>\n                <th scope=\"col\" className=\"px-4 py-2.5 text-end font-medium sm:px-5\">Total</th>\n              </tr>\n            </thead>\n            <tbody className=\"divide-y\">\n              {orders.map((o) => (\n                <tr key={o.id} className=\"hover:bg-accent/40 transition-colors\">\n                  <td className=\"px-4 py-3 font-mono text-xs sm:px-5\">{o.id}</td>\n                  <td className=\"px-4 py-3\">\n                    <span className=\"block font-medium\">{o.customer}</span>\n                    <span className=\"text-muted-foreground block text-xs\">{o.email}</span>\n                  </td>\n                  <td className=\"px-4 py-3\"><Badge status={statusTone[o.status]}>{statusLabel[o.status]}</Badge></td>\n                  <td className=\"text-muted-foreground px-4 py-3\">{dateFmt.format(new Date(`${o.date}T00:00:00Z`))}</td>\n                  <td className=\"px-4 py-3 text-end font-medium tabular-nums sm:px-5\">{money.format(o.total)}</td>\n                </tr>\n              ))}\n            </tbody>\n          </table>\n        </div>\n      </div>\n    </section>\n  )\n}\n\nexport { Dashboard1, type Dashboard1Props, type Dashboard1Order, type Dashboard1Channel, type Dashboard1Range }\n",
      "type": "registry:component",
      "target": "@components/ballmac/blocks/dashboard-1/dashboard-1.tsx"
    }
  ],
  "meta": {
    "schema": 1,
    "tier": "free",
    "tags": [
      "dashboard",
      "analytics",
      "kpi",
      "chart",
      "orders",
      "sales",
      "table"
    ],
    "version": "1.0.0",
    "updated": "2026-10-01",
    "ai": {
      "summary": "The first screen of a store or SaaS admin. Pass getSeries(range) returning { label, revenue, previous, orders }[], plus channels, orders and currency; the KPIs, deltas and chart are derived from the series.",
      "whenToUse": [
        "Admin home pages",
        "Sales, billing and usage overviews"
      ],
      "whenNotToUse": [
        "Traffic analytics (use dashboard-2)"
      ],
      "composesWith": [
        "app-shell-1",
        "dashboard-2",
        "billing-1",
        "settings-1"
      ],
      "a11y": [
        {
          "keys": "Arrow Left / Right",
          "action": "Changes the date range"
        },
        {
          "keys": "Chart",
          "action": "Labelled figure with a plain-language summary; the same numbers are in the KPI cards and the table"
        },
        {
          "keys": "Tab",
          "action": "The orders table scrolls sideways on phones and is focusable"
        }
      ],
      "customization": [
        "getSeries(range) for your own data",
        "channels, orders, currency, locale",
        "onRangeChange to fetch"
      ]
    },
    "examples": [
      "dashboard-1-demo",
      "dashboard-1-week"
    ],
    "url": "https://ui.ballmac.com/components/dashboard-1"
  },
  "categories": [
    "blocks",
    "dashboard"
  ],
  "type": "registry:block"
}