{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "delta-badge",
  "title": "Delta Badge",
  "description": "A signed percentage change with a trend icon, colored by whether the change is good.",
  "dependencies": [
    "lucide-react"
  ],
  "files": [
    {
      "path": "registry/dashboardcn/ui/delta-badge.tsx",
      "content": "import * as React from \"react\"\nimport { Minus, TrendingDown, TrendingUp } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { formatDelta } from \"@/registry/dashboardcn/lib/format\"\n\nexport type DeltaDirection = \"up\" | \"down\" | \"flat\"\n\nexport function getDeltaDirection(delta: number | undefined): DeltaDirection {\n  if (delta === undefined || delta === 0 || !Number.isFinite(delta)) return \"flat\"\n  return delta > 0 ? \"up\" : \"down\"\n}\n\nconst directionIcon: Record<DeltaDirection, React.ElementType> = {\n  up: TrendingUp,\n  down: TrendingDown,\n  flat: Minus,\n}\n\nexport interface DeltaBadgeProps extends React.ComponentProps<\"span\"> {\n  /** Fractional change, e.g. 0.124 for +12.4%. */\n  delta: number\n  /** Treat a decrease as good and an increase as bad (churn, latency, errors). */\n  invert?: boolean\n  variant?: \"outline\" | \"soft\" | \"text\"\n  showIcon?: boolean\n}\n\nfunction DeltaBadge({\n  delta,\n  invert = false,\n  variant = \"outline\",\n  showIcon = true,\n  className,\n  children,\n  ...props\n}: DeltaBadgeProps) {\n  const direction = getDeltaDirection(delta)\n  const positive = direction === \"flat\" ? null : (direction === \"up\") !== invert\n  const Icon = directionIcon[direction]\n\n  return (\n    <span\n      data-slot=\"delta-badge\"\n      data-direction={direction}\n      data-positive={positive}\n      className={cn(\n        \"inline-flex items-center gap-1 text-xs font-medium tabular-nums\",\n        variant === \"outline\" && \"rounded-md border px-1.5 py-0.5\",\n        variant === \"soft\" && \"rounded-md px-1.5 py-0.5\",\n        variant === \"soft\" && positive === true && \"bg-emerald-500/10\",\n        variant === \"soft\" && positive === false && \"bg-red-500/10\",\n        variant === \"soft\" && positive === null && \"bg-muted\",\n        positive === true && \"text-emerald-600 dark:text-emerald-400\",\n        positive === false && \"text-red-600 dark:text-red-400\",\n        positive === null && \"text-muted-foreground\",\n        className\n      )}\n      {...props}\n    >\n      {showIcon ? <Icon className=\"size-3\" aria-hidden=\"true\" /> : null}\n      {children ?? formatDelta(delta)}\n    </span>\n  )\n}\n\nexport { DeltaBadge }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/dashboardcn/lib/format.ts",
      "content": "export type NumberFormat = \"number\" | \"compact\" | \"currency\" | \"percent\"\n\nexport interface FormatNumberOptions {\n  format?: NumberFormat\n  /** ISO 4217 code, used when format is \"currency\". Defaults to USD. */\n  currency?: string\n  locale?: string\n  maximumFractionDigits?: number\n  /** Abbreviate large values, e.g. \"$158K\" instead of \"$158,143\". */\n  compact?: boolean\n}\n\nconst formatterCache = new Map<string, Intl.NumberFormat>()\n\nfunction getFormatter(locale: string, options: Intl.NumberFormatOptions) {\n  const key = locale + JSON.stringify(options)\n  let formatter = formatterCache.get(key)\n  if (!formatter) {\n    formatter = new Intl.NumberFormat(locale, options)\n    formatterCache.set(key, formatter)\n  }\n  return formatter\n}\n\n/**\n * Format a metric value for display.\n *\n * formatNumber(1234567)                          -> \"1,234,567\"\n * formatNumber(1234567, { format: \"compact\" })   -> \"1.2M\"\n * formatNumber(48.2, { format: \"currency\" })     -> \"$48.20\"\n * formatNumber(158143, { format: \"currency\", compact: true }) -> \"$158K\"\n * formatNumber(0.124, { format: \"percent\" })     -> \"12.4%\"\n */\nexport function formatNumber(\n  value: number,\n  {\n    format = \"number\",\n    currency = \"USD\",\n    locale = \"en-US\",\n    maximumFractionDigits,\n    compact = false,\n  }: FormatNumberOptions = {}\n): string {\n  if (!Number.isFinite(value)) return \"—\"\n\n  // Compact values keep three significant digits: $158K, $1.23M, 41.2K.\n  const compactOptions: Intl.NumberFormatOptions = compact\n    ? maximumFractionDigits === undefined\n      ? { notation: \"compact\", maximumSignificantDigits: 3 }\n      : { notation: \"compact\", maximumFractionDigits }\n    : {}\n  switch (format) {\n    case \"compact\":\n      return getFormatter(locale, {\n        notation: \"compact\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    case \"currency\":\n      return getFormatter(locale, {\n        style: \"currency\",\n        currency,\n        maximumFractionDigits: maximumFractionDigits ?? 2,\n        ...compactOptions,\n      }).format(value)\n    case \"percent\":\n      return getFormatter(locale, {\n        style: \"percent\",\n        maximumFractionDigits: maximumFractionDigits ?? 1,\n      }).format(value)\n    default:\n      return getFormatter(locale, {\n        maximumFractionDigits: maximumFractionDigits ?? 0,\n        ...compactOptions,\n      }).format(value)\n  }\n}\n\n/**\n * Format a fractional change as a signed percentage.\n *\n * formatDelta(0.124)  -> \"+12.4%\"\n * formatDelta(-0.03)  -> \"-3.0%\"\n * formatDelta(0)      -> \"0.0%\"\n */\nexport function formatDelta(delta: number, locale = \"en-US\"): string {\n  if (!Number.isFinite(delta)) return \"—\"\n  return getFormatter(locale, {\n    style: \"percent\",\n    signDisplay: \"exceptZero\",\n    maximumFractionDigits: 1,\n    minimumFractionDigits: 1,\n  }).format(delta)\n}\n\n/**\n * Fractional change between two values. Returns 0 when there is no baseline.\n */\nexport function computeDelta(current: number, previous: number): number {\n  if (!previous) return 0\n  return (current - previous) / Math.abs(previous)\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}