# Dashboard

> A working console with metrics, members, activity, and settings.

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/dashboard).

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

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/overtrue/dashboard.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`, `input`, `table`

## Notes from the registry

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

## Files

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

```tsx
"use client";
import { useId, useState } from "react";
import {
  IconDownload as Download,
  IconChartAreaLine as ChartAreaLine,
  IconLayoutDashboard as LayoutDashboard,
  IconSettings as Settings,
  IconUsers as Users,
  IconActivity as Activity,
  IconChevronDown as ChevronDown,
  IconArrowUpRight as ArrowUpRight,
} from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { MetricGroup, MetricGroupItem } from "./metric-group";
import { MetricChart } from "./metric-chart";
import { ActivityFeed } from "./activity-feed";
import { DataTable, type DataColumn } from "./data-table";
import { StatusBadge } from "./status-badge";
import { SettingsPanel } from "./settings-panel";
import { AvatarStack, type TeamMember } from "./avatar-stack";
import { cn } from "@/lib/utils";
import { AnalyticsOverview } from "./analytics-overview";

export const dashboardActivity = [
  {
    id: "1",
    initials: "MO",
    content: (
      <>
        <strong className="font-medium">Maya Okafor</strong> deployed a new
        release.
      </>
    ),
    time: "12 minutes ago",
  },
  {
    id: "2",
    initials: "LN",
    content: (
      <>
        <strong className="font-medium">Leo Nakamura</strong> invited two
        teammates.
      </>
    ),
    time: "48 minutes ago",
  },
  {
    id: "3",
    initials: "ID",
    content: (
      <>
        <strong className="font-medium">Ines Duarte</strong> upgraded to Pro.
      </>
    ),
    time: "2 hours ago",
  },
];
export const dashboardRevenue = [
  12, 18, 15, 24, 21, 32, 29, 38, 34, 46, 43, 56,
].map((value, index) => ({
  label: [
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec",
  ][index],
  value: value * 100,
}));
export const dashboardMembers = [
  "Maya Okafor",
  "Leo Nakamura",
  "Ines Duarte",
  "Samir Haddad",
  "June Park",
  "Alina Petrova",
  "Nora Jensen",
].map((name, index) => ({
  id: String(index),
  name,
  email: name.toLowerCase().replace(" ", ".") + "@example.com",
  role: index === 0 ? "Owner" : "Member",
  status: index === 4 ? "Invited" : "Active",
}));
type Member = (typeof dashboardMembers)[number];
const columns: DataColumn<Member>[] = [
  {
    key: "name",
    label: "Member",
    value: (row) => row.name,
    sortable: true,
    render: (row) => (
      <div className="flex items-center gap-2.5">
        <AvatarStack members={[row]} />
        <div>
          <p className="font-medium">{row.name}</p>
          <p className="text-xs text-muted-foreground">{row.email}</p>
        </div>
      </div>
    ),
  },
  { key: "role", label: "Role", value: (row) => row.role, sortable: true },
  {
    key: "status",
    label: "Status",
    value: (row) => row.status,
    render: (row) => (
      <StatusBadge variant={row.status === "Active" ? "success" : "warning"}>
        {row.status}
      </StatusBadge>
    ),
  },
];
export function Dashboard({
  compact = false,
  workspaceName = "Acme Studio",
  user = dashboardMembers[0],
}: {
  compact?: boolean;
  workspaceName?: string;
  user?: TeamMember;
}) {
  const periodId = useId();
  const [view, setView] = useState("Overview");
  const [period, setPeriod] = useState("12 months");
  const data =
    period === "6 months" ? dashboardRevenue.slice(6) : dashboardRevenue;
  function exportReport() {
    const blob = new Blob(
      [
        "Month,Revenue\n" +
          data.map((point) => `${point.label},${point.value}`).join("\n"),
      ],
      { type: "text/csv;charset=utf-8" },
    );
    const url = URL.createObjectURL(blob),
      link = document.createElement("a");
    link.href = url;
    link.download = "revenue-report.csv";
    link.click();
    setTimeout(() => URL.revokeObjectURL(url), 1000);
  }
  return (
    <div
      data-slot="dashboard"
      className="@container/dashboard overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-none"
    >
      <div className="flex items-center justify-between gap-4 border-b bg-card px-4 py-3 sm:px-6">
        <div className="flex items-center gap-3">
          <span className="flex size-8 items-center justify-center rounded-lg bg-primary/10 text-primary">
            <LayoutDashboard
              aria-hidden="true"
              className="size-4"
              stroke={1.7}
            />
          </span>
          <div>
            <p className="text-sm font-semibold tracking-tight">
              {workspaceName}
            </p>
            <p className="mt-0.5 text-[11px] text-muted-foreground">
              Workspace
            </p>
          </div>
        </div>
        <div className="flex items-center gap-3">
          <span className="hidden text-xs font-medium text-muted-foreground sm:block">
            {user.name}
          </span>
          <AvatarStack members={[user]} />
        </div>
      </div>
      <div className="flex flex-col @min-[56rem]/dashboard:flex-row">
        <aside className="shrink-0 border-b bg-card p-2.5 @min-[56rem]/dashboard:w-44 @min-[56rem]/dashboard:border-r @min-[56rem]/dashboard:border-b-0 @min-[56rem]/dashboard:p-3">
          <p className="hidden px-3 pt-3 pb-2 text-[11px] font-medium text-muted-foreground @min-[56rem]/dashboard:block">
            Workspace
          </p>
          <nav
            aria-label="Dashboard"
            className="flex gap-1 overflow-auto @min-[56rem]/dashboard:flex-col"
          >
            {[
              { label: "Overview", icon: LayoutDashboard },
              { label: "Analytics", icon: ChartAreaLine },
              { label: "Members", icon: Users },
              { label: "Activity", icon: Activity },
              { label: "Settings", icon: Settings },
            ].map(({ label, icon: Icon }) => (
              <button
                key={label}
                type="button"
                aria-current={view === label ? "page" : undefined}
                onClick={() => setView(label)}
                className={cn(
                  "flex shrink-0 items-center gap-2.5 rounded-md px-3 py-2.5 text-left text-xs font-medium transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring",
                  view === label
                    ? "bg-primary/8 text-primary"
                    : "text-muted-foreground hover:bg-muted",
                )}
              >
                <Icon className="size-4 shrink-0" />
                {label}
              </button>
            ))}
          </nav>
        </aside>
        <div className="min-w-0 flex-1 space-y-5 p-4 sm:p-6">
          {view !== "Analytics" && (
            <header className="flex flex-wrap items-center justify-between gap-4">
              <div>
                <h2 className="text-xl font-semibold tracking-tight">{view}</h2>
                <p className="mt-1 text-xs leading-5 text-muted-foreground">
                  {view === "Overview"
                    ? "A little perspective on your business."
                    : `Your workspace ${view.toLowerCase()}, in one place.`}
                </p>
              </div>
              {view === "Overview" && (
                <div className="flex items-center gap-2">
                  <label className="sr-only" htmlFor={periodId}>
                    Report period
                  </label>
                  <div className="relative">
                    <select
                      id={periodId}
                      value={period}
                      onChange={(event) => setPeriod(event.target.value)}
                      className="h-8 appearance-none rounded-md border border-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"
                    >
                      <option>12 months</option>
                      <option>6 months</option>
                    </select>
                    <ChevronDown
                      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 border-border bg-card px-3 text-xs font-medium text-foreground shadow-none hover:bg-muted"
                    onClick={exportReport}
                  >
                    <Download
                      aria-hidden="true"
                      className="size-3.5"
                      stroke={1.7}
                    />
                    Export
                  </Button>
                </div>
              )}
            </header>
          )}
          {view === "Analytics" && (
            <AnalyticsOverview className="border-0 p-0 sm:p-0" />
          )}
          {view === "Overview" && (
            <>
              <MetricGroup className="grid-cols-3">
                {[
                  {
                    label: "Revenue",
                    value: period === "12 months" ? "$36,800" : "$24,600",
                    change: "12.8%",
                  },
                  { label: "Subscribers", value: "2,420", change: "8.2%" },
                  { label: "Conversion", value: "4.36%", change: "0.6%" },
                ].map((metric) => (
                  <MetricGroupItem
                    key={metric.label}
                    label={metric.label}
                    value={metric.value}
                    className="px-2 py-4 sm:px-5 sm:py-5 [&>p:first-child]:text-[11px] sm:[&>p:first-child]:text-xs"
                    context={
                      <span className="flex flex-wrap items-center gap-1.5">
                        <span className="inline-flex items-center gap-0.5 font-medium text-emerald-700 dark:text-emerald-400">
                          <ArrowUpRight aria-hidden="true" className="size-3" />
                          <span className="sr-only">Increase </span>
                          {metric.change}
                        </span>
                        <span className="hidden text-[11px] sm:inline">
                          vs. last month
                        </span>
                      </span>
                    }
                  />
                ))}
              </MetricGroup>
              <div className="grid gap-5 @min-[64rem]/dashboard:grid-cols-[minmax(0,1.8fr)_minmax(0,1fr)]">
                <MetricChart
                  data={data}
                  height={compact ? 215 : 260}
                  description="Monthly revenue · USD"
                  valueFormatter={(value) =>
                    `$${value.toLocaleString("en-US")}`
                  }
                  axisFormatter={(value) =>
                    value === 0 ? "$0" : `$${value / 1000}k`
                  }
                />
                <ActivityFeed
                  items={dashboardActivity}
                  className="shadow-none"
                />
              </div>
              {!compact && (
                <DataTable
                  title="Team members"
                  rows={dashboardMembers}
                  columns={columns}
                  getRowId={(row) => row.id}
                />
              )}
            </>
          )}
          {view === "Members" && (
            <DataTable
              title="Team members"
              rows={dashboardMembers}
              columns={columns}
              getRowId={(row) => row.id}
            />
          )}
          {view === "Activity" && (
            <ActivityFeed items={dashboardActivity} className="shadow-none" />
          )}
          {view === "Settings" && (
            <SettingsPanel
              initialName={workspaceName}
              initialEmail="team@example.com"
            />
          )}
        </div>
      </div>
    </div>
  );
}
```

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

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

/** A shared surface; use StatCard when each metric needs its own card. */
export function MetricGroup({ className, ...props }: ComponentProps<"div">) {
  return (
    <div
      data-slot="metric-group"
      className={cn(
        "grid grid-cols-[repeat(auto-fit,minmax(min(100%,10rem),1fr))] gap-px overflow-hidden rounded-2xl border border-border bg-border 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)] [&>*]:bg-card",
        className,
      )}
      {...props}
    />
  );
}
export function MetricGroupItem({
  label,
  value,
  context,
  className,
  ...props
}: Omit<ComponentProps<"div">, "children"> & {
  label: string;
  value: ReactNode;
  context?: ReactNode;
}) {
  return (
    <div
      data-slot="metric-group-item"
      className={cn(
        "@container min-w-0 px-5 py-5 wrap-anywhere sm:px-6",
        className,
      )}
      {...props}
    >
      <p className="m-0 text-[13px] leading-5 font-medium text-muted-foreground">
        {label}
      </p>
      <p className="mt-2 mb-2.5 text-[clamp(1.125rem,14cqi,1.875rem)] leading-[1.15] font-semibold tracking-[-0.035em] tabular-nums">
        {value}
      </p>
      {context && (
        <div className="text-xs leading-5 text-muted-foreground">{context}</div>
      )}
    </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/activity-feed.tsx`

