{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "metrics-01",
  "title": "Metrics 01",
  "author": "ncdai <dai@chanhdai.com>",
  "description": "A metrics section with a line chart.",
  "dependencies": [
    "date-fns"
  ],
  "registryDependencies": [
    "@bklit/line-chart",
    "https://chanhdai.com/r/style.json"
  ],
  "files": [
    {
      "path": "src/registry/blocks/metrics-01/metrics-01.tsx",
      "content": "import { format } from \"date-fns\"\n\nimport Grid from \"@/components/charts/grid\"\nimport LineChart, { Line } from \"@/components/charts/line-chart\"\nimport { ChartTooltip } from \"@/components/charts/tooltip\"\nimport {\n  Metric,\n  MetricChange,\n  MetricLabel,\n  MetricValue,\n} from \"@/registry/blocks/metrics-01/components/metric\"\n\nexport function Metrics01() {\n  return (\n    <div className=\"max-w-screen overflow-x-clip\">\n      <div className=\"container mx-auto px-4\">\n        <div className=\"border-x border-line py-8\">\n          <div className=\"screen-line-top screen-line-bottom\">\n            <h2 className=\"screen-line-bottom ml-4 font-heading text-3xl font-medium tracking-tight\">\n              Insights\n              <sup className=\"top-[-0.75em] ml-1 text-sm font-medium tracking-normal text-muted-foreground\">\n                ({format(new Date(data.startDate), \"dd.MM\")} –{\" \"}\n                {format(new Date(data.endDate), \"dd.MM\")})\n              </sup>\n            </h2>\n\n            <div className=\"relative\">\n              <div className=\"pointer-events-none absolute inset-0 -z-1 grid grid-cols-2 md:grid-cols-4\">\n                <div className=\"border-r border-line\" />\n                <div className=\"border-r border-line max-md:hidden\" />\n                <div className=\"border-r border-line max-md:hidden\" />\n              </div>\n\n              <dl className=\"grid grid-cols-2 md:grid-cols-4\">\n                <Metric>\n                  <MetricLabel>\n                    Unique visitors\n                    <MetricChange value={data.changes.uniqueVisitors} />\n                  </MetricLabel>\n                  <MetricValue>\n                    {data.summary.uniqueVisitors.toLocaleString(\"en-US\")}\n                  </MetricValue>\n                </Metric>\n\n                <Metric>\n                  <MetricLabel>\n                    Sessions\n                    <MetricChange value={data.changes.totalSessions} />\n                  </MetricLabel>\n                  <MetricValue>\n                    {data.summary.totalSessions.toLocaleString(\"en-US\")}\n                  </MetricValue>\n                </Metric>\n\n                <Metric>\n                  <MetricLabel>\n                    Views\n                    <MetricChange value={data.changes.totalScreenViews} />\n                  </MetricLabel>\n                  <MetricValue>\n                    {data.summary.totalScreenViews.toLocaleString(\"en-US\")}\n                  </MetricValue>\n                </Metric>\n\n                <Metric>\n                  <MetricLabel>\n                    Session duration\n                    <MetricChange value={data.changes.avgSessionDuration} />\n                  </MetricLabel>\n                  <MetricValue>\n                    {formatDuration(data.summary.avgSessionDuration)}\n                  </MetricValue>\n                </Metric>\n              </dl>\n            </div>\n\n            {data.series.length > 0 ? (\n              <LineChart\n                className=\"md:aspect-3/1!\"\n                data={data.series}\n                margin={{ top: 16, right: 32, bottom: 40, left: 32 }}\n              >\n                <Grid horizontal />\n                <Line\n                  dataKey=\"totalSessions\"\n                  stroke=\"var(--chart-line-secondary)\"\n                  strokeWidth={2}\n                />\n                <Line\n                  dataKey=\"uniqueVisitors\"\n                  stroke=\"var(--chart-line-primary)\"\n                  strokeWidth={2}\n                />\n                <ChartTooltip />\n              </LineChart>\n            ) : (\n              <div className=\"grid aspect-2/1 w-full place-content-center md:aspect-3/1\">\n                <p className=\"text-muted-foreground\">No insights available.</p>\n              </div>\n            )}\n          </div>\n        </div>\n      </div>\n    </div>\n  )\n}\n\ntype ISODateString = string\n\ntype InsightsSummary = {\n  uniqueVisitors: number\n  totalSessions: number\n  totalScreenViews: number\n  avgSessionDuration: number\n}\n\ntype InsightsSeriesItem = {\n  date: ISODateString\n  uniqueVisitors: number\n  totalSessions: number\n}\n\n/**\n * `null` where the previous period was zero, since growth from zero has no\n * meaningful percentage.\n */\ntype InsightsChanges = Record<keyof InsightsSummary, number | null>\n\ntype InsightsData = {\n  summary: InsightsSummary\n  changes: InsightsChanges\n  series: InsightsSeriesItem[]\n  startDate: ISODateString\n  endDate: ISODateString\n}\n\nconst data: InsightsData = {\n  summary: {\n    uniqueVisitors: 13573,\n    totalSessions: 16017,\n    avgSessionDuration: 380.56365999999997,\n    totalScreenViews: 100563,\n  },\n  changes: {\n    uniqueVisitors: 12.4,\n    totalSessions: 8.1,\n    avgSessionDuration: 5.7,\n    totalScreenViews: -3.2,\n  },\n  series: [\n    {\n      date: \"2026-07-21T00:00:00.000Z\",\n      uniqueVisitors: 438,\n      totalSessions: 514,\n    },\n    {\n      date: \"2026-07-22T00:00:00.000Z\",\n      uniqueVisitors: 452,\n      totalSessions: 519,\n    },\n    {\n      date: \"2026-07-23T00:00:00.000Z\",\n      uniqueVisitors: 433,\n      totalSessions: 487,\n    },\n    {\n      date: \"2026-07-24T00:00:00.000Z\",\n      uniqueVisitors: 466,\n      totalSessions: 522,\n    },\n    {\n      date: \"2026-07-25T00:00:00.000Z\",\n      uniqueVisitors: 484,\n      totalSessions: 555,\n    },\n    {\n      date: \"2026-07-26T00:00:00.000Z\",\n      uniqueVisitors: 545,\n      totalSessions: 631,\n    },\n    {\n      date: \"2026-07-27T00:00:00.000Z\",\n      uniqueVisitors: 596,\n      totalSessions: 691,\n    },\n    {\n      date: \"2026-07-28T00:00:00.000Z\",\n      uniqueVisitors: 522,\n      totalSessions: 598,\n    },\n    {\n      date: \"2026-07-29T00:00:00.000Z\",\n      uniqueVisitors: 531,\n      totalSessions: 609,\n    },\n    {\n      date: \"2026-07-30T00:00:00.000Z\",\n      uniqueVisitors: 537,\n      totalSessions: 629,\n    },\n    {\n      date: \"2026-07-31T00:00:00.000Z\",\n      uniqueVisitors: 442,\n      totalSessions: 509,\n    },\n    {\n      date: \"2026-08-01T00:00:00.000Z\",\n      uniqueVisitors: 437,\n      totalSessions: 533,\n    },\n    {\n      date: \"2026-08-02T00:00:00.000Z\",\n      uniqueVisitors: 380,\n      totalSessions: 451,\n    },\n    {\n      date: \"2026-08-03T00:00:00.000Z\",\n      uniqueVisitors: 435,\n      totalSessions: 484,\n    },\n    {\n      date: \"2026-08-04T00:00:00.000Z\",\n      uniqueVisitors: 485,\n      totalSessions: 539,\n    },\n    {\n      date: \"2026-08-05T00:00:00.000Z\",\n      uniqueVisitors: 465,\n      totalSessions: 540,\n    },\n    {\n      date: \"2026-08-06T00:00:00.000Z\",\n      uniqueVisitors: 423,\n      totalSessions: 495,\n    },\n    {\n      date: \"2026-08-07T00:00:00.000Z\",\n      uniqueVisitors: 421,\n      totalSessions: 474,\n    },\n    {\n      date: \"2026-08-08T00:00:00.000Z\",\n      uniqueVisitors: 328,\n      totalSessions: 419,\n    },\n    {\n      date: \"2026-08-09T00:00:00.000Z\",\n      uniqueVisitors: 577,\n      totalSessions: 638,\n    },\n    {\n      date: \"2026-08-10T00:00:00.000Z\",\n      uniqueVisitors: 682,\n      totalSessions: 762,\n    },\n    {\n      date: \"2026-08-11T00:00:00.000Z\",\n      uniqueVisitors: 450,\n      totalSessions: 496,\n    },\n    {\n      date: \"2026-08-12T00:00:00.000Z\",\n      uniqueVisitors: 413,\n      totalSessions: 483,\n    },\n    {\n      date: \"2026-08-13T00:00:00.000Z\",\n      uniqueVisitors: 422,\n      totalSessions: 477,\n    },\n    {\n      date: \"2026-08-14T00:00:00.000Z\",\n      uniqueVisitors: 424,\n      totalSessions: 473,\n    },\n    {\n      date: \"2026-08-15T00:00:00.000Z\",\n      uniqueVisitors: 378,\n      totalSessions: 422,\n    },\n    {\n      date: \"2026-08-16T00:00:00.000Z\",\n      uniqueVisitors: 383,\n      totalSessions: 453,\n    },\n    {\n      date: \"2026-08-17T00:00:00.000Z\",\n      uniqueVisitors: 413,\n      totalSessions: 498,\n    },\n    {\n      date: \"2026-08-18T00:00:00.000Z\",\n      uniqueVisitors: 417,\n      totalSessions: 473,\n    },\n    {\n      date: \"2026-08-19T00:00:00.000Z\",\n      uniqueVisitors: 572,\n      totalSessions: 643,\n    },\n  ],\n  startDate: \"2026-07-21\",\n  endDate: \"2026-08-20\",\n}\n\n/**\n * Formats a duration given in seconds into a compact `Xh Ym Zs` string.\n * Zero-valued units are omitted; a zero duration renders as `0s`.\n */\nexport function formatDuration(seconds: number): string {\n  const totalSeconds = Math.round(seconds)\n  const hours = Math.floor(totalSeconds / 3600)\n  const minutes = Math.floor((totalSeconds % 3600) / 60)\n  const secs = totalSeconds % 60\n\n  const parts: string[] = []\n  if (hours > 0) parts.push(`${hours}h`)\n  if (minutes > 0) parts.push(`${minutes}m`)\n  if (secs > 0) parts.push(`${secs}s`)\n\n  return parts.length > 0 ? parts.join(\" \") : \"0s\"\n}\n",
      "type": "registry:component",
      "target": "@components/metrics-01.tsx"
    },
    {
      "path": "src/registry/blocks/metrics-01/components/metric.tsx",
      "content": "import { TrendingDownIcon, TrendingUpIcon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport function Metric({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"metric\"\n      className={cn(\n        // `justify-between` keeps values aligned across a row when a label\n        // wraps to two lines in a narrow column.\n        \"flex flex-col justify-between gap-2 p-4\",\n        \"max-md:nth-[2n+1]:screen-line-bottom md:nth-[4n+1]:screen-line-bottom\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport function MetricLabel({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <dt\n      data-slot=\"metric-label\"\n      className={cn(\n        \"flex items-start justify-between gap-2 text-sm/4 text-muted-foreground\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nexport type MetricChangeProps = {\n  /**\n   * Percentage change against the previous period, e.g. `12.4` for `+12.4%`.\n   * `null` when there is no previous period to compare against.\n   */\n  value: number | null\n}\n\n/**\n * Assumes every metric is one where higher is better, so up maps to green.\n * The icon carries the direction too, so the meaning survives without color.\n */\nexport function MetricChange({ value }: MetricChangeProps) {\n  if (value === null) {\n    return null\n  }\n\n  const percent = Math.round(value * 10) / 10\n  const Icon = percent > 0 ? TrendingUpIcon : TrendingDownIcon\n\n  return (\n    <span\n      data-slot=\"metric-change\"\n      className={cn(\n        \"flex shrink-0 items-center gap-0.5 text-xs/4 tabular-nums\",\n        // Shades differ per color scheme so each clears 4.5:1 on its background.\n        percent > 0 && \"text-green-700 dark:text-green-500\",\n        percent < 0 && \"text-red-700 dark:text-red-400\"\n      )}\n    >\n      {percent !== 0 && (\n        <>\n          <Icon className=\"size-3.5\" aria-hidden />\n          <span className=\"sr-only\">{percent > 0 ? \"Up by \" : \"Down by \"}</span>\n        </>\n      )}\n      {Math.abs(percent).toLocaleString(\"en-US\")}%\n      <span className=\"sr-only\"> compared to the previous period</span>\n    </span>\n  )\n}\n\nexport function MetricValue({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <dd\n      data-slot=\"metric-value\"\n      className={cn(\n        \"text-lg leading-none font-semibold tabular-nums\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n",
      "type": "registry:component",
      "target": "@components/metric.tsx"
    }
  ],
  "meta": {
    "createdAt": "2026-05-24",
    "previewClassName": "min-h-svh content-center-safe"
  },
  "categories": [
    "marketing"
  ],
  "type": "registry:block"
}