{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "change-summary",
  "title": "改动摘要",
  "description": "一张卡片逐行列出指标的变化：指标名、一句话变化和趋势标记。好坏（绿 / 红 / 灰）与方向（上 / 下 / 平）分开设置，读屏会念出两者；数据是纯 JSON，入场依次淡入并尊重「减少动态效果」。",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "src/registry/ui/change-summary/change-summary.tsx",
      "content": "import * as React from \"react\"\nimport { ArrowDown, ArrowUp, Minus } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { toneColor, type DataTone } from \"@/lib/data-display\"\n\nimport {\n  DEFAULT_LABELS,\n  TONE_DIRECTIONS,\n  type ChangeDirection,\n  type ChangeSummaryChange,\n  type ChangeSummaryLabels,\n  type ChangeTone,\n} from \"./config\"\n\n// 好坏用固定的「绿 / 玫红」，不跟涨跌配色约定（`convention`）走：\n// 约定只改涨跌色，而这里的颜色表达的是「好」和「坏」，错误率下降永远是好事。\nconst TONE_COLORS: Record<ChangeTone, DataTone> = {\n  good: \"green\",\n  bad: \"rose\",\n  neutral: \"muted\",\n}\n\nconst DIRECTION_ICONS = {\n  up: ArrowUp,\n  down: ArrowDown,\n  flat: Minus,\n} as const\n\n// 入场动画：纯 CSS，服务端直接输出最终状态，没有脚本也完整显示。\n// 前 12 行每行晚 50ms 依次淡入，之后的行与第 12 行同时出现；系统开启「减少动态效果」时不播放。\nconst STAGGER = Array.from(\n  { length: 12 },\n  (_, i) =>\n    `[data-slot=\"change-summary-item\"]:nth-child(${i + 1}){animation-delay:${i * 50}ms}`,\n).join(\"\\n\")\n\nconst MOTION_CSS = `@media (prefers-reduced-motion: no-preference) {\n@keyframes change-summary-in { from { opacity: 0; transform: translateY(4px) } }\n[data-slot=\"change-summary-item\"] { animation: change-summary-in 0.3s cubic-bezier(0.22, 1, 0.36, 1) both }\n${STAGGER}\n[data-slot=\"change-summary-item\"]:nth-child(n+13) { animation-delay: 550ms }\n}`\n\ntype ChangeSummaryProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  /**\n   * 标题；不传时用 `labels.title`（默认「改动摘要」）。\n   * 只有传了 `changes`、`title`、`icon`、`period` 之一时才渲染默认头部与列表，否则只渲染外壳，内容全部由 `children` 组合\n   */\n  title?: React.ReactNode\n  /** 标题前的图标，放一个 lucide 图标即可 */\n  icon?: React.ReactNode\n  /** 右侧的周期文字，如「本周」 */\n  period?: React.ReactNode\n  /** 变化列表；是纯数据，可以来自接口或聊天消息 */\n  changes?: ChangeSummaryChange[]\n  /** 覆盖默认文案（标题、读屏文字） */\n  labels?: Partial<ChangeSummaryLabels>\n}\n\n/**\n * 改动摘要卡片：一个标题、一个周期，下面逐行列出指标的变化。\n * 两种写法：传 `changes` 数组一步到位；或者不传，用 `ChangeSummaryHeader`、`ChangeSummaryList`、\n * `ChangeSummaryItem` 自由组合。\n */\nfunction ChangeSummary({\n  className,\n  title,\n  icon,\n  period,\n  changes,\n  labels,\n  children,\n  ...props\n}: ChangeSummaryProps) {\n  const text = { ...DEFAULT_LABELS, ...labels }\n  const auto = changes !== undefined || title || icon || period\n  return (\n    <div\n      data-slot=\"change-summary\"\n      className={cn(\n        \"@container flex w-full min-w-0 flex-col rounded-xl border border-edge bg-background text-sm text-card-foreground shadow-xs\",\n        className,\n      )}\n      {...props}\n    >\n      <ChangeSummaryMotion />\n      {auto ? (\n        <>\n          <ChangeSummaryHeader>\n            {icon ? <ChangeSummaryIcon>{icon}</ChangeSummaryIcon> : null}\n            <ChangeSummaryTitle>{title ?? text.title}</ChangeSummaryTitle>\n            {period ? <ChangeSummaryPeriod>{period}</ChangeSummaryPeriod> : null}\n          </ChangeSummaryHeader>\n          {changes && changes.length > 0 ? (\n            <ChangeSummaryList>\n              {changes.map((item, index) => (\n                <ChangeSummaryItem key={`${item.label}-${index}`} {...item} labels={labels} />\n              ))}\n            </ChangeSummaryList>\n          ) : null}\n        </>\n      ) : null}\n      {children}\n    </div>\n  )\n}\n\n/** 样式由 React 19 按 `href` 去重并提升到 `<head>`，同一页放多少张卡片都只有一份。 */\nfunction ChangeSummaryMotion() {\n  return React.createElement(\n    \"style\",\n    { href: \"change-summary-motion\", precedence: \"default\" },\n    MOTION_CSS,\n  )\n}\n\n/** 头部：图标、标题、周期排成一行，下方一条细线。 */\nfunction ChangeSummaryHeader({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"change-summary-header\"\n      className={cn(\n        \"flex items-center gap-2 border-b border-edge px-4 py-3\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/** 标题前的图标，装饰性，不进读屏。 */\nfunction ChangeSummaryIcon({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"change-summary-icon\"\n      aria-hidden=\"true\"\n      className={cn(\n        \"flex shrink-0 text-muted-foreground [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/** 标题，渲染成 `h3`；页面的标题层级不同时，直接用 `<ChangeSummaryTitle>` 的位置换成自己的标题元素。 */\nfunction ChangeSummaryTitle({\n  className,\n  ...props\n}: React.ComponentProps<\"h3\">) {\n  return (\n    <h3\n      data-slot=\"change-summary-title\"\n      className={cn(\"min-w-0 truncate text-sm leading-5 font-medium\", className)}\n      {...props}\n    />\n  )\n}\n\n/** 右侧的周期文字，如「本周」。 */\nfunction ChangeSummaryPeriod({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"change-summary-period\"\n      className={cn(\n        \"ml-auto shrink-0 text-xs text-muted-foreground tabular-nums\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/** 变化列表：`dl`，每一行是 `ChangeSummaryItem`；行与行之间用细线分隔。 */\nfunction ChangeSummaryList({\n  className,\n  ...props\n}: React.ComponentProps<\"dl\">) {\n  return (\n    <dl\n      data-slot=\"change-summary-list\"\n      className={cn(\"m-0 flex flex-col divide-y divide-edge\", className)}\n      {...props}\n    />\n  )\n}\n\ntype ChangeSummaryItemProps = Omit<React.ComponentProps<\"div\">, \"children\"> &\n  ChangeSummaryChange & {\n    /** 覆盖读屏文字（上升 / 下降 / 持平、好转 / 恶化 / 中性） */\n    labels?: Partial<ChangeSummaryLabels>\n  }\n\n/**\n * 一行变化：`dt` 是指标名，`dd` 是趋势标记加一句话。\n * 方向和好坏分开：标记的箭头表示方向，颜色表示好坏；读屏会念出「方向，好坏：」，不只靠颜色。\n * 窄于 24rem 时指标名在上、变化在下；更宽时左右分布。\n */\nfunction ChangeSummaryItem({\n  className,\n  label,\n  change,\n  tone,\n  direction,\n  labels,\n  ...props\n}: ChangeSummaryItemProps) {\n  const text = { ...DEFAULT_LABELS, ...labels }\n  const resolved = direction ?? TONE_DIRECTIONS[tone]\n  const Icon = DIRECTION_ICONS[resolved]\n  const mark = text.mark\n    .replace(\"{direction}\", text[resolved])\n    .replace(\"{tone}\", text[tone])\n  return (\n    <div\n      data-slot=\"change-summary-item\"\n      data-tone={tone}\n      data-direction={resolved}\n      className={cn(\n        \"flex min-w-0 flex-col gap-1 px-4 py-3 @sm:flex-row @sm:items-baseline @sm:justify-between @sm:gap-4\",\n        className,\n      )}\n      {...props}\n    >\n      <dt\n        data-slot=\"change-summary-label\"\n        className=\"min-w-0 text-[13px] leading-5 text-muted-foreground\"\n      >\n        {label}\n      </dt>\n      <dd\n        data-slot=\"change-summary-change\"\n        className=\"m-0 flex min-w-0 items-start gap-2 leading-5 @sm:max-w-[65%] @sm:justify-end @sm:text-right\"\n      >\n        <span\n          data-slot=\"change-summary-mark\"\n          aria-hidden=\"true\"\n          style={{ \"--change-tone\": toneColor(TONE_COLORS[tone]) } as React.CSSProperties}\n          className=\"flex size-5 shrink-0 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--change-tone)_14%,transparent)] text-[color:var(--change-tone)] @sm:order-2\"\n        >\n          <Icon className=\"size-3.5\" />\n        </span>\n        <span className=\"sr-only\">{mark}</span>\n        <span className=\"min-w-0 wrap-break-word text-foreground\">{change}</span>\n      </dd>\n    </div>\n  )\n}\n\nexport {\n  ChangeSummary,\n  ChangeSummaryHeader,\n  ChangeSummaryIcon,\n  ChangeSummaryItem,\n  ChangeSummaryList,\n  ChangeSummaryPeriod,\n  ChangeSummaryTitle,\n}\nexport type {\n  ChangeDirection,\n  ChangeSummaryChange,\n  ChangeSummaryItemProps,\n  ChangeSummaryLabels,\n  ChangeSummaryProps,\n  ChangeTone,\n}\n",
      "type": "registry:ui",
      "target": "@ui/change-summary/change-summary.tsx"
    },
    {
      "path": "src/registry/ui/change-summary/config.ts",
      "content": "import type { Trend } from \"@/lib/data-display\"\n\n/** 好坏：与涨跌方向分开表达。错误率下降是 `good` 加 `down`。 */\ntype ChangeTone = \"good\" | \"bad\" | \"neutral\"\n\n/** 方向；不传时按 `tone` 推断：good 向上、bad 向下、neutral 持平。 */\ntype ChangeDirection = Trend\n\n/**\n * 一行变化。只含字符串，可以直接 JSON 序列化，\n * 方便放进接口返回值或聊天消息的数据块里传输。\n */\ntype ChangeSummaryChange = {\n  /** 指标名，如「错误率」 */\n  label: string\n  /** 一句话描述变化，如「下降 0.3 个百分点，至 1.2%」 */\n  change: string\n  /** 好坏：`good` 好转、`bad` 恶化、`neutral` 没有好坏之分 */\n  tone: ChangeTone\n  /** 方向；不传时按 `tone` 推断 */\n  direction?: ChangeDirection\n}\n\ntype ChangeSummaryLabels = {\n  /** 没有传 `title` 时的默认标题 */\n  title: string\n  up: string\n  down: string\n  flat: string\n  good: string\n  bad: string\n  neutral: string\n  /**\n   * 读屏文字模板，放在每行变化文字前面，`{direction}` 和 `{tone}` 会被替换成上面的方向与好坏文字。\n   * 英文可以写成 `\"{direction}, {tone}: \"`\n   */\n  mark: string\n}\n\nconst DEFAULT_LABELS: ChangeSummaryLabels = {\n  title: \"改动摘要\",\n  up: \"上升\",\n  down: \"下降\",\n  flat: \"持平\",\n  good: \"好转\",\n  bad: \"恶化\",\n  neutral: \"中性\",\n  mark: \"{direction}，{tone}：\",\n}\n\nconst TONE_DIRECTIONS: Record<ChangeTone, ChangeDirection> = {\n  good: \"up\",\n  bad: \"down\",\n  neutral: \"flat\",\n}\n\nexport { DEFAULT_LABELS, TONE_DIRECTIONS }\nexport type {\n  ChangeDirection,\n  ChangeSummaryChange,\n  ChangeSummaryLabels,\n  ChangeTone,\n}\n",
      "type": "registry:ui",
      "target": "@ui/change-summary/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"
}