```tsx
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
export interface ActivityItem {
  id: string;
  initials: string;
  content: ReactNode;
  time: string;
}
export function ActivityFeed({
  items,
  title = "Recent activity",
  className,
}: {
  items: ActivityItem[];
  title?: string;
  className?: string;
}) {
  return (
    <Card
      data-slot="activity-feed"
      className={cn(
        "gap-0 rounded-lg border bg-card py-0 shadow-none",
        className,
      )}
    >
      <CardHeader className="border-b px-5 py-4 [.border-b]:pb-4">
        <CardTitle className="text-sm font-semibold">{title}</CardTitle>
      </CardHeader>
      <CardContent className="px-5 py-1">
        <ol className="divide-y divide-border">
          {items.map((item) => (
            <li key={item.id} className="flex gap-3 py-4">
              <span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-xs font-medium text-primary">
                {item.initials}
              </span>
              <div className="min-w-0 text-sm">
                <div className="text-card-foreground">{item.content}</div>
                <p className="mt-1 text-xs text-muted-foreground">
                  {item.time}
                </p>
              </div>
            </li>
          ))}
        </ol>
        {!items.length && (
          <p className="py-6 text-center text-sm text-muted-foreground">
            No activity yet.
          </p>
        )}
      </CardContent>
    </Card>
  );
}
```

