{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stat-card",
  "title": "统计卡片",
  "description": "一组可组合的统计卡片原语：KPI 数字与涨跌幅（绿涨红跌 / 红涨绿跌可切换）、目标状态行、配额进度条、分格计量条、延迟分区表、占比分布条与图例，数字默认按万 / 亿压缩。",
  "dependencies": [
    "lucide-react",
    "@base-ui/react"
  ],
  "files": [
    {
      "path": "src/registry/ui/stat-card/stat-card.tsx",
      "content": "import * as React from \"react\"\nimport { ArrowDownRight, ArrowUpRight, Minus } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  TREND_TEXT,\n  conventionClass,\n  formatStat,\n  toneColor,\n  trendOf,\n  type DataConvention,\n  type DataTone,\n  type StatFormatOptions,\n  type Trend,\n} from \"@/lib/data-display\"\n\nimport {\n  DEFAULT_LABELS,\n  healthStatus,\n  usageStatus,\n  type StatLabels,\n  type StatStatusName,\n} from \"./config\"\nimport {\n  StatBadge,\n  StatStatus,\n  type StatBadgeProps,\n  type StatStatusProps,\n} from \"./stat-status\"\nimport {\n  StatDistribution,\n  StatMeter,\n  StatProgress,\n  StatSegments,\n  type StatDistributionProps,\n  type StatMeterProps,\n  type StatProgressProps,\n  type StatSegment,\n  type StatSegmentsProps,\n} from \"./stat-bars\"\nimport { StatZones, type StatZone, type StatZonesProps } from \"./stat-zones\"\n\ntype StatCardProps = React.ComponentProps<\"div\"> & {\n  /**\n   * 涨跌配色约定：`intl` 绿涨红跌，`cn` 红涨绿跌。\n   * 变量沿 DOM 继承，给外层容器设置一次，里面所有卡片与图表都会跟着变；不设置时沿用外层，最外层默认 `intl`\n   */\n  convention?: DataConvention\n}\n\n/** 统计卡片的外壳：细边框、无阴影，内容用下面的原语自由组合。 */\nfunction StatCard({ className, convention, ...props }: StatCardProps) {\n  return (\n    <div\n      data-slot=\"stat-card\"\n      data-convention={convention}\n      className={cn(\n        \"flex min-w-0 flex-col gap-3 rounded-lg border border-edge bg-card p-4 text-sm text-card-foreground\",\n        conventionClass(convention),\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\ntype StatGroupProps = React.ComponentProps<\"div\"> & {\n  /** 这一组里所有卡片、涨跌幅与图表共用的涨跌配色约定 */\n  convention?: DataConvention\n}\n\n/** 一组卡片的容器：只负责统一设置 `convention`，布局由 `className` 决定。 */\nfunction StatGroup({ className, convention, ...props }: StatGroupProps) {\n  return (\n    <div\n      data-slot=\"stat-group\"\n      data-convention={convention}\n      className={cn(conventionClass(convention), className)}\n      {...props}\n    />\n  )\n}\n\n/** 标题行：左边放 `StatLabel`，右边放图标或状态徽标。 */\nfunction StatHeader({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"stat-header\"\n      className={cn(\"flex items-start justify-between gap-3\", className)}\n      {...props}\n    />\n  )\n}\n\n/** 指标名称，12–13px 的次要文字。 */\nfunction StatLabel({ className, ...props }: React.ComponentProps<\"p\">) {\n  return (\n    <p\n      data-slot=\"stat-label\"\n      className={cn(\"min-w-0 truncate text-[13px] leading-5 text-muted-foreground\", className)}\n      {...props}\n    />\n  )\n}\n\ntype StatIconProps = React.ComponentProps<\"span\"> & {\n  /** 图标色调；不设置时为次要文字色 */\n  tone?: DataTone\n}\n\n/** 图标方块：`size-8` 的细边框小方块，里面放一个 lucide 图标。 */\nfunction StatIcon({ className, tone, style, ...props }: StatIconProps) {\n  return (\n    <span\n      data-slot=\"stat-icon\"\n      aria-hidden=\"true\"\n      data-tone={tone}\n      className={cn(\n        \"flex size-8 shrink-0 items-center justify-center rounded-md border border-edge bg-muted/50 text-muted-foreground data-[tone]:text-[color:var(--stat-tone)] [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      style={\n        tone\n          ? ({\n              \"--stat-tone\": toneColor(tone),\n              ...style,\n            } as React.CSSProperties)\n          : style\n      }\n      {...props}\n    />\n  )\n}\n\ntype StatValueProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 数值；数字会按 `format` 格式化，字符串原样显示 */\n  value?: number | string\n  /** 数字格式，默认 `zh-CN` 压缩（1.2万、3.4亿） */\n  format?: StatFormatOptions\n  /** 单位，跟在数字后，字号更小、颜色更淡 */\n  unit?: React.ReactNode\n  /**\n   * 字号：`sm` 20px、`default` 24px、`lg` 30px\n   * @default \"default\"\n   */\n  size?: \"sm\" | \"default\" | \"lg\"\n  /** 自定义内容，传入后替代 `value` */\n  children?: React.ReactNode\n}\n\nconst VALUE_SIZES = {\n  sm: \"text-xl\",\n  default: \"text-2xl\",\n  lg: \"text-3xl\",\n} as const\n\n/** 大号数字加单位；数字一律等宽对齐（`tabular-nums`）。 */\nfunction StatValue({\n  className,\n  value,\n  format,\n  unit,\n  size = \"default\",\n  children,\n  ...props\n}: StatValueProps) {\n  const text = typeof value === \"number\" ? formatStat(value, format) : value\n  return (\n    <div\n      data-slot=\"stat-value\"\n      data-size={size}\n      className={cn(\n        \"flex items-baseline gap-1.5 leading-none font-semibold tracking-tight tabular-nums\",\n        VALUE_SIZES[size],\n        className,\n      )}\n      {...props}\n    >\n      <span data-slot=\"stat-value-number\">{children ?? text}</span>\n      {unit ? (\n        <span\n          data-slot=\"stat-unit\"\n          className=\"text-sm font-normal tracking-normal text-muted-foreground\"\n        >\n          {unit}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\ntype StatTrendProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /** 涨跌幅，单位是百分点：`12.4` 表示 +12.4% */\n  value: number\n  /** 数字格式，默认带正负号的百分比 */\n  format?: StatFormatOptions\n  /**\n   * 越低越好的指标（延迟、错误率）：下降显示为「好」的颜色，箭头方向不变\n   * @default false\n   */\n  invert?: boolean\n  /**\n   * 显示方向箭头\n   * @default true\n   */\n  showIcon?: boolean\n  /** 单独指定涨跌配色约定，不设置时沿用外层 */\n  convention?: DataConvention\n  /** 覆盖读屏文字（上升 / 下降 / 持平） */\n  labels?: Partial<Pick<StatLabels, \"up\" | \"down\" | \"flat\">>\n  /** 数字后的说明，如「较上周」 */\n  children?: React.ReactNode\n}\n\nconst TREND_ICONS = {\n  up: ArrowUpRight,\n  down: ArrowDownRight,\n  flat: Minus,\n} as const\n\nconst OPPOSITE: Record<Trend, Trend> = { up: \"down\", down: \"up\", flat: \"flat\" }\n\n/** 涨跌幅：箭头 + 带符号的百分比；颜色来自 `convention`，同时有箭头和读屏文字，不只靠颜色。 */\nfunction StatTrend({\n  className,\n  value,\n  format,\n  invert = false,\n  showIcon = true,\n  convention,\n  labels,\n  children,\n  ...props\n}: StatTrendProps) {\n  const trend = trendOf(value)\n  const Icon = TREND_ICONS[trend]\n  const text = { ...DEFAULT_LABELS, ...labels }\n  return (\n    <span\n      data-slot=\"stat-trend\"\n      data-trend={trend}\n      className={cn(\n        \"inline-flex flex-wrap items-center gap-0.5 whitespace-nowrap text-xs font-medium tabular-nums\",\n        TREND_TEXT[invert ? OPPOSITE[trend] : trend],\n        conventionClass(convention),\n        className,\n      )}\n      {...props}\n    >\n      {showIcon ? <Icon aria-hidden=\"true\" className=\"size-3.5 shrink-0\" /> : null}\n      <span className=\"sr-only\">{text[trend]}</span>\n      {formatStat(value, { style: \"percent\", sign: true, ...format })}\n      {children ? (\n        <span className=\"ml-1 font-normal text-muted-foreground\">\n          {children}\n        </span>\n      ) : null}\n    </span>\n  )\n}\n\n/** 两端对齐的说明行，放在进度条 / 计量条下面：左边「剩余 62%」，右边「已用 3.8 / 10 GB」。 */\nfunction StatMeta({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"stat-meta\"\n      className={cn(\n        \"flex items-center justify-between gap-3 text-xs text-muted-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport {\n  StatBadge,\n  StatCard,\n  StatDistribution,\n  StatGroup,\n  StatHeader,\n  StatIcon,\n  StatLabel,\n  StatMeta,\n  StatMeter,\n  StatProgress,\n  StatSegments,\n  StatStatus,\n  StatTrend,\n  StatValue,\n  StatZones,\n  healthStatus,\n  usageStatus,\n}\nexport type {\n  StatBadgeProps,\n  StatCardProps,\n  StatDistributionProps,\n  StatGroupProps,\n  StatIconProps,\n  StatLabels,\n  StatMeterProps,\n  StatProgressProps,\n  StatSegment,\n  StatSegmentsProps,\n  StatStatusName,\n  StatStatusProps,\n  StatTrendProps,\n  StatValueProps,\n  StatZone,\n  StatZonesProps,\n}\n",
      "type": "registry:ui",
      "target": "@ui/stat-card/stat-card.tsx"
    },
    {
      "path": "src/registry/ui/stat-card/stat-status.tsx",
      "content": "import * as React from \"react\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { ChevronRight } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { toneColor, type DataTone } from \"@/lib/data-display\"\n\nimport {\n  DEFAULT_LABELS,\n  STATUS_ICONS,\n  STATUS_TONES,\n  type StatLabels,\n  type StatStatusName,\n} from \"./config\"\n\n// 浅色下多彩色调直接当文字色对比度偏低，向前景色混入一部分压深；暗色下同样的混合会略微提亮，两边都清晰。\nconst TONE_TEXT =\n  \"text-[color:color-mix(in_oklab,var(--stat-tone)_72%,var(--foreground))]\"\n\nfunction toneVars(tone: DataTone, style?: React.CSSProperties) {\n  return { \"--stat-tone\": toneColor(tone), ...style } as React.CSSProperties\n}\n\ntype StatStatusProps = useRender.ComponentProps<\"div\"> & {\n  /** 状态：达标 / 关注 / 风险 / 审核中，决定图标、色调和默认文字 */\n  status: StatStatusName\n  /** 覆盖状态文字 */\n  label?: string\n  /** 覆盖四种状态的默认文字 */\n  labels?: Partial<StatLabels>\n  /**\n   * 右侧显示 `>` 箭头，配合 `render` 做成可点击的入口\n   * @default false\n   */\n  showChevron?: boolean\n}\n\n/**\n * 状态行：状态图标方块 + 两行文字（上：说明，如「3/5 个目标」；下：状态名）。\n * 想让整行可点击，用 `render={<a href=\"…\" />}` 换成链接。\n */\nfunction StatStatus({\n  className,\n  status,\n  label,\n  labels,\n  showChevron = false,\n  render,\n  children,\n  style,\n  ...props\n}: StatStatusProps) {\n  const Icon = STATUS_ICONS[status]\n  const text = label ?? { ...DEFAULT_LABELS, ...labels }[status]\n  return useRender({\n    defaultTagName: \"div\",\n    render,\n    props: mergeProps<\"div\">(\n      {\n        className: cn(\n          \"flex w-full items-center gap-3 rounded-md bg-muted/50 px-3 py-2 text-left transition-colors outline-none focus-visible:ring-3 focus-visible:ring-ring/50\",\n          render && \"hover:bg-muted\",\n          className,\n        ),\n        style: toneVars(STATUS_TONES[status], style),\n        children: (\n          <>\n            <span\n              aria-hidden=\"true\"\n              className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-[color:var(--stat-tone)] text-background [&_svg]:size-4\"\n            >\n              <Icon />\n            </span>\n            <span className=\"flex min-w-0 flex-1 flex-col gap-0.5 text-xs leading-4 font-medium\">\n              <span className=\"truncate text-foreground\">{children}</span>\n              <span className={TONE_TEXT}>{text}</span>\n            </span>\n            {showChevron ? (\n              <ChevronRight\n                aria-hidden=\"true\"\n                className=\"size-4 shrink-0 text-muted-foreground\"\n              />\n            ) : null}\n          </>\n        ),\n      },\n      {\n        \"data-slot\": \"stat-status\",\n        \"data-status\": status,\n        ...props,\n      } as React.ComponentProps<\"div\">,\n    ),\n  })\n}\n\ntype StatBadgeProps = React.ComponentProps<\"span\"> & {\n  /** 用状态决定色调与默认文字；与 `tone` 二选一 */\n  status?: StatStatusName\n  /** 直接指定色调 */\n  tone?: DataTone\n  /** 覆盖四种状态的默认文字 */\n  labels?: Partial<StatLabels>\n}\n\n/** 状态徽标：色点 + 文字的小药丸。颜色之外永远有文字，不依赖颜色传达状态。 */\nfunction StatBadge({\n  className,\n  status,\n  tone,\n  labels,\n  style,\n  children,\n  ...props\n}: StatBadgeProps) {\n  const color = tone ?? (status ? STATUS_TONES[status] : \"muted\")\n  return (\n    <span\n      data-slot=\"stat-badge\"\n      data-status={status}\n      className={cn(\n        \"inline-flex h-5 w-fit shrink-0 items-center gap-1.5 rounded-full border border-edge px-2 text-xs font-medium whitespace-nowrap text-foreground\",\n        className,\n      )}\n      style={toneVars(color, style)}\n      {...props}\n    >\n      <span\n        aria-hidden=\"true\"\n        className=\"size-1.5 rounded-full bg-[color:var(--stat-tone)]\"\n      />\n      {children ?? (status ? { ...DEFAULT_LABELS, ...labels }[status] : null)}\n    </span>\n  )\n}\n\nexport { StatBadge, StatStatus }\nexport type { StatBadgeProps, StatStatusProps }\n",
      "type": "registry:ui",
      "target": "@ui/stat-card/stat-status.tsx"
    },
    {
      "path": "src/registry/ui/stat-card/stat-bars.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  SERIES_TONES,\n  formatStat,\n  motionStyles,\n  toneColor,\n  type DataTone,\n  type StatFormatOptions,\n} from \"@/lib/data-display\"\n\nimport {\n  DEFAULT_LABELS,\n  STATUS_TONES,\n  healthStatus,\n  usageStatus,\n  type StatLabels,\n} from \"./config\"\n\nfunction toneVar(tone: DataTone, style?: React.CSSProperties) {\n  return { \"--stat-tone\": toneColor(tone), ...style } as React.CSSProperties\n}\n\nconst clampPercent = (value: number, max: number) =>\n  max > 0 ? Math.min(100, Math.max(0, (value / max) * 100)) : 0\n\ntype StatProgressProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 当前值 */\n  value: number\n  /**\n   * 满值\n   * @default 100\n   */\n  max?: number\n  /**\n   * 填充色。`\"usage\"`：按用量自动变色（65% 起关注、90% 起风险），适合配额；\n   * `\"health\"`：按健康度变色（低于 40% 风险、低于 75% 关注）\n   * @default \"primary\"\n   */\n  tone?: DataTone | \"usage\" | \"health\"\n  /**\n   * 粗细：`sm` 4px、`default` 6px、`lg` 8px\n   * @default \"default\"\n   */\n  size?: \"sm\" | \"default\" | \"lg\"\n  /** 读屏名称 */\n  label?: string\n  /** 覆盖默认文案 */\n  labels?: Partial<StatLabels>\n}\n\nconst BAR_SIZES = { sm: \"h-1\", default: \"h-1.5\", lg: \"h-2\" } as const\n\nfunction autoTone(tone: StatProgressProps[\"tone\"], percent: number): DataTone {\n  if (tone === \"usage\") return STATUS_TONES[usageStatus(percent)]\n  if (tone === \"health\") return STATUS_TONES[healthStatus(percent)]\n  return tone ?? \"primary\"\n}\n\n/** 进度条：服务端直接输出最终宽度，填充用 CSS 从左向右展开；系统开启「减少动态效果」时不播放。 */\nfunction StatProgress({\n  className,\n  value,\n  max = 100,\n  tone,\n  size = \"default\",\n  label,\n  labels,\n  style,\n  ...props\n}: StatProgressProps) {\n  const percent = clampPercent(value, max)\n  return (\n    <div\n      data-slot=\"stat-progress\"\n      role=\"progressbar\"\n      aria-label={label ?? { ...DEFAULT_LABELS, ...labels }.progress}\n      aria-valuemin={0}\n      aria-valuemax={max}\n      aria-valuenow={value}\n      className={cn(\n        \"w-full overflow-hidden rounded-full bg-muted\",\n        BAR_SIZES[size],\n        className,\n      )}\n      style={toneVar(autoTone(tone, percent), style)}\n      {...props}\n    >\n      {motionStyles()}\n      <div\n        data-slot=\"stat-progress-fill\"\n        data-dd-anim=\"reveal\"\n        className=\"h-full rounded-full bg-[color:var(--stat-tone)]\"\n        style={{ width: `${percent}%` }}\n      />\n    </div>\n  )\n}\n\ntype StatMeterProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 0–100 的百分比，按它点亮格子 */\n  value: number\n  /**\n   * 格数\n   * @default 10\n   */\n  segments?: number\n  /**\n   * 点亮的颜色。默认按健康度自动变色：低于 40% 风险、低于 75% 关注，其余达标\n   * @default \"health\"\n   */\n  tone?: DataTone | \"health\" | \"usage\"\n  /** 读屏名称 */\n  label?: string\n  /** 覆盖默认文案 */\n  labels?: Partial<StatLabels>\n}\n\n/** 分格计量条：N 个小格，按值从左到右依次点亮；常用来表示服务健康度。 */\nfunction StatMeter({\n  className,\n  value,\n  segments = 10,\n  tone = \"health\",\n  label,\n  labels,\n  style,\n  ...props\n}: StatMeterProps) {\n  const percent = clampPercent(value, 100)\n  const lit = Math.round((percent * segments) / 100)\n  return (\n    <div\n      data-slot=\"stat-meter\"\n      role=\"meter\"\n      aria-label={label ?? { ...DEFAULT_LABELS, ...labels }.meter}\n      aria-valuemin={0}\n      aria-valuemax={100}\n      aria-valuenow={value}\n      className={cn(\"flex h-6 w-28 items-stretch gap-0.5\", className)}\n      style={toneVar(autoTone(tone, percent), style)}\n      {...props}\n    >\n      {motionStyles()}\n      {Array.from({ length: segments }, (_, index) =>\n        index < lit ? (\n          <span\n            key={index}\n            data-state=\"on\"\n            data-dd-anim=\"rise\"\n            className=\"flex-1 origin-bottom rounded-[3px] bg-[color:var(--stat-tone)]\"\n            style={{ animationDelay: `${index * 40}ms` }}\n          />\n        ) : (\n          <span\n            key={index}\n            data-state=\"off\"\n            className=\"flex-1 rounded-[3px] bg-foreground/8\"\n          />\n        ),\n      )}\n    </div>\n  )\n}\n\ntype StatSegment = {\n  /** 分类名 */\n  label: string\n  /** 数值，按占比决定宽度 */\n  value: number\n  /** 色调，不设置时按顺序取默认色板 */\n  tone?: DataTone\n}\n\ntype StatSegmentsProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 分段数据 */\n  segments: StatSegment[]\n  /** 读屏名称；默认把每段的占比念出来 */\n  label?: string\n}\n\n/** 横向分段条：每段宽度按占比分配，段间留 2px 缝；整条从左向右扫出。 */\nfunction StatSegments({\n  className,\n  segments,\n  label,\n  ...props\n}: StatSegmentsProps) {\n  const total = segments.reduce((sum, item) => sum + item.value, 0)\n  const summary =\n    label ??\n    segments\n      .map(\n        (item) =>\n          `${item.label} ${formatStat(total ? (item.value / total) * 100 : 0, { style: \"percent\" })}`,\n      )\n      .join(\"，\")\n  return (\n    <div\n      data-slot=\"stat-segments\"\n      role=\"img\"\n      aria-label={summary}\n      className={cn(\"h-2 w-full\", className)}\n      {...props}\n    >\n      {motionStyles()}\n      <div\n        data-dd-anim=\"reveal\"\n        className=\"flex size-full gap-0.5 overflow-hidden rounded-full\"\n      >\n        {segments.map((item, index) =>\n          item.value > 0 ? (\n            <span\n              key={item.label}\n              className=\"h-full min-w-0 basis-0\"\n              style={{\n                flexGrow: item.value,\n                backgroundColor: toneColor(\n                  item.tone ??\n                    SERIES_TONES[index % SERIES_TONES.length] ??\n                    \"blue\",\n                ),\n              }}\n            />\n          ) : null,\n        )}\n      </div>\n    </div>\n  )\n}\n\ntype StatDistributionProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 分段数据 */\n  segments: StatSegment[]\n  /** 图例里每项数值的格式，默认 `zh-CN` 压缩 */\n  format?: StatFormatOptions\n  /**\n   * 显示图例格（色条 + 分类名 + 占比 + 数值）\n   * @default true\n   */\n  legend?: boolean\n  /**\n   * 图例列数上限，窄屏自动减少\n   * @default 4\n   */\n  columns?: 2 | 3 | 4\n}\n\nconst LEGEND_COLUMNS = {\n  2: \"grid-cols-1 sm:grid-cols-2\",\n  3: \"grid-cols-1 sm:grid-cols-3\",\n  4: \"grid-cols-1 sm:grid-cols-2 lg:grid-cols-4\",\n} as const\n\n/** 占比分布：分段条加图例格。图例同时写出占比和数值，不靠颜色区分分类。 */\nfunction StatDistribution({\n  className,\n  segments,\n  format,\n  legend = true,\n  columns = 4,\n  ...props\n}: StatDistributionProps) {\n  const total = segments.reduce((sum, item) => sum + item.value, 0)\n  return (\n    <div\n      data-slot=\"stat-distribution\"\n      className={cn(\"flex flex-col gap-3\", className)}\n      {...props}\n    >\n      <StatSegments segments={segments} />\n      {legend ? (\n        <ul\n          data-slot=\"stat-legend\"\n          className={cn(\"grid gap-2\", LEGEND_COLUMNS[columns])}\n        >\n          {segments.map((item, index) => {\n            const percent = total ? (item.value / total) * 100 : 0\n            return (\n              <li\n                key={item.label}\n                data-slot=\"stat-legend-item\"\n                className=\"flex items-center gap-2.5 rounded-md border border-edge px-3 py-2\"\n              >\n                <span\n                  aria-hidden=\"true\"\n                  className=\"h-8 w-1 shrink-0 rounded-full\"\n                  style={{\n                    backgroundColor: toneColor(\n                      item.tone ??\n                        SERIES_TONES[index % SERIES_TONES.length] ??\n                        \"blue\",\n                    ),\n                  }}\n                />\n                <div className=\"flex min-w-0 flex-col gap-0.5\">\n                  <span className=\"truncate text-xs text-muted-foreground\">\n                    {item.label}\n                  </span>\n                  <span className=\"text-sm font-semibold tabular-nums\">\n                    {formatStat(percent, { style: \"percent\" })}\n                    <span className=\"ml-1.5 font-normal text-muted-foreground\">\n                      {formatStat(item.value, format)}\n                    </span>\n                  </span>\n                </div>\n              </li>\n            )\n          })}\n        </ul>\n      ) : null}\n    </div>\n  )\n}\n\nexport { StatDistribution, StatMeter, StatProgress, StatSegments }\nexport type {\n  StatDistributionProps,\n  StatMeterProps,\n  StatProgressProps,\n  StatSegment,\n  StatSegmentsProps,\n}\n",
      "type": "registry:ui",
      "target": "@ui/stat-card/stat-bars.tsx"
    },
    {
      "path": "src/registry/ui/stat-card/stat-zones.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  formatStat,\n  toneColor,\n  type DataTone,\n} from \"@/lib/data-display\"\n\nimport { DEFAULT_LABELS, type StatLabels } from \"./config\"\n\ntype StatZone = {\n  /** 分区名，如「快」 */\n  label: string\n  /** 区间，如 `< 50 ms` */\n  range: string\n  /** 占比，单位是百分点：`62.4` 表示 62.4% */\n  share: number\n  /** 色条色调，不设置时按顺序取绿 / 蓝 / 琥珀 / 玫红 */\n  tone?: DataTone\n}\n\ntype StatZonesProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /** 分区，按从好到差的顺序 */\n  zones: StatZone[]\n  /** 覆盖表头文案 */\n  labels?: Partial<Pick<StatLabels, \"zone\" | \"rangeShare\">>\n}\n\nconst ZONE_TONES: DataTone[] = [\"green\", \"blue\", \"amber\", \"rose\"]\n\n/** 分区表：每行一根彩色竖条 + 分区名 + 区间 + 占比，适合延迟、响应码这类「落在哪一档」的分布。 */\nfunction StatZones({ className, zones, labels, ...props }: StatZonesProps) {\n  const text = { ...DEFAULT_LABELS, ...labels }\n  return (\n    <div\n      data-slot=\"stat-zones\"\n      className={cn(\"flex flex-col text-sm\", className)}\n      {...props}\n    >\n      <div className=\"flex items-center justify-between border-b border-edge pb-2 text-xs text-muted-foreground\">\n        <span>{text.zone}</span>\n        <span>{text.rangeShare}</span>\n      </div>\n      <ul className=\"divide-y divide-edge\">\n        {zones.map((zone, index) => (\n          <li\n            key={zone.label}\n            data-slot=\"stat-zone\"\n            className=\"flex items-center justify-between gap-3 py-2 last:pb-0\"\n          >\n            <span className=\"flex min-w-0 items-center gap-2.5\">\n              <span\n                aria-hidden=\"true\"\n                className=\"h-5 w-1 shrink-0 rounded-full\"\n                style={{\n                  backgroundColor: toneColor(\n                    zone.tone ??\n                      ZONE_TONES[index % ZONE_TONES.length] ??\n                      \"green\",\n                  ),\n                }}\n              />\n              <span className=\"truncate\">{zone.label}</span>\n            </span>\n            <span className=\"flex shrink-0 items-center gap-3 tabular-nums\">\n              <span className=\"text-muted-foreground\">{zone.range}</span>\n              <span className=\"w-14 text-right font-semibold\">\n                {formatStat(zone.share, { style: \"percent\" })}\n              </span>\n            </span>\n          </li>\n        ))}\n      </ul>\n    </div>\n  )\n}\n\nexport { StatZones }\nexport type { StatZone, StatZonesProps }\n",
      "type": "registry:ui",
      "target": "@ui/stat-card/stat-zones.tsx"
    },
    {
      "path": "src/registry/ui/stat-card/config.ts",
      "content": "import {\n  Minus,\n  ShieldAlert,\n  TrendingDown,\n  TrendingUp,\n  type LucideIcon,\n} from \"lucide-react\"\n\nimport type { DataTone } from \"@/lib/data-display\"\n\n/** 达标 / 关注 / 风险 / 审核中，四种状态与它们的色调、图标。 */\ntype StatStatusName = \"ok\" | \"watch\" | \"risk\" | \"review\"\n\nconst STATUS_TONES: Record<StatStatusName, DataTone> = {\n  ok: \"green\",\n  watch: \"amber\",\n  risk: \"rose\",\n  review: \"blue\",\n}\n\nconst STATUS_ICONS: Record<StatStatusName, LucideIcon> = {\n  ok: TrendingUp,\n  watch: Minus,\n  risk: TrendingDown,\n  review: ShieldAlert,\n}\n\ntype StatLabels = {\n  ok: string\n  watch: string\n  risk: string\n  review: string\n  /** 涨跌方向的读屏文字 */\n  up: string\n  down: string\n  flat: string\n  /** 进度条、计量条的读屏名称 */\n  progress: string\n  meter: string\n  /** 分布条的读屏名称 */\n  distribution: string\n  /** 延迟分区表的表头 */\n  zone: string\n  rangeShare: string\n}\n\nconst DEFAULT_LABELS: StatLabels = {\n  ok: \"达标\",\n  watch: \"关注\",\n  risk: \"风险\",\n  review: \"审核中\",\n  up: \"上升\",\n  down: \"下降\",\n  flat: \"持平\",\n  progress: \"进度\",\n  meter: \"计量\",\n  distribution: \"占比分布\",\n  zone: \"分区\",\n  rangeShare: \"范围 / 占比\",\n}\n\n/** 「用量」口径：越高越危险。默认 65% 起关注，90% 起风险。 */\nfunction usageStatus(\n  percent: number,\n  thresholds: { watch?: number; risk?: number } = {},\n): StatStatusName {\n  const { watch = 65, risk = 90 } = thresholds\n  return percent >= risk ? \"risk\" : percent >= watch ? \"watch\" : \"ok\"\n}\n\n/** 「健康度」口径：越高越好。低于 40% 风险，低于 75% 关注。 */\nfunction healthStatus(\n  percent: number,\n  thresholds: { risk?: number; watch?: number } = {},\n): StatStatusName {\n  const { risk = 40, watch = 75 } = thresholds\n  return percent < risk ? \"risk\" : percent < watch ? \"watch\" : \"ok\"\n}\n\nexport { DEFAULT_LABELS, STATUS_ICONS, STATUS_TONES, healthStatus, usageStatus }\nexport type { StatLabels, StatStatusName }\n",
      "type": "registry:ui",
      "target": "@ui/stat-card/config.ts"
    },
    {
      "path": "src/registry/lib/data-display.ts",
      "content": "import * as React from \"react\"\n\n/**\n * 数据展示共用工具：涨跌配色约定、色调、数字与日期格式化。\n * stat-card、mini-chart、chart-card 三个条目共用这一份，所以涨跌色、数字格式只在这里定义。\n */\n\n/**\n * 涨跌配色约定：\n * - `intl`（默认）：绿涨红跌；\n * - `cn`：红涨绿跌，国内行情软件的习惯。\n */\ntype DataConvention = \"intl\" | \"cn\"\n\n// 约定只改写 `--stat-up` / `--stat-down` 两个 CSS 变量。变量沿 DOM 向下继承，\n// 所以给整块看板的外层设一次 `convention`，里面所有涨跌色（包括图表里的 SVG）都跟着变，\n// 不需要 context，服务端组件也能用。没有设置时回落到默认的绿涨红跌。\nconst CONVENTION_CLASSES: Record<DataConvention, string> = {\n  intl: \"[--stat-up:var(--flow-green,var(--primary))] [--stat-down:var(--flow-rose,var(--destructive))]\",\n  cn: \"[--stat-up:var(--flow-rose,var(--destructive))] [--stat-down:var(--flow-green,var(--primary))]\",\n}\n\n/** 把 `convention` 换成要加到根元素上的类名；不传返回 `undefined`，沿用外层的约定。 */\nfunction conventionClass(convention?: DataConvention) {\n  return convention ? CONVENTION_CLASSES[convention] : undefined\n}\n\ntype Trend = \"up\" | \"down\" | \"flat\"\n\nfunction trendOf(value: number): Trend {\n  return value > 0 ? \"up\" : value < 0 ? \"down\" : \"flat\"\n}\n\n/** 涨跌文字色（Tailwind 只认完整类名，所以三种写全）。 */\nconst TREND_TEXT: Record<Trend, string> = {\n  up: \"text-[color:var(--stat-up,var(--flow-green,var(--primary)))]\",\n  down: \"text-[color:var(--stat-down,var(--flow-rose,var(--destructive)))]\",\n  flat: \"text-muted-foreground\",\n}\n\n/**\n * 内置色调：\n * - `primary`、`foreground`、`muted`、`destructive` 直接引用主题变量，跟随颜色主题；\n * - `blue` 等多彩色调引用 `--flow-*` 变量，没有安装时回落到主题强调色；\n * - `chart-1` … `chart-5` 引用主题的图表色板；\n * - `up` / `down` 是涨跌色，随 `convention` 变化。\n */\ntype DataToneName =\n  | \"primary\"\n  | \"foreground\"\n  | \"muted\"\n  | \"destructive\"\n  | \"blue\"\n  | \"green\"\n  | \"amber\"\n  | \"rose\"\n  | \"violet\"\n  | \"cyan\"\n  | \"chart-1\"\n  | \"chart-2\"\n  | \"chart-3\"\n  | \"chart-4\"\n  | \"chart-5\"\n  | \"up\"\n  | \"down\"\n\n/** 色调：内置名，或任意 CSS 颜色（`\"var(--brand)\"`、`\"oklch(0.7 0.2 150)\"` 都可以）。 */\ntype DataTone = DataToneName | (string & {})\n\nconst TONES: Record<DataToneName, string> = {\n  primary: \"var(--primary)\",\n  foreground: \"var(--foreground)\",\n  muted: \"var(--muted-foreground)\",\n  destructive: \"var(--destructive)\",\n  blue: \"var(--flow-blue, var(--primary))\",\n  green: \"var(--flow-green, var(--primary))\",\n  amber: \"var(--flow-amber, var(--primary))\",\n  rose: \"var(--flow-rose, var(--destructive))\",\n  violet: \"var(--flow-violet, var(--primary))\",\n  cyan: \"var(--flow-cyan, var(--primary))\",\n  \"chart-1\": \"var(--chart-1)\",\n  \"chart-2\": \"var(--chart-2)\",\n  \"chart-3\": \"var(--chart-3)\",\n  \"chart-4\": \"var(--chart-4)\",\n  \"chart-5\": \"var(--chart-5)\",\n  up: \"var(--stat-up, var(--flow-green, var(--primary)))\",\n  down: \"var(--stat-down, var(--flow-rose, var(--destructive)))\",\n}\n\n/** 把色调换成可直接用于 `fill` / `stroke` / `color` 的 CSS 颜色。 */\nfunction toneColor(tone: DataTone): string {\n  return Object.hasOwn(TONES, tone) ? TONES[tone as DataToneName] : tone\n}\n\n/**\n * 多序列图表默认的色调轮换：六种互不相近的多彩色调。\n * 不把主题强调色放进来：强调色会落在蓝、绿、紫、琥珀任意一种附近，\n * 切到对应的颜色主题时会与其中一条序列撞色。单条序列想跟随主题，显式写 `tone=\"primary\"`。\n */\nconst SERIES_TONES: DataTone[] = [\n  \"blue\",\n  \"amber\",\n  \"green\",\n  \"violet\",\n  \"rose\",\n  \"cyan\",\n]\n\ntype StatFormatOptions = {\n  /**\n   * 数字样式：`number` 普通数字，`percent` 百分比（传入的 12.5 表示 12.5%），`currency` 货币\n   * @default \"number\"\n   */\n  style?: \"number\" | \"percent\" | \"currency\"\n  /**\n   * BCP 47 区域标识；`zh-CN` 压缩成「万 / 亿」，`en` 压缩成 K / M / B\n   * @default \"zh-CN\"\n   */\n  locale?: string\n  /**\n   * 压缩大数（1.2万、3.4亿）；百分比不压缩\n   * @default true\n   */\n  compact?: boolean\n  /**\n   * 货币代码，仅 `style=\"currency\"` 生效；人民币显示 ¥\n   * @default \"CNY\"\n   */\n  currency?: string\n  /**\n   * 最多保留的小数位\n   * @default 1（压缩与百分比），2（普通数字与货币）\n   */\n  digits?: number\n  /**\n   * 正数前加 `+`（零不加），用于涨跌幅\n   * @default false\n   */\n  sign?: boolean\n}\n\nconst formatters = new Map<string, Intl.NumberFormat>()\n\n/**\n * 格式化统计数字，默认 `zh-CN` 压缩：`12480` → `1.2万`，`345000000` → `3.5亿`；\n * `locale: \"en\"` 时为 `12K`、`345M`。同参数的 `Intl.NumberFormat` 会被缓存复用。\n */\nfunction formatStat(value: number, options: StatFormatOptions = {}): string {\n  const {\n    style = \"number\",\n    locale = \"zh-CN\",\n    compact = style !== \"percent\",\n    currency = \"CNY\",\n    sign = false,\n  } = options\n  const digits = options.digits ?? (compact || style === \"percent\" ? 1 : 2)\n  const key = [style, locale, compact, currency, digits, sign].join(\"|\")\n  let formatter = formatters.get(key)\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, {\n      style: style === \"number\" ? \"decimal\" : style,\n      currency: style === \"currency\" ? currency : undefined,\n      currencyDisplay: style === \"currency\" ? \"narrowSymbol\" : undefined,\n      notation: compact && style !== \"percent\" ? \"compact\" : \"standard\",\n      maximumFractionDigits: digits,\n      signDisplay: sign ? \"exceptZero\" : \"auto\",\n    })\n    formatters.set(key, formatter)\n  }\n  return formatter.format(style === \"percent\" ? value / 100 : value)\n}\n\ntype DayStyle = \"long\" | \"short\" | \"weekday\"\n\nconst dayFormatters = new Map<string, Intl.DateTimeFormat>()\n\n/**\n * 格式化日期轴文字：`long` → `2月14日`，`short` → `02/14`，`weekday` → `一`（单字星期）。\n * 日期按 UTC 解析与输出，`\"2026-02-14\"` 这类字符串在任何时区、任何环境下结果一致，不会引起水合差异。\n */\nfunction formatDay(\n  input: string | number | Date,\n  style: DayStyle = \"long\",\n  locale = \"zh-CN\",\n): string {\n  const key = `${style}|${locale}`\n  let formatter = dayFormatters.get(key)\n  if (!formatter) {\n    const options: Intl.DateTimeFormatOptions =\n      style === \"weekday\"\n        ? { weekday: \"narrow\" }\n        : style === \"short\"\n          ? { month: \"2-digit\", day: \"2-digit\" }\n          : { month: \"long\", day: \"numeric\" }\n    formatter = new Intl.DateTimeFormat(locale, { ...options, timeZone: \"UTC\" })\n    dayFormatters.set(key, formatter)\n  }\n  return formatter.format(new Date(input))\n}\n\ntype AxisFormatOptions = Pick<\n  StatFormatOptions,\n  \"style\" | \"locale\" | \"currency\" | \"digits\"\n>\n\n// 压缩单位表：从大到小，命中第一个不超过轴最大值的单位\nconst AXIS_UNITS = {\n  zh: [\n    [1e8, \"亿\"],\n    [1e4, \"万\"],\n  ],\n  \"zh-trad\": [\n    [1e8, \"億\"],\n    [1e4, \"萬\"],\n  ],\n  en: [\n    [1e9, \"B\"],\n    [1e6, \"M\"],\n    [1e3, \"K\"],\n  ],\n} as const\n\n/**\n * 按「整条轴」统一单位的刻度格式化：先用轴的最大值选定一个单位，所有刻度都换算成它，\n * 避免 `9000` 与 `1.2万` 混在同一根轴上。例如最大值 12000 时刻度是 `0`、`0.3万`、`0.6万`、`0.9万`、`1.2万`；\n * 最大值 8000 时全部是整数（`2000`、`4000`…）。`en` 同理换成 K / M / B。\n *\n * @param max 轴上数据的最大绝对值；留 15% 余量，因为图表会把坐标轴上沿取整到比数据更大的刻度\n */\nfunction createAxisFormatter(\n  max: number,\n  options: AxisFormatOptions = {},\n): (value: number) => string {\n  const { style = \"number\", locale = \"zh-CN\", currency = \"CNY\" } = options\n  if (style === \"percent\")\n    return (value) =>\n      formatStat(value, { style: \"percent\", locale, digits: options.digits })\n  const family = /^zh-(TW|HK|MO|Hant)/i.test(locale)\n    ? \"zh-trad\"\n    : /^zh/i.test(locale)\n      ? \"zh\"\n      : \"en\"\n  const reach = Math.abs(max) * 1.15\n  const unit = AXIS_UNITS[family].find(([size]) => reach >= size)\n  const divisor = unit?.[0] ?? 1\n  const suffix = unit?.[1] ?? \"\"\n  const formatter = new Intl.NumberFormat(locale, {\n    style: style === \"currency\" ? \"currency\" : \"decimal\",\n    currency: style === \"currency\" ? currency : undefined,\n    currencyDisplay: style === \"currency\" ? \"narrowSymbol\" : undefined,\n    notation: \"standard\",\n    maximumFractionDigits: options.digits ?? 2,\n  })\n  return (value) =>\n    value === 0\n      ? formatter.format(0)\n      : `${formatter.format(value / divisor)}${suffix}`\n}\n\n// 进度条、计量条、分段条、进度环、刻度仪表的入场动画：纯 CSS，服务端直接输出最终状态，\n// 所以没有脚本时也完整显示；系统开启「减少动态效果」时不播放。\nconst MOTION_CSS = `@media (prefers-reduced-motion: no-preference) {\n@keyframes dd-reveal { from { clip-path: inset(0 100% 0 0 round 9999px) } to { clip-path: inset(0 0 0 0 round 9999px) } }\n@keyframes dd-rise { from { opacity: 0; transform: scaleY(0.35) } }\n@keyframes dd-fade { from { opacity: 0 } }\n@keyframes dd-arc { from { stroke-dasharray: 0 1 } }\n[data-dd-anim=\"reveal\"] { animation: dd-reveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"rise\"] { animation: dd-rise 0.3s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"fade\"] { animation: dd-fade 0.35s cubic-bezier(0.22, 1, 0.36, 1) both }\n[data-dd-anim=\"arc\"] { animation: dd-arc 0.9s cubic-bezier(0.22, 1, 0.36, 1) both }\n}`\n\n/**\n * 渲染上面的动画样式。React 19 会按 `href` 去重并提升到 `<head>`，\n * 同一页放多少个组件都只有一份；放在服务端组件里也能用。\n */\nfunction motionStyles() {\n  return React.createElement(\n    \"style\",\n    { href: \"data-display-motion\", precedence: \"default\" },\n    MOTION_CSS,\n  )\n}\n\nexport {\n  TREND_TEXT,\n  SERIES_TONES,\n  conventionClass,\n  createAxisFormatter,\n  formatDay,\n  formatStat,\n  motionStyles,\n  toneColor,\n  trendOf,\n}\nexport type {\n  AxisFormatOptions,\n  DataConvention,\n  DataTone,\n  DataToneName,\n  DayStyle,\n  StatFormatOptions,\n  Trend,\n}\n",
      "type": "registry:ui",
      "target": "lib/data-display.ts"
    }
  ],
  "cssVars": {
    "theme": {
      "color-edge": "var(--edge)"
    },
    "light": {
      "flow-blue": "oklch(0.56 0.2 258)",
      "flow-green": "oklch(0.62 0.16 150)",
      "flow-amber": "oklch(0.7 0.16 70)",
      "flow-rose": "oklch(0.62 0.21 5)",
      "flow-violet": "oklch(0.56 0.22 292)",
      "flow-cyan": "oklch(0.66 0.12 215)",
      "edge": "color-mix(in oklch, var(--foreground) 12%, transparent)"
    },
    "dark": {
      "flow-blue": "oklch(0.72 0.15 255)",
      "flow-green": "oklch(0.77 0.17 152)",
      "flow-amber": "oklch(0.82 0.15 82)",
      "flow-rose": "oklch(0.74 0.16 8)",
      "flow-violet": "oklch(0.74 0.15 295)",
      "flow-cyan": "oklch(0.82 0.11 208)",
      "edge": "color-mix(in oklch, var(--foreground) 18%, transparent)"
    }
  },
  "type": "registry:ui"
}
