{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"badge","type":"registry:component","title":"Badge","description":"A status, a tag or a count: statuses ease their colour and morph their word, live dots breathe, counts roll and fold away at zero.","author":"Hairline UI","dependencies":[],"registryDependencies":["https://hairlineui.com/r/number-roll.json?as=dependency","https://hairlineui.com/r/text-morph.json?as=dependency","https://hairlineui.com/r/ui-interaction-motion.json?as=dependency"],"files":[{"path":"registry/badge.tsx","type":"registry:component","content":"// Hairline UI · badge · MIT License\n// Copyright (c) 2026 Hairline UI · https://hairlineui.com/license#components\n\n\"use client\";\n\nimport * as React from \"react\";\nimport { NumberRoll } from \"./number-roll\";\nimport { TextMorph } from \"./text-morph\";\nimport { EASE_OUT, useInteractionReducedMotion } from \"./ui-interaction-motion\";\n\n/* ─────────────────────────────────────────────────────────\n * BADGE: a status, a tag or a count\n *\n *   status   \"Pending\" → \"Paid\": the colours ease across and the\n *            word morphs, letters both share staying put\n *   live     a dot that breathes a ring outward (for \"Live\",\n *            \"Building\"); it rests when nothing is happening\n *   count    numbers roll the way they changed and cap at 99+;\n *            a count that drops to zero folds away\n * ───────────────────────────────────────────────────────── */\n\nexport type BadgeTone = \"neutral\" | \"primary\" | \"success\" | \"warning\" | \"danger\";\n\nexport interface BadgeProps extends Omit<React.HTMLAttributes<HTMLSpanElement>, \"children\"> {\n  children?: string;\n  tone?: BadgeTone;\n  /** Hairline only, no fill. */\n  outline?: boolean;\n  /** A dot before the text; `pulse` makes it breathe. */\n  dot?: boolean | \"pulse\";\n  /** A number instead of text: rolls, caps at `max`, folds away at 0 unless `showZero`. */\n  count?: number;\n  max?: number;\n  showZero?: boolean;\n  icon?: React.ReactNode;\n  /** 20px or 24px tall. */\n  size?: \"sm\" | \"md\";\n}\n\nconst FILL: Record<BadgeTone, string> = {\n  neutral: \"bg-muted text-foreground\",\n  primary: \"bg-primary/10 text-primary\",\n  success: \"bg-emerald-500/12 text-emerald-700 dark:text-emerald-400\",\n  warning: \"bg-amber-500/14 text-amber-700 dark:text-amber-400\",\n  danger: \"bg-red-500/10 text-red-600 dark:text-red-400\",\n};\nconst LINE: Record<BadgeTone, string> = {\n  neutral: \"text-foreground shadow-[inset_0_0_0_1px_var(--border)]\",\n  primary: \"text-primary shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--primary)_40%,transparent)]\",\n  success: \"text-emerald-700 dark:text-emerald-400 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-emerald-500)_40%,transparent)]\",\n  warning: \"text-amber-700 dark:text-amber-400 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-amber-500)_45%,transparent)]\",\n  danger: \"text-red-600 dark:text-red-400 shadow-[inset_0_0_0_1px_color-mix(in_oklab,var(--color-red-500)_40%,transparent)]\",\n};\nconst DOT: Record<BadgeTone, string> = { neutral: \"bg-muted-foreground\", primary: \"bg-primary\", success: \"bg-emerald-500\", warning: \"bg-amber-500\", danger: \"bg-red-500\" };\n\nexport function Badge({ children, tone = \"neutral\", outline, dot, count, max = 99, showZero, icon, size = \"md\", className = \"\", ...props }: BadgeProps) {\n  const reduced = useInteractionReducedMotion();\n  const isCount = count !== undefined;\n  const hidden = isCount && count <= 0 && !showZero;\n  const sm = size === \"sm\";\n\n  const badge = (\n    <span\n      className={`inline-flex shrink-0 items-center whitespace-nowrap rounded-full font-medium tabular-nums transition-[background-color,color,box-shadow] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] [&_svg]:size-3 ${\n        sm ? \"h-5 gap-1 px-1.5 text-[11px]\" : \"h-6 gap-1.5 px-2 text-[12px]\"\n      } ${isCount ? (sm ? \"min-w-5 justify-center\" : \"min-w-6 justify-center\") : \"\"} ${outline ? LINE[tone] : FILL[tone]} ${className}`}\n      {...props}\n    >\n      {dot && (\n        <span className=\"relative flex size-1.5 shrink-0\">\n          {dot === \"pulse\" && !reduced && <span className={`absolute inset-0 rounded-full ${DOT[tone]} [animation:ui-ping_1.6s_cubic-bezier(0,0,0.2,1)_infinite]`} />}\n          <span className={`relative size-1.5 rounded-full transition-colors duration-300 ${DOT[tone]}`} />\n        </span>\n      )}\n      {icon}\n      {isCount ? (\n        count > max ? (\n          <span>\n            <NumberRoll value={max} />+\n          </span>\n        ) : (\n          <NumberRoll value={Math.max(0, count)} />\n        )\n      ) : (\n        children && <TextMorph>{children}</TextMorph>\n      )}\n    </span>\n  );\n\n  if (!isCount) return badge;\n  // A count folds away at zero instead of blinking out.\n  return (\n    <span\n      className=\"inline-grid\"\n      aria-hidden={hidden || undefined}\n      style={{\n        gridTemplateColumns: hidden ? \"0fr\" : \"1fr\",\n        opacity: hidden ? 0 : 1,\n        scale: hidden && !reduced ? \"0.6\" : \"1\",\n        transition: reduced ? \"none\" : `grid-template-columns 320ms ${EASE_OUT}, opacity 200ms ${EASE_OUT}, scale 320ms ${EASE_OUT}`,\n      }}\n    >\n      <span className=\"min-w-0 overflow-hidden\">{badge}</span>\n    </span>\n  );\n}\n\nexport default Badge;\n"}],"css":{"@keyframes ui-ping":{"from":{"transform":"scale(1)","opacity":"0.55"},"to":{"transform":"scale(2.6)","opacity":"0"}}},"docs":"Docs and live preview: https://hairlineui.com/ui/badge. MIT licensed: https://hairlineui.com/license#components","meta":{"license":"MIT"}}