### `src/registry/overtrue/data-table.tsx`

```tsx
"use client";
import { useMemo, useRef, useState, type ReactNode } from "react";
import {
  IconArrowDown as ArrowDown,
  IconArrowUp as ArrowUp,
  IconArrowsSort as ArrowsSort,
  IconChevronLeft as ChevronLeft,
  IconChevronRight as ChevronRight,
} from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { SearchField } from "./search-field";
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from "@/components/ui/table";
export interface DataColumn<T> {
  key: string;
  label: string;
  value: (row: T) => string | number;
  render?: (row: T) => ReactNode;
  sortable?: boolean;
}
export function DataTable<T>({
  rows,
  columns,
  getRowId,
  title = "Records",
  pageSize = 5,
}: {
  rows: T[];
  columns: DataColumn<T>[];
  getRowId: (row: T) => string;
  title?: string;
  pageSize?: number;
}) {
  const searchRef = useRef<HTMLInputElement>(null);
  const [query, setQuery] = useState("");
  const [sort, setSort] = useState<{ key: string; direction: 1 | -1 } | null>(
    null,
  );
  const [page, setPage] = useState(0);
  const size = Number.isFinite(pageSize)
    ? Math.max(1, Math.floor(pageSize) || 5)
    : 5;
  const filtered = useMemo(() => {
    const found = rows.filter((row) =>
      columns.some((column) =>
        String(column.value(row))
          .toLowerCase()
          .includes(query.trim().toLowerCase()),
      ),
    );
    const column = columns.find((item) => item.key === sort?.key);
    if (column && sort)
      found.sort((a, b) => {
        const x = column.value(a),
          y = column.value(b);
        return (
          (typeof x === "number" && typeof y === "number"
            ? x - y
            : String(x).localeCompare(String(y))) * sort.direction
        );
      });
    return found;
  }, [rows, columns, query, sort]);
  const lastPage = Math.max(0, Math.ceil(filtered.length / size) - 1);
  const safePage = Math.min(page, lastPage);
  return (
    <section
      data-slot="data-table"
      className="min-w-0 overflow-hidden rounded-lg border bg-card shadow-none"
      aria-label={title}
    >
      <div className="flex flex-wrap items-center justify-between gap-3 border-b p-4">
        <h2 className="text-sm font-semibold text-card-foreground">{title}</h2>
        <SearchField
          ref={searchRef}
          label={`Search ${title.toLowerCase()}`}
          value={query}
          onValueChange={(value) => {
            setQuery(value);
            setPage(0);
          }}
          placeholder="Search records…"
          containerClassName="w-full max-w-56"
        />
      </div>
      <div className="overflow-x-auto">
        <Table className="whitespace-nowrap">
          <TableHeader>
            <TableRow>
              {columns.map((column) => (
                <TableHead
                  key={column.key}
                  aria-sort={
                    sort?.key === column.key
                      ? sort.direction === 1
                        ? "ascending"
                        : "descending"
                      : undefined
                  }
                  className="h-10 bg-muted/30 px-4 text-xs font-medium"
                >
                  {column.sortable ? (
                    <button
                      type="button"
                      onClick={() => {
                        setSort({
                          key: column.key,
                          direction:
                            sort?.key === column.key && sort.direction === 1
                              ? -1
                              : 1,
                        });
                        setPage(0);
                      }}
                      className="-ms-2 flex items-center gap-1.5 rounded-md px-2 py-2 hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
                    >
                      {column.label}
                      {sort?.key === column.key ? (
                        sort.direction === 1 ? (
                          <ArrowUp aria-hidden="true" className="size-3" />
                        ) : (
                          <ArrowDown aria-hidden="true" className="size-3" />
                        )
                      ) : (
                        <ArrowsSort
                          aria-hidden="true"
                          className="size-3 text-muted-foreground"
                        />
                      )}
                    </button>
                  ) : (
                    column.label
                  )}
                </TableHead>
              ))}
            </TableRow>
          </TableHeader>
          <TableBody>
            {filtered
              .slice(safePage * size, (safePage + 1) * size)
              .map((row) => (
                <TableRow key={getRowId(row)}>
                  {columns.map((column) => (
                    <TableCell key={column.key} className="px-4 py-3.5 text-sm">
                      {column.render ? column.render(row) : column.value(row)}
                    </TableCell>
                  ))}
                </TableRow>
              ))}
          </TableBody>
        </Table>
      </div>
      {!filtered.length && (
        <div role="status" className="px-4 py-10 text-center">
          <p className="m-0 text-sm font-medium">
            {query.trim() ? "No matching records." : "No records yet."}
          </p>
          <p className="m-0 mt-1 text-xs leading-5 text-muted-foreground">
            {query.trim()
              ? "Try another search or clear it to see all records."
              : "Records will appear here when they are available."}
          </p>
          {query.trim() && (
            <Button
              type="button"
              variant="outline"
              size="sm"
              className="mt-4 shadow-none"
              onClick={() => {
                setQuery("");
                setPage(0);
                searchRef.current?.focus();
              }}
            >
              Clear search
            </Button>
          )}
        </div>
      )}
      <div className="flex flex-wrap items-center justify-between gap-3 border-t px-4 py-3">
        <p className="text-xs text-muted-foreground" aria-live="polite">
          {filtered.length
            ? `${safePage * size + 1}–${Math.min((safePage + 1) * size, filtered.length)}`
            : "0"}{" "}
          of {filtered.length} records
        </p>
        <div className="flex gap-1">
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="size-8 border-border bg-card text-foreground shadow-none hover:bg-muted"
            aria-label="Previous page"
            disabled={!safePage}
            onClick={() => setPage(safePage - 1)}
          >
            <ChevronLeft className="size-3.5" />
          </Button>
          <Button
            type="button"
            variant="outline"
            size="icon"
            className="size-8 border-border bg-card text-foreground shadow-none hover:bg-muted"
            aria-label="Next page"
            disabled={safePage >= lastPage}
            onClick={() => setPage(safePage + 1)}
          >
            <ChevronRight className="size-3.5" />
          </Button>
        </div>
      </div>
    </section>
  );
}
```

