{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "delta-badge",
  "type": "registry:ui",
  "title": "Delta Badge",
  "description": "Signed change indicator with a direction icon, meaning-aware success or destructive tone (positiveDirection handles metrics where down is good), a neutral threshold, and a screen-reader comparison sentence.",
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://components.dethink.co.uk/r/dethink-base.json",
    "https://components.dethink.co.uk/r/badge.json",
    "https://components.dethink.co.uk/r/chart-core.json"
  ],
  "files": [
    {
      "path": "packages/components/src/components/delta-badge/delta-badge.tsx",
      "type": "registry:file",
      "target": "~/components/dethink/components/delta-badge/delta-badge.tsx",
      "content": "import { forwardRef, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"../../utils/cn\";\nimport { badgeClassNames, type BadgeSize, type BadgeTone } from \"../badge\";\nimport { formatChartValue, isFiniteValue } from \"../chart/chart-core\";\n\nexport type DeltaDirection = \"up\" | \"down\" | \"flat\";\nexport type DeltaMeaning = \"positive\" | \"negative\" | \"neutral\";\nexport type DeltaBadgeVariant = \"soft\" | \"outline\" | \"plain\";\nexport type DeltaBadgeSize = Extract<BadgeSize, \"xs\" | \"sm\" | \"md\">;\n\nexport interface DeltaBadgeProps extends Omit<\n  HTMLAttributes<HTMLSpanElement>,\n  \"children\"\n> {\n  /** Signed change. With the default formatter this is percentage points: 12.4 → \"12.4%\". */\n  value: number | null | undefined;\n  /** Which direction is good. Use \"down\" for churn, latency, cost or errors. */\n  positiveDirection?: \"up\" | \"down\";\n  /** Changes whose magnitude is at or below this read as flat and neutral. */\n  neutralThreshold?: number;\n  /** Formats the magnitude; the sign and direction are added for you. */\n  formatValue?: (magnitude: number) => string;\n  /** Comparison period for assistive technology, for example \"vs last month\". */\n  comparison?: string;\n  variant?: DeltaBadgeVariant;\n  size?: DeltaBadgeSize;\n}\n\nexport interface DeltaState {\n  direction: DeltaDirection;\n  meaning: DeltaMeaning;\n}\n\n/** Direction comes from the sign; meaning also depends on which way is good. */\nexport function getDeltaState(\n  value: number | null | undefined,\n  { positiveDirection = \"up\", neutralThreshold = 0 } = {},\n): DeltaState {\n  if (!isFiniteValue(value) || Math.abs(value) <= neutralThreshold) {\n    return { direction: \"flat\", meaning: \"neutral\" };\n  }\n\n  const direction = value > 0 ? \"up\" : \"down\";\n  return {\n    direction,\n    meaning: direction === positiveDirection ? \"positive\" : \"negative\",\n  };\n}\n\nconst defaultFormat = (magnitude: number) =>\n  `${formatChartValue(magnitude, { maximumFractionDigits: 1 })}%`;\n\nconst meaningTone: Record<DeltaMeaning, BadgeTone> = {\n  positive: \"success\",\n  negative: \"destructive\",\n  neutral: \"neutral\",\n};\n\nconst iconToneClasses: Record<DeltaMeaning, string> = {\n  positive: \"text-success\",\n  negative: \"text-destructive\",\n  neutral: \"text-muted-foreground\",\n};\n\nconst plainClasses =\n  \"inline-flex max-w-full shrink-0 items-center gap-1 whitespace-nowrap font-medium leading-none text-foreground\";\n\nconst plainSizeClasses: Record<DeltaBadgeSize, string> = {\n  xs: \"text-[0.6875rem]\",\n  sm: \"text-xs\",\n  md: \"text-sm\",\n};\n\nconst iconSizeClasses: Record<DeltaBadgeSize, string> = {\n  xs: \"size-3\",\n  sm: \"size-3.5\",\n  md: \"size-4\",\n};\n\nconst directionWords: Record<DeltaDirection, string> = {\n  up: \"Up\",\n  down: \"Down\",\n  flat: \"No change\",\n};\n\nfunction DeltaIcon({\n  direction,\n  className,\n}: {\n  direction: DeltaDirection;\n  className: string;\n}) {\n  return (\n    <svg\n      aria-hidden=\"true\"\n      focusable=\"false\"\n      viewBox=\"0 0 16 16\"\n      fill=\"none\"\n      stroke=\"currentColor\"\n      strokeWidth={1.75}\n      strokeLinecap=\"round\"\n      strokeLinejoin=\"round\"\n      className={cn(\"shrink-0\", className)}\n    >\n      {direction === \"up\" ? (\n        <path d=\"M4.5 11.5 11.5 4.5M6 4.5h5.5V10\" />\n      ) : direction === \"down\" ? (\n        <path d=\"M4.5 4.5 11.5 11.5M11.5 6v5.5H6\" />\n      ) : (\n        <path d=\"M3.5 8h9\" />\n      )}\n    </svg>\n  );\n}\n\n/** Build the text a screen reader hears, for example \"Up 12.4% vs last month\". */\nexport function describeDelta(\n  value: number | null | undefined,\n  {\n    comparison,\n    formatValue = defaultFormat,\n    neutralThreshold = 0,\n  }: Pick<\n    DeltaBadgeProps,\n    \"comparison\" | \"formatValue\" | \"neutralThreshold\"\n  > = {},\n): string {\n  const suffix = comparison ? ` ${comparison}` : \"\";\n  if (!isFiniteValue(value)) return `No change data${suffix}`;\n\n  const { direction } = getDeltaState(value, { neutralThreshold });\n  if (direction === \"flat\") return `No change${suffix}`;\n  return `${directionWords[direction]} ${formatValue(Math.abs(value))}${suffix}`;\n}\n\nexport const DeltaBadge = forwardRef<HTMLSpanElement, DeltaBadgeProps>(\n  (\n    {\n      className,\n      comparison,\n      formatValue = defaultFormat,\n      neutralThreshold = 0,\n      positiveDirection = \"up\",\n      size = \"sm\",\n      value,\n      variant = \"soft\",\n      ...props\n    },\n    ref,\n  ) => {\n    const { direction, meaning } = getDeltaState(value, {\n      neutralThreshold,\n      positiveDirection,\n    });\n    const sign = direction === \"up\" ? \"+\" : direction === \"down\" ? \"−\" : \"\";\n    let visible: ReactNode = \"—\";\n    if (isFiniteValue(value)) {\n      visible = `${sign}${formatValue(direction === \"flat\" ? 0 : Math.abs(value))}`;\n    }\n\n    return (\n      <span\n        {...props}\n        ref={ref}\n        data-slot=\"delta-badge\"\n        data-direction={direction}\n        data-meaning={meaning}\n        data-variant={variant}\n        className={\n          variant === \"plain\"\n            ? cn(plainClasses, plainSizeClasses[size], className)\n            : badgeClassNames({\n                className: cn(\"tabular-nums\", className),\n                size,\n                tone: meaningTone[meaning],\n                variant,\n              })\n        }\n      >\n        {isFiniteValue(value) ? (\n          <DeltaIcon\n            direction={direction}\n            className={cn(iconSizeClasses[size], iconToneClasses[meaning])}\n          />\n        ) : null}\n        {/* Signed numbers stay left-to-right inside right-to-left text. */}\n        <span aria-hidden=\"true\" dir=\"ltr\" className=\"tabular-nums\">\n          {visible}\n        </span>\n        <span className=\"sr-only\">\n          {describeDelta(value, { comparison, formatValue, neutralThreshold })}\n        </span>\n      </span>\n    );\n  },\n);\n\nDeltaBadge.displayName = \"DeltaBadge\";\n"
    },
    {
      "path": "packages/components/src/components/delta-badge/index.ts",
      "type": "registry:file",
      "target": "~/components/dethink/components/delta-badge/index.ts",
      "content": "export {\n  DeltaBadge,\n  describeDelta,\n  getDeltaState,\n  type DeltaBadgeProps,\n  type DeltaBadgeSize,\n  type DeltaBadgeVariant,\n  type DeltaDirection,\n  type DeltaMeaning,\n  type DeltaState,\n} from \"./delta-badge\";\n"
    },
    {
      "path": "packages/components/src/utils/cn.ts",
      "type": "registry:file",
      "target": "~/components/dethink/utils/cn.ts",
      "content": "import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\nexport function cn(...inputs: ClassValue[]) {\n  return twMerge(clsx(inputs));\n}\n"
    }
  ]
}
