# Analytics overview

> A complete acquisition report with period comparisons, traffic sources, conversion stages, and CSV export.

A shadcn/ui block from the [@overtrue](https://ui.overtrue.xyz/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/overtrue/analytics-overview).

- Page, with a live preview: https://shadcn.exchange/r/overtrue/analytics-overview
- Screenshot: https://shadcn.exchange/shot/overtrue/analytics-overview/light.png
- Registry item: https://ui.overtrue.xyz/r/analytics-overview.json
- Categories: blocks
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/overtrue/analytics-overview.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`, `recharts`
- Registry items: `card`, `button`

## Notes from the registry

Documentation: https://ui.overtrue.xyz/blocks/analytics-overview. Requires an initialized shadcn/ui project with Tailwind CSS v4.

## Files

### `src/registry/overtrue/analytics-overview.tsx`

```tsx
"use client";
import { useId, useState } from "react";
import {
  IconArrowDownRight,
  IconArrowUpRight,
  IconDownload,
  IconChevronDown,
  IconEye,
  IconTarget,
  IconUserPlus,
} from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { PageHeader } from "./page-header";
import { StatCard } from "./stat-card";
import { MetricChart, type MetricPoint } from "./metric-chart";
import { RankedList, type RankedListItem } from "./ranked-list";
import { ConversionFunnel, type FunnelStage } from "./conversion-funnel";
import { cn } from "@/lib/utils";

export interface AnalyticsReport {
  id: string;
  label: string;
  description: string;
  traffic: MetricPoint[];
  sources: RankedListItem[];
  /** Ordered from sessions to conversions; the first stage should match traffic. */
  stages: FunnelStage[];
  previousConversions: number;
}

export const analyticsReports: AnalyticsReport[] = [
  {
    id: "30-days",
    label: "Last 30 days",
    description: "August 23 – September 21, 2026",
    traffic: [
      { label: "Aug 23", value: 4280, previous: 3840 },
      { label: "Aug 28", value: 5120, previous: 4480 },
      { label: "Sep 2", value: 4680, previous: 4120 },
      { label: "Sep 7", value: 5640, previous: 4920 },
      { label: "Sep 12", value: 6240, previous: 5280 },
      { label: "Sep 17", value: 6840, previous: 5860 },
    ],
    sources: [
      { id: "search", label: "Organic search", value: 14432 },
      { id: "direct", label: "Direct", value: 8200 },
      { id: "referral", label: "Referrals", value: 5576 },
      { id: "social", label: "Social", value: 3280 },
      { id: "email", label: "Email", value: 1312 },
    ],
    stages: [
      { id: "sessions", label: "Sessions", value: 32800 },
      { id: "pricing", label: "Viewed pricing", value: 8200 },
      { id: "trial", label: "Started a trial", value: 1640 },
      { id: "subscribed", label: "Subscribed", value: 492 },
    ],
    previousConversions: 342,
  },
  {
    id: "7-days",
    label: "Last 7 days",
    description: "September 15 – 21, 2026",
    traffic: [
      { label: "Tue", value: 980, previous: 860 },
      { label: "Wed", value: 1120, previous: 940 },
      { label: "Thu", value: 1050, previous: 1080 },
      { label: "Fri", value: 1360, previous: 1120 },
      { label: "Sat", value: 1240, previous: 1060 },
      { label: "Sun", value: 1480, previous: 1260 },
      { label: "Mon", value: 1570, previous: 1380 },
    ],
    sources: [
      { id: "search", label: "Organic search", value: 3784 },
      { id: "direct", label: "Direct", value: 2200 },
      { id: "referral", label: "Referrals", value: 1584 },
      { id: "social", label: "Social", value: 880 },
      { id: "email", label: "Email", value: 352 },
    ],
    stages: [
      { id: "sessions", label: "Sessions", value: 8800 },
      { id: "pricing", label: "Viewed pricing", value: 2376 },
      { id: "trial", label: "Started a trial", value: 528 },
      { id: "subscribed", label: "Subscribed", value: 176 },
    ],
    previousConversions: 139,
  },
];

const sum = (values: number[]) =>
  values.reduce(
    (total, value) => total + (Number.isFinite(value) ? Math.max(0, value) : 0),
    0,
  );
function comparison(current: number, previous: number) {
  const delta =
    previous > 0 ? ((current - previous) / previous) * 100 : undefined;
  return {
    change: delta === undefined ? undefined : `${Math.abs(delta).toFixed(1)}%`,
    trend:
      delta === undefined || delta === 0
        ? ("neutral" as const)
        : delta > 0
          ? ("up" as const)
          : ("down" as const),
  };
}

/** A complete demo by default; pass reports from your own analytics endpoint. */
export function AnalyticsOverview({
  reports = analyticsReports,
  className,
}: {
  reports?: AnalyticsReport[];
  className?: string;
}) {
  const periodId = useId();
  const [selected, setSelected] = useState(reports[0]?.id);
  const report = reports.find((item) => item.id === selected) ?? reports[0];
  const sessions = sum(report?.traffic.map((point) => point.value) ?? []);
  const previousSessions = sum(
    report?.traffic.map((point) => point.previous ?? 0) ?? [],
  );
  const conversions = sum([report?.stages.at(-1)?.value ?? 0]);
  const previousConversions = sum([report?.previousConversions ?? 0]);
  const conversion = sessions ? (conversions / sessions) * 100 : 0;
  const previousConversion = previousSessions
    ? (previousConversions / previousSessions) * 100
    : 0;
  const hasComparison = Boolean(
    report?.traffic.length &&
    report.traffic.every((point) => Number.isFinite(point.previous)),
  );
  const improved = conversion >= previousConversion;
  const TrendIcon = improved ? IconArrowUpRight : IconArrowDownRight;

  function exportReport() {
    if (!report) return;
    const rows = [
      ["Section", "Label", "Current", "Previous"],
      ...report.traffic.map((point) => [
        "Traffic",
        point.label,
        point.value,
        point.previous ?? "",
      ]),
      ...report.sources.map((source) => [
        "Source",
        source.label,
        source.value,
        "",
      ]),
      ...report.stages.map((stage) => ["Funnel", stage.label, stage.value, ""]),
    ];
    const csv = rows
      .map((row) =>
        row
          .map((value) => {
            const text = String(value);
            return `"${(/^[=+\-@\t\r]/.test(text) ? "'" + text : text).replaceAll('"', '""')}"`;
          })
          .join(","),
      )
      .join("\r\n");
    const url = URL.createObjectURL(
      new Blob([csv], { type: "text/csv;charset=utf-8" }),
    );
    const link = document.createElement("a");
    link.href = url;
    link.download = "analytics-report.csv";
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }

  return (
    <section
      data-slot="analytics-overview"
      aria-label="Acquisition analytics"
      className={cn(
        "@container min-w-0 space-y-5 text-foreground",
        className,
      )}
    >
      <PageHeader
        eyebrow="Analytics"
        title="Acquisition overview"
        description={
          report?.description ?? "Connect your analytics to get started."
        }
        actions={
          <>
            <label className="sr-only" htmlFor={periodId}>
              Analytics period
            </label>
            <div className="relative">
              <select
                id={periodId}
                value={report?.id ?? ""}
                disabled={!reports.length}
                onChange={(event) => setSelected(event.target.value)}
                className="h-8 max-w-full appearance-none rounded-md border bg-card py-0 pl-3 pr-8 text-xs font-medium leading-8 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
              >
                {!reports.length && <option value="">No reports</option>}
                {reports.map((item) => (
                  <option key={item.id} value={item.id}>
                    {item.label}
                  </option>
                ))}
              </select>
              <IconChevronDown
                aria-hidden="true"
                className="pointer-events-none absolute top-1/2 right-2.5 size-3.5 -translate-y-1/2 text-muted-foreground"
                stroke={1.7}
              />
            </div>
            <Button
              size="sm"
              variant="outline"
              className="h-8 px-3 text-xs font-medium shadow-none"
              disabled={!report}
              onClick={exportReport}
            >
              <IconDownload aria-hidden="true" className="size-3.5" />
              Export
            </Button>
          </>
        }
      />
      <div className="grid gap-4 @min-[36rem]:grid-cols-3">
        <StatCard
          title="Sessions"
          value={sessions}
          icon={<IconEye />}
          {...comparison(sessions, hasComparison ? previousSessions : 0)}
          description={
            hasComparison && previousSessions
              ? "vs. previous period"
              : "No previous comparison"
          }
        />
        <StatCard
          title="New subscriptions"
          value={conversions}
          icon={<IconUserPlus />}
          {...comparison(conversions, hasComparison ? previousConversions : 0)}
          description={
            hasComparison && previousConversions
              ? "vs. previous period"
              : "No previous comparison"
          }
        />
        <StatCard
          title="Conversion rate"
          value={sessions ? `${conversion.toFixed(2)}%` : "—"}
          icon={<IconTarget />}
          {...comparison(conversion, hasComparison ? previousConversion : 0)}
          description={
            hasComparison && previousConversion
              ? "vs. previous period"
              : "Subscriptions / sessions"
          }
        />
      </div>
      <MetricChart
        title="Traffic over time"
        seriesLabel="Sessions"
        comparisonLabel="Previous period"
        description={
          report?.label === "Last 30 days"
            ? "Sessions grouped into five-day intervals"
            : report?.label
        }
        data={report?.traffic ?? []}
        height={250}
      />
      <div className="grid gap-4 @min-[42rem]:grid-cols-2">
        <Card className="min-w-0 gap-0 rounded-lg bg-card py-0 shadow-none">
          <CardHeader className="flex flex-col items-start gap-1 border-b px-5 py-4 [.border-b]:pb-4">
            <CardTitle className="text-sm font-semibold">
              Acquisition sources
            </CardTitle>
            <p className="mt-1 text-xs text-muted-foreground">
              Sessions and share of tracked traffic
            </p>
          </CardHeader>
          <CardContent className="p-5">
            <RankedList
              label="Acquisition sources"
              items={report?.sources ?? []}
            />
            <p className="mt-4 text-xs leading-5 text-muted-foreground">
              {report?.sources.length
                ? `Tracked across ${report.sources.length} acquisition channels.`
                : "Sources will appear when traffic is recorded."}
            </p>
          </CardContent>
        </Card>
        <Card className="min-w-0 gap-0 rounded-lg bg-card py-0 shadow-none">
          <CardHeader className="flex flex-col items-start gap-1 border-b px-5 py-4 [.border-b]:pb-4">
            <CardTitle className="text-sm font-semibold">
              From visit to subscription
            </CardTitle>
            <p className="mt-1 text-xs text-muted-foreground">
              Follow the journey through each step
            </p>
          </CardHeader>
          <CardContent className="p-5">
            <ConversionFunnel
              label="Subscription conversion"
              stages={report?.stages ?? []}
            />
          </CardContent>
        </Card>
      </div>
      {sessions > 0 && hasComparison && previousSessions > 0 && (
        <div className="flex items-start gap-3 rounded-lg border bg-card px-4 py-3 text-sm">
          <TrendIcon
            aria-hidden="true"
            className={cn(
              "mt-0.5 size-4 shrink-0",
              improved
                ? "text-emerald-700 dark:text-emerald-400"
                : "text-destructive",
            )}
          />
          <p className="m-0 leading-6 text-muted-foreground">
            <span className="font-medium text-foreground">
              {conversion === previousConversion
                ? "Conversion is steady."
                : improved
                  ? "Conversion is improving."
                  : "Conversion needs attention."}
            </span>{" "}
            {conversions.toLocaleString("en-US")} sessions became subscriptions,
            a {conversion.toFixed(2)}% conversion rate
            {conversion === previousConversion
              ? "."
              : ` — ${Math.abs(conversion - previousConversion).toFixed(2)} percentage points ${improved ? "above" : "below"} the previous period.`}
          </p>
        </div>
      )}
    </section>
  );
}
```

### `src/registry/overtrue/page-header.tsx`

```tsx
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

export function PageHeader({
  eyebrow,
  title,
  description,
  actions,
  className,
}: {
  eyebrow?: string;
  title: string;
  description?: string;
  actions?: ReactNode;
  className?: string;
}) {
  return (
    <header
      data-slot="page-header"
      className={cn(
        "flex flex-wrap items-end justify-between gap-x-6 gap-y-4",
        className,
      )}
    >
      <div
        data-slot="page-header-heading"
        className="min-w-0 transition-[opacity,translate] duration-700 ease-[cubic-bezier(0.22,1,0.36,1)] motion-safe:starting:translate-y-1 motion-safe:starting:opacity-0 motion-reduce:transition-none"
      >
        {eyebrow && (
          <p
            data-slot="page-header-eyebrow"
            className="m-0 mb-2 flex items-center gap-2 text-[13px] leading-5 font-medium text-muted-foreground"
          >
            <span
              aria-hidden="true"
              className="size-1.5 shrink-0 rounded-full bg-primary shadow-[0_0_0_3px_color-mix(in_oklab,var(--color-primary)_16%,transparent)] dark:shadow-[0_0_0_3px_color-mix(in_oklab,var(--color-primary)_22%,transparent),0_0_10px_color-mix(in_oklab,var(--color-primary)_60%,transparent)]"
            />
            {eyebrow}
          </p>
        )}
        <h1 className="m-0 text-2xl leading-8 font-semibold tracking-[-0.025em] text-balance text-foreground">
          {title}
        </h1>
        {description && (
          <p className="m-0 mt-1.5 max-w-3xl text-sm leading-6 text-pretty text-muted-foreground">
            {description}
          </p>
        )}
      </div>
      {actions && (
        <div
          data-slot="page-header-actions"
          className="flex flex-wrap items-center gap-2"
        >
          {actions}
        </div>
      )}
    </header>
  );
}
```

### `src/registry/overtrue/stat-card.tsx`

```tsx
"use client";
import type { ReactNode } from "react";
import {
  IconArrowDownRight as ArrowDownRight,
  IconArrowUpRight as ArrowUpRight,
  IconMinus as Minus,
} from "@tabler/icons-react";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { Sparkline } from "./sparkline";

export interface StatCardProps {
  title: string;
  value: string | number;
  change?: string;
  trend?: "up" | "down" | "neutral";
  /** A decrease is positive for metrics such as latency or churn. */
  positiveTrend?: "up" | "down";
  sparkline?: number[];
  icon?: ReactNode;
  description?: string;
  className?: string;
}

// Currency symbols and units sit quieter than the digits; "Q3 2026" stays untouched.
const quiet = "font-medium tracking-normal text-current/50";
const unitSize = "text-[0.6em]";
const symbolSize = "mr-[0.04em] text-[0.62em] align-[0.4em]";
function typeset(text: string) {
  const match =
    /^([+\-−]?)([^\d\s+\-−]{0,3})(\d(?:[\d.,  ]*\d)?[KkMBT]?)(%|\s+\S.*)?$/.exec(
      text,
    );
  if (!match || /[a-z]/i.test(match[2].replace(/[A-Z]{1,2}(?=\$)/, "")))
    return text;
  const [, sign, symbol, digits, unit] = match;
  return (
    <>
      {sign}
      {symbol ? <span className={cn(quiet, symbolSize)}>{symbol}</span> : null}
      {digits}
      {unit ? <span className={cn(quiet, unitSize)}>{unit}</span> : null}
    </>
  );
}

// Same tones as DeltaBadge, so a change reads the same everywhere.
const toneText = {
  positive: "text-emerald-700 dark:text-emerald-400",
  negative: "text-rose-700 dark:text-rose-400",
  neutral: "text-muted-foreground",
};
const tonePill = {
  positive: "bg-emerald-500/10 dark:bg-emerald-400/10",
  negative: "bg-rose-500/10 dark:bg-rose-400/10",
  neutral: "bg-muted",
};
const toneLine = {
  positive: "text-emerald-600 dark:text-emerald-400",
  negative: "text-rose-600 dark:text-rose-400",
  neutral: "text-muted-foreground",
};

export function StatCard({
  title,
  value,
  change,
  trend = "neutral",
  positiveTrend = "up",
  sparkline,
  icon,
  description,
  className,
}: StatCardProps) {
  const Icon =
    trend === "up" ? ArrowUpRight : trend === "down" ? ArrowDownRight : Minus;
  const tone =
    trend === "neutral"
      ? "neutral"
      : trend === positiveTrend
        ? "positive"
        : "negative";
  return (
    <Card
      data-slot="stat-card"
      data-trend={trend}
      data-tone={tone}
      className={cn(
        "min-w-0 gap-0 rounded-2xl border border-border/70 bg-card py-0 text-card-foreground ring-0 shadow-[0_1px_2px_rgb(0_0_0/0.04),0_8px_24px_-12px_rgb(0_0_0/0.10)] dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.05),0_1px_2px_rgb(0_0_0/0.5)]",
        className,
      )}
    >
      <CardContent className="p-5">
        <p
          data-slot="stat-card-title"
          className="flex min-h-7 items-center justify-between gap-3 text-sm leading-5 font-medium text-muted-foreground"
        >
          {title}
          {icon && (
            <span
              aria-hidden="true"
              className="grid size-7 shrink-0 place-items-center rounded-lg bg-muted/60 text-muted-foreground ring-1 ring-border/60 ring-inset [&>svg]:size-4"
            >
              {icon}
            </span>
          )}
        </p>
        <p
          data-slot="stat-card-value"
          className="mt-2 text-3xl leading-9 font-semibold tracking-[-0.035em] wrap-anywhere text-card-foreground tabular-nums"
        >
          {typeset(
            typeof value === "number" ? value.toLocaleString("en-US") : value,
          )}
        </p>
        {sparkline && (
          <Sparkline
            data={sparkline}
            label={`${title} trend`}
            className={cn("mt-4", toneLine[tone])}
          />
        )}
        {(change || description) && (
          <div className="mt-3 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs">
            {change && (
              <span
                data-slot="stat-card-change"
                className={cn(
                  "inline-flex h-5 items-center gap-0.5 rounded-full pr-1.5 pl-1 leading-none font-medium tracking-[-0.01em] tabular-nums",
                  toneText[tone],
                  tonePill[tone],
                )}
              >
                <Icon
                  className="size-3 shrink-0"
                  aria-hidden="true"
                  stroke={2.5}
                />
                <span className="sr-only">
                  {trend === "up"
                    ? "Increase"
                    : trend === "down"
                      ? "Decrease"
                      : "No change"}
                  {tone !== "neutral" ? ` (${tone})` : ""}:{" "}
                </span>
                {change}
              </span>
            )}
            <span className="text-muted-foreground">
              {description ?? "vs. last month"}
            </span>
          </div>
        )}
      </CardContent>
    </Card>
  );
}
```

### `src/registry/overtrue/sparkline.tsx`

```tsx
"use client";
import { useId, type CSSProperties } from "react";
import { cn } from "@/lib/utils";

export interface SparklineProps {
  data: number[];
  label: string;
  className?: string;
  filled?: boolean;
  /** Mark the most recent value. */
  endpoint?: boolean;
  /** Any CSS color. Defaults to the current text color, which is primary. */
  color?: string;
  /** Draw the line in and raise the area when the sparkline mounts. Skipped under reduced motion. */
  animate?: boolean;
}

const WIDTH = 240;
const HEIGHT = 48;
const PAD_X = 3;
const PAD_Y = 5;
const EASE = "ease-[cubic-bezier(0.22,1,0.36,1)]";

/** Monotone cubic path: smooth, but never overshoots the data. */
function monotonePath(points: [number, number][]) {
  const n = points.length;
  if (n < 2) return "";
  const dx: number[] = [];
  const slope: number[] = [];
  for (let i = 0; i < n - 1; i++) {
    dx.push(points[i + 1][0] - points[i][0]);
    slope.push((points[i + 1][1] - points[i][1]) / dx[i]);
  }
  const tangent = [slope[0]];
  for (let i = 1; i < n - 1; i++) {
    tangent.push(
      slope[i - 1] * slope[i] <= 0 ? 0 : (slope[i - 1] + slope[i]) / 2,
    );
  }
  tangent.push(slope[n - 2]);
  for (let i = 0; i < n - 1; i++) {
    if (slope[i] === 0) {
      tangent[i] = tangent[i + 1] = 0;
      continue;
    }
    const a = tangent[i] / slope[i];
    const b = tangent[i + 1] / slope[i];
    const h = a * a + b * b;
    if (h > 9) {
      const t = 3 / Math.sqrt(h);
      tangent[i] = t * a * slope[i];
      tangent[i + 1] = t * b * slope[i];
    }
  }
  const r = (value: number) => Math.round(value * 100) / 100;
  let d = `M${r(points[0][0])},${r(points[0][1])}`;
  for (let i = 0; i < n - 1; i++) {
    const [x0, y0] = points[i];
    const [x1, y1] = points[i + 1];
    const h = dx[i] / 3;
    d += `C${r(x0 + h)},${r(y0 + tangent[i] * h)} ${r(x1 - h)},${r(y1 - tangent[i + 1] * h)} ${r(x1)},${r(y1)}`;
  }
  return d;
}

/** A compact overview. Use a full chart when individual values need inspection. */
export function Sparkline({
  data,
  label,
  className,
  filled = true,
  endpoint = true,
  color,
  animate = true,
}: SparklineProps) {
  const gradient = `sparkline-${useId().replace(/:/g, "")}`;
  const values = data.filter(Number.isFinite);
  let low = Infinity,
    high = -Infinity;
  for (const value of values) {
    low = Math.min(low, value);
    high = Math.max(high, value);
  }
  const points = values.map((value, i): [number, number] => [
    values.length > 1
      ? PAD_X + (i / (values.length - 1)) * (WIDTH - PAD_X * 2)
      : WIDTH / 2,
    high === low
      ? HEIGHT / 2
      : HEIGHT - PAD_Y - ((value - low) / (high - low)) * (HEIGHT - PAD_Y * 2),
  ]);
  const line = monotonePath(points);
  const last = points[points.length - 1];
  const lastValue = values[values.length - 1];
  const motion = animate ? "motion-reduce:transition-none" : "transition-none";
  return (
    <div
      data-slot="sparkline"
      role="img"
      aria-label={`${label}: ${values.length ? values.join(", ") : "No data"}`}
      className={cn("relative h-12 w-full text-primary", className)}
      style={color ? { color } : undefined}
    >
      {values.length > 1 && filled && (
        <svg
          viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
          preserveAspectRatio="none"
          aria-hidden="true"
          data-slot="sparkline-area"
          className={cn(
            "absolute inset-0 size-full origin-bottom overflow-visible transition-[opacity,scale] delay-75 duration-700",
            EASE,
            animate && "starting:scale-y-0 starting:opacity-0",
            motion,
          )}
        >
          <defs>
            <linearGradient id={gradient} x1="0" y1="0" x2="0" y2="1">
              <stop offset="0%" stopColor="currentColor" stopOpacity="0.28" />
              <stop offset="100%" stopColor="currentColor" stopOpacity="0" />
            </linearGradient>
          </defs>
          <path
            d={`${line}L${last[0]},${HEIGHT}L${points[0][0]},${HEIGHT}Z`}
            fill={`url(#${gradient})`}
            className={cn("transition-[d] duration-500", EASE, motion)}
          />
        </svg>
      )}
      <svg
        viewBox={`0 0 ${WIDTH} ${HEIGHT}`}
        preserveAspectRatio="none"
        aria-hidden="true"
        data-slot="sparkline-line"
        className={cn(
          // The line wipes in from the left; the inset leaves room for its glow.
          "absolute inset-0 size-full overflow-visible [clip-path:inset(-60%_-8%_-60%_-8%)] transition-[clip-path] duration-700",
          EASE,
          animate &&
            values.length > 1 &&
            "starting:[clip-path:inset(-60%_100%_-60%_-8%)]",
          motion,
        )}
      >
        {values.length > 1 ? (
          <path
            d={line}
            fill="none"
            stroke="currentColor"
            strokeWidth="2"
            strokeLinejoin="round"
            strokeLinecap="round"
            vectorEffect="non-scaling-stroke"
            className={cn(
              "transition-[d] duration-500 dark:[filter:drop-shadow(0_0_6px_color-mix(in_oklab,currentColor_50%,transparent))]",
              EASE,
              motion,
            )}
          />
        ) : values.length === 0 ? (
          <path
            d={`M${PAD_X} ${HEIGHT / 2}H${WIDTH - PAD_X}`}
            stroke="currentColor"
            strokeDasharray="2 5"
            strokeLinecap="round"
            opacity="0.3"
            vectorEffect="non-scaling-stroke"
          />
        ) : null}
      </svg>
      {/* Drawn in HTML so the marker stays round when the line stretches. */}
      {endpoint && last && (
        <span
          aria-hidden="true"
          data-slot="sparkline-endpoint"
          className={cn(
            // Moves with the line, and on mount appears once the line has drawn in.
            "absolute -translate-x-1/2 -translate-y-1/2 [transition:left_500ms_cubic-bezier(0.22,1,0.36,1),top_500ms_cubic-bezier(0.22,1,0.36,1),opacity_300ms_ease-out_550ms,scale_500ms_cubic-bezier(0.22,1,0.36,1)_550ms]",
            animate && "starting:scale-0 starting:opacity-0",
            motion,
          )}
          style={
            {
              left: `${(last[0] / WIDTH) * 100}%`,
              top: `${(last[1] / HEIGHT) * 100}%`,
            } as CSSProperties
          }
        >
          {/* Pulses once whenever the latest value changes. */}
          <span
            key={lastValue}
            className={cn(
              "absolute inset-0 scale-[3.2] rounded-full bg-current opacity-0 transition-[opacity,scale] duration-1000 ease-out starting:scale-100 starting:opacity-40 motion-reduce:hidden",
              !animate && "hidden",
            )}
          />
          <span className="relative block size-2 rounded-full border-[1.5px] border-card bg-current shadow-[0_0_0_3px_color-mix(in_oklab,currentColor_18%,transparent)] dark:shadow-[0_0_0_3px_color-mix(in_oklab,currentColor_24%,transparent),0_0_12px_1px_color-mix(in_oklab,currentColor_70%,transparent)]" />
        </span>
      )}
    </div>
  );
}
```

### `src/registry/overtrue/metric-chart.tsx`

```tsx
"use client";
import { useId, useSyncExternalStore, type ReactNode } from "react";
import { ChartLegend } from "./chart-legend";
import {
  Area,
  AreaChart,
  CartesianGrid,
  ResponsiveContainer,
  Tooltip,
  XAxis,
  YAxis,
} from "recharts";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export interface MetricPoint {
  label: string;
  value: number;
  previous?: number;
}

const reducedMotionQuery = "(prefers-reduced-motion: reduce)";
function subscribeToReducedMotion(onChange: () => void) {
  const query = window.matchMedia(reducedMotionQuery);
  query.addEventListener("change", onChange);
  return () => query.removeEventListener("change", onChange);
}
function getReducedMotionSnapshot() {
  return window.matchMedia(reducedMotionQuery).matches;
}
/** The server snapshot disables motion so hydration starts from identical markup. */
function getReducedMotionServerSnapshot() {
  return true;
}

const PRIMARY = "var(--color-primary)";
const COMPARISON = "var(--color-muted-foreground)";
const GRID = "color-mix(in oklab, var(--color-foreground) 7%, transparent)";
const CROSSHAIR =
  "color-mix(in oklab, var(--color-foreground) 25%, transparent)";
const TICK = { fontSize: 11, fill: "var(--color-muted-foreground)" };
/** react-smooth accepts any cubic-bezier() string; the Recharts type lists only keywords. */
const EASE = "cubic-bezier(0.22,1,0.36,1)" as "ease-out";

function HaloDot({
  cx,
  cy,
  color,
}: {
  cx?: number;
  cy?: number;
  color: string;
}) {
  if (cx == null || cy == null) return <g />;
  return (
    <g pointerEvents="none">
      <circle cx={cx} cy={cy} r={8} fill={color} fillOpacity={0.18} />
      <circle
        cx={cx}
        cy={cy}
        r={3.5}
        fill={color}
        stroke="var(--color-card)"
        strokeWidth={1.5}
      />
    </g>
  );
}

export function MetricChart({
  title = "Revenue overview",
  data,
  height = 240,
  seriesLabel = "Revenue",
  comparisonLabel = "Previous period",
  valueFormatter = (value: number) => value.toLocaleString("en-US"),
  axisFormatter = (value: number) =>
    value.toLocaleString("en-US", {
      notation: "compact",
      maximumFractionDigits: 1,
    }),
  description,
  actions,
}: {
  title?: string;
  data: MetricPoint[];
  height?: number;
  seriesLabel?: string;
  comparisonLabel?: string;
  valueFormatter?: (value: number) => string;
  axisFormatter?: (value: number) => string;
  description?: string;
  actions?: ReactNode;
}) {
  const gradient = useId().replace(/:/g, "");
  const reducedMotion = useSyncExternalStore(
    subscribeToReducedMotion,
    getReducedMotionSnapshot,
    getReducedMotionServerSnapshot,
  );
  const points = data
    .filter((point) => Number.isFinite(point.value))
    .map((point) => ({
      ...point,
      previous: Number.isFinite(point.previous) ? point.previous : undefined,
    }));
  const hasComparison = points.some((point) => point.previous !== undefined);
  const chartHeight = Number.isFinite(height) ? Math.max(120, height) : 240;
  const last = points.length - 1;
  return (
    <Card
      data-slot="metric-chart"
      className="min-w-0 gap-0 rounded-2xl border border-border/70 bg-card py-0 text-card-foreground shadow-[0_1px_2px_rgb(0_0_0/0.04),0_8px_24px_-12px_rgb(0_0_0/0.10)] dark:shadow-[inset_0_1px_0_rgb(255_255_255/0.05),0_1px_2px_rgb(0_0_0/0.5)]"
    >
      <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-3 border-b border-border/60 px-5 py-4 [.border-b]:pb-4">
        <div className="min-w-0">
          <CardTitle className="text-sm font-semibold tracking-tight">
            {title}
          </CardTitle>
          {description && (
            <p className="mt-1 text-xs leading-5 text-muted-foreground">
              {description}
            </p>
          )}
        </div>
        {actions}
        <ChartLegend
          layout="horizontal"
          items={[
            { label: seriesLabel, color: PRIMARY },
            ...(hasComparison
              ? [
                  {
                    label: comparisonLabel,
                    color: COMPARISON,
                    dashed: true,
                  },
                ]
              : []),
          ]}
        />
      </CardHeader>
      <CardContent className="p-5 pb-3">
        {!points.length ? (
          <div
            className="flex items-center justify-center text-sm text-muted-foreground"
            style={{ height: chartHeight }}
          >
            No data for this period.
          </div>
        ) : (
          <div
            role="img"
            aria-label={`${title}: ${points.map((point) => `${point.label}: ${seriesLabel} ${valueFormatter(point.value)}${point.previous !== undefined ? `, ${comparisonLabel} ${valueFormatter(point.previous)}` : ""}`).join("; ")}`}
            data-slot="metric-chart-plot"
            className="tabular-nums dark:[&_.recharts-area-curve]:[filter:drop-shadow(0_0_6px_color-mix(in_oklab,currentColor_50%,transparent))]"
            style={{ height: chartHeight }}
          >
            <ResponsiveContainer width="100%" height="100%">
              <AreaChart
                data={points}
                margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
              >
                <defs>
                  <linearGradient id={gradient} x1="0" y1="0" x2="0" y2="1">
                    <stop offset="0%" stopColor={PRIMARY} stopOpacity={0.32} />
                    <stop
                      offset="55%"
                      stopColor={PRIMARY}
                      stopOpacity={0.11}
                    />
                    <stop offset="100%" stopColor={PRIMARY} stopOpacity={0} />
                  </linearGradient>
                </defs>
                <CartesianGrid vertical={false} stroke={GRID} />
                <XAxis
                  dataKey="label"
                  tickLine={false}
                  axisLine={false}
                  tick={TICK}
                  minTickGap={24}
                  dy={8}
                />
                <YAxis
                  width={48}
                  tickFormatter={axisFormatter}
                  tickLine={false}
                  axisLine={false}
                  tick={TICK}
                />
                <Tooltip
                  isAnimationActive={false}
                  cursor={{ stroke: CROSSHAIR, strokeWidth: 1 }}
                  content={({ active, payload, label }) => {
                    const row = payload?.[0]?.payload as
                      | (MetricPoint & { previous?: number })
                      | undefined;
                    if (!active || !row) return null;
                    const change =
                      row.previous !== undefined && row.previous !== 0
                        ? (row.value - row.previous) / Math.abs(row.previous)
                        : null;
                    return (
                      <div
                        data-slot="metric-chart-tooltip"
                        className="min-w-44 rounded-xl border border-border/70 bg-popover/90 px-3 py-2.5 text-xs text-popover-foreground shadow-xl backdrop-blur-md"
                      >
                        <p className="mb-1.5 font-medium text-foreground">
                          {label}
                        </p>
                        <div className="grid gap-1">
                          <div className="flex items-center gap-2">
                            <span
                              aria-hidden="true"
                              className="size-2 shrink-0 rounded-full"
                              style={{ backgroundColor: PRIMARY }}
                            />
                            <span className="text-muted-foreground">
                              {seriesLabel}
                            </span>
                            <span className="ml-auto pl-4 font-medium text-foreground tabular-nums">
                              {valueFormatter(row.value)}
                            </span>
                          </div>
                          {row.previous !== undefined && (
                            <div className="flex items-center gap-2">
                              <span
                                aria-hidden="true"
                                className="w-2 shrink-0 border-t-2 border-dashed"
                                style={{ borderColor: COMPARISON }}
                              />
                              <span className="text-muted-foreground">
                                {comparisonLabel}
                              </span>
                              <span className="ml-auto pl-4 font-medium text-foreground tabular-nums">
                                {valueFormatter(row.previous)}
                              </span>
                            </div>
                          )}
                        </div>
                        {change !== null && Number.isFinite(change) && (
                          <p className="mt-1.5 border-t border-border/60 pt-1.5 text-muted-foreground tabular-nums">
                            <span className="font-medium text-foreground">
                              {change > 0 ? "+" : change < 0 ? "−" : ""}
                              {Math.abs(change * 100).toFixed(1)}%
                            </span>{" "}
                            vs. {comparisonLabel.toLowerCase()}
                          </p>
                        )}
                      </div>
                    );
                  }}
                />
                {hasComparison && (
                  <Area
                    type="monotone"
                    dataKey="previous"
                    name={comparisonLabel}
                    stroke={COMPARISON}
                    strokeOpacity={0.7}
                    strokeWidth={1.5}
                    strokeDasharray="4 4"
                    fill="none"
                    activeDot={(props: { cx?: number; cy?: number }) => (
                      <circle
                        cx={props.cx}
                        cy={props.cy}
                        r={3}
                        fill="var(--color-card)"
                        stroke={COMPARISON}
                        strokeWidth={1.5}
                      />
                    )}
                    isAnimationActive={!reducedMotion}
                    animationDuration={700}
                    animationEasing={EASE}
                  />
                )}
                <Area
                  type="monotone"
                  dataKey="value"
                  name={seriesLabel}
                  stroke={PRIMARY}
                  color={PRIMARY}
                  strokeWidth={2}
                  strokeLinecap="round"
                  strokeLinejoin="round"
                  fill={`url(#${gradient})`}
                  dot={(props: { cx?: number; cy?: number; index?: number }) =>
                    props.index === last ? (
                      <HaloDot
                        key="metric-chart-latest"
                        cx={props.cx}
                        cy={props.cy}
                        color={PRIMARY}
                      />
                    ) : (
                      <g key={`metric-chart-dot-${props.index}`} />
                    )
                  }
                  activeDot={(props: { cx?: number; cy?: number }) => (
                    <HaloDot cx={props.cx} cy={props.cy} color={PRIMARY} />
                  )}
                  isAnimationActive={!reducedMotion}
                  animationDuration={700}
                  animationEasing={EASE}
                />
              </AreaChart>
            </ResponsiveContainer>
          </div>
        )}
      </CardContent>
    </Card>
  );
}
```

### `src/registry/overtrue/chart-legend.tsx`

```tsx
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";

export interface ChartLegendItem {
  label: string;
  color: string;
  value?: ReactNode;
  dashed?: boolean;
}
export function ChartLegend({
  items,
  label = "Chart legend",
  layout = "vertical",
  className,
}: {
  items: readonly ChartLegendItem[];
  label?: string;
  layout?: "horizontal" | "vertical";
  className?: string;
}) {
  return (
    <ul
      data-slot="chart-legend"
      data-layout={layout}
      aria-label={label}
      className={cn(
        "m-0 list-none p-0 text-xs",
        layout === "horizontal"
          ? "flex flex-wrap items-center gap-x-4 gap-y-2"
          : "grid gap-3",
        className,
      )}
    >
      {items.map((item) => (
        <li
          key={item.label}
          data-slot="chart-legend-item"
          className="flex min-w-0 items-center gap-2"
        >
          {item.dashed ? (
            <svg
              aria-hidden="true"
              data-slot="chart-legend-swatch"
              viewBox="0 0 14 2"
              className="h-0.5 w-3.5 shrink-0 overflow-visible"
            >
              <line
                x1="1"
                y1="1"
                x2="13"
                y2="1"
                stroke={item.color}
                strokeWidth="2"
                strokeLinecap="round"
                strokeDasharray="2 4"
              />
            </svg>
          ) : (
            <span
              aria-hidden="true"
              data-slot="chart-legend-swatch"
              className="size-2 shrink-0 rounded-full"
              style={{
                backgroundColor: item.color,
                boxShadow: `0 0 0 2px color-mix(in oklab, ${item.color} 16%, transparent)`,
              }}
            />
          )}
          <span className="min-w-0 break-words text-muted-foreground">
            {item.label}
          </span>
          {item.value != null && (
            <span className="ms-auto ps-3 font-medium text-foreground tabular-nums">
              {item.value}
            </span>
          )}
        </li>
      ))}
    </ul>
  );
}
```

### `src/registry/overtrue/ranked-list.tsx`

```tsx
import type { CSSProperties, ReactNode } from "react";
import { IconArrowUpRight } from "@tabler/icons-react";
import { cn } from "@/lib/utils";

export interface RankedListItem {
  id: string;
  label: string;
  value: number;
  icon?: ReactNode;
  href?: string;
}
export interface RankedListProps {
  items: RankedListItem[];
  label: string;
  formatValue?: (value: number) => string;
  emptyMessage?: string;
  className?: string;
  /** Any CSS color for the bars. Defaults to the primary color. */
  color?: string;
  /** Show each row's rank before its label. */
  showRank?: boolean;
}

/**
 * A sorted list of labelled values with a proportional bar behind each row, the value,
 * and its share of the total. Hovering a row brings its bar forward and quiets the rest.
 */
export function RankedList({
  items,
  label,
  formatValue = (value) => value.toLocaleString("en-US"),
  emptyMessage = "No results for this period.",
  className,
  color = "var(--color-primary)",
  showRank = false,
}: RankedListProps) {
  const rows = items
    .map((item) => ({
      ...item,
      value: Number.isFinite(item.value) ? Math.max(0, item.value) : 0,
    }))
    .sort((a, b) => b.value - a.value);
  const max = rows[0]?.value ?? 0;
  const total = rows.reduce((sum, item) => sum + item.value, 0);
  return (
    <ol
      data-slot="ranked-list"
      aria-label={label}
      className={cn(
        "@container/ranking m-0 list-none space-y-2 p-0",
        // While one row is hovered, the other bars step back.
        "[&:has(>li:hover)>li:not(:hover)_[data-slot=ranked-list-bar]]:opacity-45",
        className,
      )}
      style={{ "--ranked-list-color": color } as CSSProperties}
    >
      <style href="overtrue-ranked-list" precedence="default">
        {`@keyframes ranked-list-grow{from{transform:scaleX(0)}}`}
      </style>
      {!rows.length && (
        <li className="py-8 text-center text-sm text-muted-foreground">
          {emptyMessage}
        </li>
      )}
      {rows.map((item, index) => {
        const width = max ? (item.value / max) * 100 : 0;
        const content = (
          <>
            <span
              aria-hidden="true"
              data-slot="ranked-list-bar"
              className={cn(
                "pointer-events-none absolute inset-y-0 left-0 origin-left rounded-lg",
                // The fill deepens toward its end, so its length reads without a hard rule through the label.
                "bg-[linear-gradient(to_right,color-mix(in_oklab,var(--ranked-list-color)_8%,transparent),color-mix(in_oklab,var(--ranked-list-color)_22%,transparent))]",
                "shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--ranked-list-color)_7%,transparent)]",
                "dark:bg-[linear-gradient(to_right,color-mix(in_oklab,var(--ranked-list-color)_8%,transparent),color-mix(in_oklab,var(--ranked-list-color)_28%,transparent))]",
                "dark:shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--ranked-list-color)_10%,transparent),inset_0_1px_0_rgb(255_255_255/0.05)]",
                "transition-[width,opacity,filter] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] group-hover/row:brightness-[1.06] group-hover/row:saturate-150 motion-reduce:transition-none",
                "motion-safe:animate-[ranked-list-grow_700ms_cubic-bezier(0.22,1,0.36,1)_backwards]",
              )}
              style={{
                width: `${width}%`,
                animationDelay: `${index * 40}ms`,
              }}
            />
            <span className="relative flex min-w-0 items-center gap-2">
              {showRank && (
                <span
                  aria-hidden="true"
                  className="w-5 shrink-0 text-xs text-muted-foreground tabular-nums"
                >
                  {index + 1}
                </span>
              )}
              {item.icon && (
                <span
                  aria-hidden="true"
                  className="shrink-0 text-muted-foreground transition-colors duration-150 group-hover/row:text-foreground motion-reduce:transition-none [&>svg]:size-4"
                >
                  {item.icon}
                </span>
              )}
              <span className="min-w-0 wrap-anywhere">{item.label}</span>
              {item.href && (
                <IconArrowUpRight
                  aria-hidden="true"
                  className="size-3.5 shrink-0 text-muted-foreground transition-[translate,color] duration-150 group-hover/row:translate-x-px group-hover/row:-translate-y-px group-hover/row:text-foreground motion-reduce:transition-none"
                />
              )}
            </span>
            <span className="relative flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5 text-right tabular-nums @min-[18rem]/ranking:justify-end">
              <span className="font-medium">{formatValue(item.value)}</span>
              <span className="w-12 text-right text-xs text-muted-foreground">
                {total ? `${((item.value / total) * 100).toFixed(1)}%` : "—"}
                <span className="sr-only"> of total</span>
              </span>
            </span>
          </>
        );
        const rowClass =
          "group/row relative grid grid-cols-1 items-center gap-1.5 rounded-lg px-3 py-3 text-sm text-foreground transition-colors duration-150 hover:bg-muted/50 motion-reduce:transition-none @min-[18rem]/ranking:grid-cols-[minmax(0,1fr)_auto] @min-[18rem]/ranking:gap-3";
        return (
          <li key={item.id}>
            {item.href ? (
              <a
                href={item.href}
                className={cn(
                  rowClass,
                  "outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
                )}
              >
                {content}
              </a>
            ) : (
              <div className={rowClass}>{content}</div>
            )}
          </li>
        );
      })}
    </ol>
  );
}
```

### `src/registry/overtrue/conversion-funnel.tsx`

```tsx
import { cn } from "@/lib/utils";

export interface FunnelStage {
  id: string;
  label: string;
  value: number;
}
export interface ConversionFunnelProps {
  stages: FunnelStage[];
  label: string;
  formatValue?: (value: number) => string;
  className?: string;
}

const keyframes =
  "@keyframes conversion-funnel-grow{from{transform:scaleX(0)}to{transform:scaleX(1)}}";

export function ConversionFunnel({
  stages,
  label,
  formatValue = (value) => value.toLocaleString("en-US"),
  className,
}: ConversionFunnelProps) {
  const rows = stages.map((stage) => ({
    ...stage,
    value: Number.isFinite(stage.value) ? Math.max(0, stage.value) : 0,
  }));
  const first = rows[0]?.value ?? 0;
  const maximum = rows.reduce((max, stage) => Math.max(max, stage.value), 0);
  return (
    <ol
      data-slot="conversion-funnel"
      aria-label={label}
      className={cn("m-0 list-none space-y-5 p-0", className)}
    >
      <style href="overtrue-conversion-funnel" precedence="default">
        {keyframes}
      </style>
      {!rows.length && (
        <li className="py-8 text-center text-sm text-muted-foreground">
          No conversion data for this period.
        </li>
      )}
      {rows.map((stage, index) => {
        const previous = rows[index - 1]?.value;
        const width = maximum ? (stage.value / maximum) * 100 : 0;
        const previousWidth =
          maximum && previous !== undefined ? (previous / maximum) * 100 : 0;
        return (
          <li key={stage.id} data-slot="conversion-funnel-stage">
            <div className="mb-2 flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 text-sm">
              <span className="min-w-0 wrap-anywhere text-foreground">
                <span className="mr-2 font-mono text-xs tabular-nums text-muted-foreground">
                  {String(index + 1).padStart(2, "0")}
                </span>
                {stage.label}
              </span>
              <span className="font-semibold tracking-[-0.01em] tabular-nums text-foreground">
                {formatValue(stage.value)}
              </span>
            </div>
            <div
              aria-hidden="true"
              data-slot="conversion-funnel-track"
              className="relative h-2 overflow-hidden rounded-full bg-muted"
            >
              {previousWidth > width && (
                <div
                  data-slot="conversion-funnel-loss"
                  className="absolute inset-y-0 transition-[left,width] duration-500 motion-reduce:transition-none"
                  style={{
                    left: `${width}%`,
                    width: `${previousWidth - width}%`,
                    backgroundImage:
                      "repeating-linear-gradient(-45deg, color-mix(in oklab, var(--color-primary) 24%, transparent) 0 1px, transparent 1px 4px)",
                  }}
                />
              )}
              <div
                data-slot="conversion-funnel-bar"
                className="relative h-full origin-left rounded-full transition-[width] duration-500 ease-[cubic-bezier(0.22,1,0.36,1)] motion-safe:animate-[conversion-funnel-grow_700ms_cubic-bezier(0.22,1,0.36,1)_both] motion-reduce:transition-none"
                style={{
                  width: `${width}%`,
                  opacity: 1 - Math.min(index, 4) * 0.1,
                  backgroundImage:
                    "linear-gradient(90deg, color-mix(in oklab, var(--color-primary) 72%, transparent), var(--color-primary))",
                  animationDelay: `${Math.min(index, 8) * 60}ms`,
                }}
              />
            </div>
            <p className="mt-1.5 flex flex-wrap justify-between gap-x-3 text-xs tabular-nums text-muted-foreground">
              <span>
                {first
                  ? `${((stage.value / first) * 100).toFixed(1)}% of ${rows[0].label.toLowerCase()}`
                  : "Conversion unavailable"}
              </span>
              {index > 0 && (
                <span>
                  {previous
                    ? `${((stage.value / previous) * 100).toFixed(1)}% from previous`
                    : "No previous entries"}
                </span>
              )}
            </p>
          </li>
        );
      })}
    </ol>
  );
}
```