### `src/registry/overtrue/search-field.tsx`

```tsx
"use client";
import { useId, useImperativeHandle, useRef, type ComponentProps } from "react";
import { IconSearch, IconX } from "@tabler/icons-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";

export interface SearchFieldProps extends Omit<
  ComponentProps<typeof Input>,
  "type" | "value" | "defaultValue" | "onChange"
> {
  label: string;
  value: string;
  onValueChange: (value: string) => void;
  containerClassName?: string;
}
/** Controlled search input. Filtering and debouncing belong to the caller. */
export function SearchField({
  label,
  value,
  onValueChange,
  id,
  ref,
  containerClassName,
  className,
  placeholder = "Search…",
  ...props
}: SearchFieldProps) {
  const generatedId = useId();
  const inputId = id ?? generatedId;
  const inputRef = useRef<HTMLInputElement>(null);
  useImperativeHandle(ref, () => inputRef.current!, []);
  return (
    <div
      data-slot="search-field"
      className={cn("relative min-w-0", containerClassName)}
    >
      <label htmlFor={inputId} className="sr-only">
        {label}
      </label>
      <IconSearch
        aria-hidden="true"
        className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
      />
      <Input
        {...props}
        ref={inputRef}
        id={inputId}
        type="search"
        value={value}
        onChange={(event) => onValueChange(event.target.value)}
        placeholder={placeholder}
        className={cn(
          "h-10 w-full rounded-md border-input bg-card dark:bg-card ps-9 pe-9 text-base text-foreground shadow-xs placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20 sm:text-sm [&::-webkit-search-cancel-button]:appearance-none",
          className,
        )}
      />
      {value && !props.readOnly && (
        <button
          type="button"
          aria-label={`Clear ${label.toLowerCase()}`}
          disabled={props.disabled}
          className="absolute end-1 top-1/2 flex size-7 -translate-y-1/2 items-center justify-center rounded-sm text-muted-foreground hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50"
          onClick={() => {
            onValueChange("");
            inputRef.current?.focus();
          }}
        >
          <IconX aria-hidden="true" className="size-3.5" />
        </button>
      )}
    </div>
  );
}
```

