{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"metric-card","type":"registry:ui","title":"Metric Card","description":"One number that matters, with its change against a past period and room for a sparkline.","dependencies":["lucide-react"],"registryDependencies":["https://dashral.com/r/utils.json","https://dashral.com/r/lib-locale.json","https://dashral.com/r/lib-motion.json","https://dashral.com/r/card.json"],"files":[{"path":"registry/dashral/metric-card.tsx","type":"registry:file","target":"components/dashral/metric-card.tsx","content":"\"use client\"\n\nimport * as React from \"react\"\nimport { ArrowDownRight, ArrowUpRight } from \"lucide-react\"\n\nimport { useLocale } from \"./lib/locale\"\nimport { useCountTo } from \"./lib/motion\"\nimport { cn } from \"./lib/utils\"\nimport { Card } from \"./card\"\n\ntype MetricCardProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  label: React.ReactNode\n  /** null when there's nothing to show yet: the card reads \"—\". */\n  value: number | null\n  /** How the value is written, e.g. `{ style: \"currency\", currency: \"USD\" }` or `{ style: \"percent\" }`. */\n  formatOptions?: Intl.NumberFormatOptions\n  /** Writes the value yourself, for what Intl can't (durations like \"2m 05s\"). Overrides formatOptions; client only. */\n  formatValue?: (value: number) => string\n  /** BCP 47 locale, e.g. \"id-ID\". */\n  locale?: string\n  /** Change in percent against the comparison period, e.g. 12.5 or -3. */\n  change?: number | null\n  /** After the change's number: \"%\" by default, \" pts\" for a change in a rate. */\n  changeUnit?: string\n  /** What the change compares with, e.g. \"vs last month\". */\n  changeLabel?: React.ReactNode\n  /** For metrics where down is good (churn, costs, response time): a fall shows in green. */\n  invert?: boolean\n  icon?: React.ReactNode\n  /** Under the numbers, e.g. a Sparkline. */\n  children?: React.ReactNode\n  /**\n   * Count up from zero when the card first shows (a dashboard's opening). By default only later changes count.\n   * The server renders the final value, so it's there without JavaScript.\n   */\n  countOnMount?: boolean\n}\n\n/** One number that matters, with its change against a past period. Put a Sparkline in it for the trend. */\nfunction MetricCard({\n  label,\n  value,\n  formatOptions,\n  formatValue,\n  locale: localeProp,\n  change,\n  changeUnit = \"%\",\n  changeLabel,\n  invert = false,\n  icon,\n  children,\n  countOnMount = false,\n  className,\n  ...props\n}: MetricCardProps) {\n  const { locale: contextLocale } = useLocale()\n  const locale = localeProp ?? contextLocale\n  const shown = useCountTo(value ?? 0, countOnMount && value !== null)\n  const intl = React.useMemo(() => new Intl.NumberFormat(locale, formatOptions), [locale, formatOptions])\n  const format = formatValue ?? intl.format\n  const up = (change ?? 0) >= 0\n  const good = invert ? !up : up\n  const Arrow = up ? ArrowUpRight : ArrowDownRight\n\n  return (\n    // A Card underneath, so it looks and follows themes (and the dashboard's card style setting) like every other card.\n    <Card data-metric-card=\"\" padding=\"none\" className={cn(\"p-5\", className)} classNames={{ body: \"flex flex-col gap-3\" }} {...props}>\n      <div className=\"flex items-start justify-between gap-3\">\n        <p className=\"text-sm text-muted-foreground\">{label}</p>\n        {icon && (\n          <span aria-hidden className=\"flex size-8 shrink-0 items-center justify-center rounded-md bg-primary/12 text-primary-text [&_svg:not([class*='size-'])]:size-4\">\n            {icon}\n          </span>\n        )}\n      </div>\n      <div>\n        {/* The live number is for sight; screen readers get the final value once. */}\n        <p className=\"text-2xl font-semibold tracking-tight tabular-nums\">\n          {value === null ? (\n            \"—\"\n          ) : (\n            <>\n              <span aria-hidden>{format(shown)}</span>\n              <span className=\"sr-only\">{format(value)}</span>\n            </>\n          )}\n        </p>\n        {(change !== undefined && change !== null) || changeLabel ? (\n          <p className=\"mt-1 flex flex-wrap items-center gap-1 text-xs text-muted-foreground\">\n            {change !== undefined && change !== null && (\n              <span className={cn(\"inline-flex items-center gap-0.5 font-medium\", good ? \"text-success-text\" : \"text-destructive-text\")}>\n                <Arrow className=\"size-3.5\" aria-hidden />\n                <span className=\"sr-only\">{up ? \"Up\" : \"Down\"} </span>\n                {/* Sign, number and unit read left to right in every language: \"+8.5%\", never \"8.5%+\" in Arabic. */}\n                <span dir=\"ltr\">\n                  {up ? \"+\" : \"−\"}\n                  {Math.abs(change).toLocaleString(locale, { maximumFractionDigits: 1 })}\n                  {changeUnit}\n                </span>\n              </span>\n            )}\n            {changeLabel}\n          </p>\n        ) : null}\n      </div>\n      {children}\n    </Card>\n  )\n}\n\nexport { MetricCard }\n"}],"cssVars":{"theme":{"color-primary-text":"var(--primary-text, var(--primary))","color-destructive-foreground":"var(--destructive-foreground)","color-destructive-text":"var(--destructive-text)","color-success":"var(--success)","color-success-foreground":"var(--success-foreground)","color-success-text":"var(--success-text)"},"light":{"destructive-foreground":"oklch(0.985 0 0)","destructive-text":"#bd2800","success":"oklch(0.508 0.118 165.612)","success-foreground":"oklch(0.985 0 0)","success-text":"oklch(0.432 0.095 166.913)"},"dark":{"destructive-foreground":"oklch(0.2 0 0)","destructive-text":"#ff8a5c","success":"oklch(0.696 0.17 162.48)","success-foreground":"oklch(0.2 0.04 160)","success-text":"oklch(0.765 0.177 163.223)"}}}