### `src/registry/overtrue/status-badge.tsx`

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

const variants = {
  success:
    "bg-emerald-500/10 text-emerald-700 ring-emerald-600/20 dark:bg-emerald-400/10 dark:text-emerald-300 dark:ring-emerald-400/25",
  warning:
    "bg-amber-500/10 text-amber-700 ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-300 dark:ring-amber-400/25",
  // Tinted text leans toward the foreground so it stays legible on its own tint.
  error:
    "bg-destructive/10 text-[color-mix(in_oklab,var(--color-destructive)_85%,var(--color-foreground))] ring-destructive/20 dark:ring-destructive/30",
  info: "bg-primary/10 text-[color-mix(in_oklab,var(--color-primary)_85%,var(--color-foreground))] ring-primary/20 dark:ring-primary/30",
  neutral:
    "bg-muted/70 text-[color-mix(in_oklab,var(--color-muted-foreground)_85%,var(--color-foreground))] ring-border dark:bg-muted/50",
};
const dots = {
  success: "bg-emerald-500 dark:bg-emerald-400",
  warning: "bg-amber-500 dark:bg-amber-400",
  error: "bg-destructive",
  info: "bg-primary",
  neutral: "bg-muted-foreground/70",
};
const styles = `@keyframes status-badge-pulse{0%{transform:scale(1);opacity:.6}70%,100%{transform:scale(2.75);opacity:0}}`;

export interface StatusBadgeProps
  extends Omit<ComponentProps<"span">, "children"> {
  children: ReactNode;
  variant?: keyof typeof variants;
  /** Pulse the dot softly for states that are happening right now. */
  live?: boolean;
}

export function StatusBadge({
  children,
  variant = "neutral",
  live = false,
  className,
  ...props
}: StatusBadgeProps) {
  return (
    <span
      data-slot="status-badge"
      data-variant={variant}
      data-live={live || undefined}
      className={cn(
        "inline-flex items-center gap-1.5 whitespace-nowrap rounded-full py-0.5 pr-2.5 pl-2 text-xs leading-5 font-medium tabular-nums ring-1 ring-inset",
        variants[variant],
        className,
      )}
      {...props}
    >
      {live ? (
        <style href="overtrue-status-badge" precedence="default">
          {styles}
        </style>
      ) : null}
      <span
        aria-hidden="true"
        data-slot="status-badge-dot"
        className="relative flex size-1.5 shrink-0"
      >
        {live ? (
          <span
            className={cn(
              "absolute inset-0 rounded-full motion-safe:animate-[status-badge-pulse_2s_cubic-bezier(0.22,1,0.36,1)_infinite] motion-reduce:hidden",
              dots[variant],
            )}
          />
        ) : null}
        <span className={cn("relative size-1.5 rounded-full", dots[variant])} />
      </span>
      {children}
    </span>
  );
}
```

### `src/registry/overtrue/settings-panel.tsx`

```tsx
"use client";
import { useId, useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { SectionCard } from "./section-card";
import { cn } from "@/lib/utils";
export function SettingsPanel({
  initialName = "Acme Studio",
  initialEmail = "team@acme.example",
  onSave,
}: {
  initialName?: string;
  initialEmail?: string;
  onSave?: (values: { name: string; email: string }) => void | Promise<void>;
}) {
  const id = useId();
  const [name, setName] = useState(initialName),
    [email, setEmail] = useState(initialEmail);
  const [status, setStatus] = useState(""),
    [saving, setSaving] = useState(false);
  const [saved, setSaved] = useState({
    name: initialName,
    email: initialEmail,
  });
  const [failed, setFailed] = useState(false);
  const dirty = name !== saved.name || email !== saved.email;
  return (
    <SectionCard
      title="Workspace settings"
      description="Manage your workspace name and contact details."
      contentClassName="p-0"
    >
      <form
        onSubmit={async (event) => {
          event.preventDefault();
          if (saving || !dirty) return;
          const values = { name, email };
          setSaving(true);
          setStatus("");
          setFailed(false);
          try {
            await onSave?.(values);
            setSaved(values);
            setStatus(
              onSave ? "Changes saved." : "Demo saved locally in this preview.",
            );
          } catch {
            setFailed(true);
            setStatus("Could not save changes. Please try again.");
          } finally {
            setSaving(false);
          }
        }}
      >
        <fieldset disabled={saving} className="min-w-0 space-y-6 p-6">
          <div className="grid gap-2">
            <label htmlFor={`${id}-name`} className="text-sm font-medium">
              Workspace name
            </label>
            <Input
              id={`${id}-name`}
              className="h-10 rounded-md border-input bg-card dark:bg-card text-base shadow-xs focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20 [&:user-invalid]:border-destructive [&:user-invalid]:ring-2 [&:user-invalid]:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:bg-muted dark:disabled:bg-muted sm:text-sm"
              required
              name="workspace-name"
              autoComplete="organization"
              value={name}
              onChange={(event) => {
                setName(event.target.value);
                setStatus("");
                setFailed(false);
              }}
            />
          </div>
          <div className="grid gap-2">
            <label htmlFor={`${id}-email`} className="text-sm font-medium">
              Contact email
            </label>
            <Input
              id={`${id}-email`}
              className="h-10 rounded-md border-input bg-card dark:bg-card text-base shadow-xs focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/20 [&:user-invalid]:border-destructive [&:user-invalid]:ring-2 [&:user-invalid]:ring-destructive/20 aria-invalid:border-destructive aria-invalid:ring-destructive/20 disabled:bg-muted dark:disabled:bg-muted sm:text-sm"
              required
              type="email"
              name="contact-email"
              autoComplete="email"
              value={email}
              onChange={(event) => {
                setEmail(event.target.value);
                setStatus("");
                setFailed(false);
              }}
            />
          </div>
        </fieldset>
        <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-muted/30 px-6 py-4">
          <p
            role={failed ? "alert" : "status"}
            className={cn(
              "m-0 min-w-0 grow basis-48 text-xs leading-5 text-muted-foreground",
              failed && "text-destructive",
            )}
          >
            {status ||
              (dirty ? "You have unsaved changes." : "You’re all up to date.")}
          </p>
          <Button
            type="submit"
            className="ml-auto h-10 shrink-0 rounded-md border border-primary bg-primary px-4 text-sm text-primary-foreground shadow-xs hover:bg-primary/90"
            disabled={saving || !dirty}
          >
            {saving ? "Saving…" : "Save changes"}
          </Button>
        </div>
      </form>
    </SectionCard>
  );
}
```

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

```tsx
import type { ComponentProps, ReactNode } from "react";
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
  CardDescription,
} from "@/components/ui/card";
import { cn } from "@/lib/utils";

export interface SectionCardProps extends Omit<
  ComponentProps<typeof Card>,
  "title"
> {
  title?: ReactNode;
  description?: ReactNode;
  actions?: ReactNode;
  contentClassName?: string;
}

/** A shared heading, actions and content surface for admin panels. */
export function SectionCard({
  title,
  description,
  actions,
  children,
  className,
  contentClassName,
  ...props
}: SectionCardProps) {
  return (
    <Card
      data-slot="section-card"
      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,
      )}
      {...props}
    >
      {(title || description || actions) && (
        <CardHeader className="flex flex-row flex-wrap items-center justify-between gap-x-4 gap-y-3 border-b border-border/60 px-5 py-4 [.border-b]:pb-4">
          <div data-slot="section-card-heading" className="min-w-0 flex-1">
            {title && (
              <CardTitle className="m-0 text-sm leading-6 font-semibold tracking-tight text-card-foreground">
                {title}
              </CardTitle>
            )}
            {description && (
              <CardDescription className="m-0 mt-0.5 text-[13px] leading-5 text-muted-foreground">
                {description}
              </CardDescription>
            )}
          </div>
          {actions && (
            <div
              data-slot="section-card-actions"
              className="flex shrink-0 items-center gap-2"
            >
              {actions}
            </div>
          )}
        </CardHeader>
      )}
      <CardContent className={cn("p-5", contentClassName)}>
        {children}
      </CardContent>
    </Card>
  );
}
```

### `src/registry/overtrue/avatar-stack.tsx`

```tsx
"use client";
import { useState } from "react";
import { cn } from "@/lib/utils";
export interface TeamMember {
  id: string;
  name: string;
  image?: string;
}

// Initials take a stable tint from the theme's chart palette.
const tints = [
  "[--avatar-tint:var(--color-chart-1)]",
  "[--avatar-tint:var(--color-chart-2)]",
  "[--avatar-tint:var(--color-chart-3)]",
  "[--avatar-tint:var(--color-chart-4)]",
  "[--avatar-tint:var(--color-chart-5)]",
];
function tintFor(key: string) {
  let hash = 0;
  for (const char of key) hash = (hash * 31 + char.charCodeAt(0)) >>> 0;
  return tints[hash % tints.length];
}

export function AvatarStack({
  members,
  max = 4,
  className,
}: {
  members: TeamMember[];
  max?: number;
  className?: string;
}) {
  const visible = members.slice(0, Math.max(0, max));
  return (
    <div
      data-slot="avatar-stack"
      className={cn("flex items-center -space-x-2", className)}
      aria-label={members.map((member) => member.name).join(", ")}
    >
      {visible.map((member) => (
        <MemberAvatar key={member.id} member={member} />
      ))}
      {members.length > visible.length && (
        <span
          data-slot="avatar-stack-more"
          className="relative flex size-9 shrink-0 items-center justify-center rounded-full border-2 border-background bg-muted text-xs font-medium tracking-tight text-muted-foreground tabular-nums shadow-[0_1px_2px_rgb(0_0_0/0.06)]"
        >
          +{members.length - visible.length}
        </span>
      )}
    </div>
  );
}

function MemberAvatar({ member }: { member: TeamMember }) {
  const [failedImage, setFailedImage] = useState<string>();
  const initials = member.name
    .trim()
    .split(/\s+/)
    .map((word) => word[0])
    .slice(0, 2)
    .join("")
    .toUpperCase();
  const showImage = member.image && member.image !== failedImage;
  return (
    <span
      role="img"
      aria-label={member.name}
      title={member.name}
      data-slot="avatar-stack-item"
      className={cn(
        "relative flex size-9 shrink-0 items-center justify-center overflow-hidden rounded-full border-2 border-background text-xs font-semibold tracking-tight shadow-[0_1px_2px_rgb(0_0_0/0.06)] transition-transform duration-150 ease-[cubic-bezier(0.22,1,0.36,1)] hover:z-10 hover:-translate-y-0.5 motion-reduce:transition-none motion-reduce:hover:translate-y-0",
        "after:pointer-events-none after:absolute after:inset-0 after:rounded-full after:ring-1 after:ring-foreground/10 after:ring-inset",
        showImage
          ? "bg-muted"
          : cn(
              tintFor(member.id || member.name),
              "bg-[color-mix(in_oklab,var(--avatar-tint)_16%,var(--color-card))] text-[color-mix(in_oklab,var(--avatar-tint)_72%,var(--color-foreground))]",
            ),
      )}
    >
      {showImage ? (
        <img
          src={member.image}
          alt=""
          className="size-full object-cover"
          onError={() => setFailedImage(member.image)}
        />
      ) : (
        initials || "?"
      )}
    </span>
  );
}
```

### `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/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>
  );
}
```
