{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"badge-clean","type":"registry:ui","title":"Badge (Clean)","description":"A small status label that carries the color – six soft tones, a leading dot or icon, a live dot that pulses while something runs and a round count up to 99+","dependencies":[],"files":[{"path":"registry/badge-clean/badge-clean.tsx","type":"registry:ui","content":"\"use client\";\n\nimport { createElement, type ComponentPropsWithRef, type ReactNode } from \"react\";\n\n/*\n * Badge – a single self-contained file, clean style.\n * A small status label – the one place a status gets its color: a soft tint with matching text, a leading dot or icon,\n * a live dot that pulses softly while something runs, or a round count like 3 or 99+.\n * The styles are injected below, so no extra files are needed.\n */\n\n/** An icon from Hugeicons (e.g. `import { Clock01Icon } from \"@hugeicons/core-free-icons\"`) – drawn inline */\nexport type HugeIcon = readonly (readonly [string, { readonly [key: string]: string | number }])[];\n\nexport type BadgeTone = \"neutral\" | \"info\" | \"success\" | \"warning\" | \"danger\" | \"purple\";\n\nexport type BadgeProps = ComponentPropsWithRef<\"span\"> & {\n  /** neutral: gray, the default · info: blue, running · success: green, done · warning: orange, needs you · danger: red, failed · purple: a kind, like Beta */\n  tone?: BadgeTone;\n  /** sm: 18px high, 11px text · md: 20px high, 12px text */\n  size?: \"sm\" | \"md\";\n  /** A small dot in the tone's color before the text */\n  dot?: boolean;\n  /** The dot pulses softly – for running, live and recording states */\n  live?: boolean;\n  /** An icon before the text: any element (an inline SVG) or a Hugeicons icon object */\n  icon?: ReactNode | HugeIcon;\n  /** Makes it a round count: the number in place of the text, \"99+\" above max */\n  count?: number;\n  /** The highest count shown in full – above it the badge reads max+ */\n  max?: number;\n};\n\nexport function Badge({ tone = \"neutral\", size = \"sm\", dot, live, icon, count, max = 99, className, children, ...rest }: BadgeProps) {\n  // Unknown values fall back to the defaults instead of an unstyled badge\n  const hue = TONES.includes(tone) ? tone : \"neutral\";\n  const scale = size === \"md\" ? \"md\" : \"sm\";\n  const isCount = typeof count === \"number\" && Number.isFinite(count);\n  const lead = isCount ? null : live || dot ? \"dot\" : icon != null && icon !== false ? \"icon\" : null;\n  // Nothing to say – no text, no count, no mark: no empty pill either (e.g. count={NaN} from missing data)\n  const empty = !isCount && !lead && (children == null || children === false || children === \"\");\n  if (empty) return null;\n\n  return (\n    <>\n      {/* React 19 hoists this into <head> once, no matter how many badges render */}\n      <style href=\"ui-badge-clean\" precedence=\"default\">\n        {css}\n      </style>\n      <span\n        className={[\"ui-bdgc\", className].filter(Boolean).join(\" \")}\n        data-tone={hue}\n        data-size={scale}\n        data-count={isCount ? \"\" : undefined}\n        data-lead={lead ?? undefined}\n        {...rest}\n      >\n        {lead === \"dot\" && <span className=\"ui-bdgc__dot\" data-live={live ? \"\" : undefined} aria-hidden=\"true\" />}\n        {lead === \"icon\" && (\n          <span className=\"ui-bdgc__icon\" aria-hidden=\"true\">\n            <Glyph icon={icon} />\n          </span>\n        )}\n        {isCount ? formatCount(count, max) : <span className=\"ui-bdgc__label\">{children}</span>}\n      </span>\n    </>\n  );\n}\n\nconst TONES: BadgeTone[] = [\"neutral\", \"info\", \"success\", \"warning\", \"danger\", \"purple\"];\n\n/** 7 · 1,234 · 99+ – whole numbers, never below zero, in the reader's own digits */\nconst formatCount = (count: number, max: number) => {\n  const n = Math.max(0, Math.floor(count));\n  const top = Math.max(0, Math.floor(Number.isFinite(max) ? max : 99));\n  const format = (v: number) => new Intl.NumberFormat().format(v);\n  return n > top ? `${format(top)}+` : format(n);\n};\n\nconst isHugeIcon = (icon: unknown): icon is HugeIcon =>\n  Array.isArray(icon) && icon.length > 0 && icon.every((part) => Array.isArray(part) && typeof part[0] === \"string\");\n\n/** Draws a Hugeicons icon object as an inline SVG at the badge's stroke; anything else is shown as it is */\nfunction Glyph({ icon }: { icon: ReactNode | HugeIcon }) {\n  if (!isHugeIcon(icon)) return <>{icon}</>;\n  return (\n    <svg viewBox=\"0 0 24 24\" fill=\"none\" focusable=\"false\">\n      {icon.map(([tag, { key, ...attrs }], i) =>\n        createElement(tag, { ...attrs, key: String(key ?? i), ...(\"strokeWidth\" in attrs ? { strokeWidth: STROKE } : null) }),\n      )}\n    </svg>\n  );\n}\nconst STROKE = 2;\n\n/* ---------- Styles ---------- */\n\nconst css = /* css */ `\n/* Colors as variables – override them on .ui-bdgc (see Theming in the docs) */\n.ui-bdgc {\n  --ui-bdgc-neutral-bg: var(--aui-subtle, rgba(0, 0, 0, 0.06));\n  --ui-bdgc-neutral-fg: rgba(0, 0, 0, 0.61);\n  --ui-bdgc-neutral-dot: rgba(0, 0, 0, 0.3);\n  --ui-bdgc-info-bg: var(--aui-info-soft, #008ff519);\n  --ui-bdgc-info-fg: var(--aui-info, #006dcbf2);\n  --ui-bdgc-success-bg: var(--aui-success-soft, #00a43319);\n  --ui-bdgc-success-fg: var(--aui-success, #007d2ee0);\n  --ui-bdgc-warning-bg: var(--aui-warning-soft, #ff9c0029);\n  --ui-bdgc-warning-fg: var(--aui-warning, #b25e00);\n  --ui-bdgc-danger-bg: var(--aui-danger-soft, #f52b0018);\n  --ui-bdgc-danger-fg: var(--aui-danger, #d72400da);\n  --ui-bdgc-purple-bg: color-mix(in srgb, var(--aui-purple, #a651f0) 11%, transparent);\n  --ui-bdgc-purple-fg: #8a35d6;\n\n  --bg: var(--ui-bdgc-neutral-bg);\n  --fg: var(--ui-bdgc-neutral-fg);\n  --dot: currentColor;\n  --h: 18px;\n  --pad: 6px;\n  --lead: 4px;\n  --mark: 12px;\n\n  display: inline-flex;\n  flex: none;\n  align-items: center;\n  gap: 4px;\n  box-sizing: border-box;\n  max-width: 100%;\n  height: var(--h);\n  padding: 0 var(--pad);\n  border-radius: calc(var(--aui-radius-sm, 5px) - 1px);\n  background: var(--bg);\n  color: var(--fg);\n  font-family: var(--aui-font, \"Inter\", \"InterVariable\", ui-sans-serif, system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif);\n  font-size: 11px;\n  font-weight: 500;\n  line-height: 16px;\n  letter-spacing: 0;\n  white-space: nowrap;\n  vertical-align: middle;\n  font-variant-numeric: tabular-nums;\n  -webkit-font-smoothing: antialiased;\n}\n.ui-bdgc[data-size=\"md\"] {\n  --h: 20px;\n  --pad: 7px;\n  --lead: 5px;\n  --mark: 13px;\n  gap: 5px;\n  border-radius: var(--aui-radius-sm, 5px);\n  font-size: 12px;\n}\n/* A dot or icon in front sits a little closer to the edge, so it looks as far in as the text on the other side */\n.ui-bdgc[data-lead] {\n  padding-inline-start: var(--lead);\n}\n\n.ui-bdgc[data-tone=\"neutral\"] { --dot: var(--ui-bdgc-neutral-dot); }\n.ui-bdgc[data-tone=\"info\"] { --bg: var(--ui-bdgc-info-bg); --fg: var(--ui-bdgc-info-fg); }\n.ui-bdgc[data-tone=\"success\"] { --bg: var(--ui-bdgc-success-bg); --fg: var(--ui-bdgc-success-fg); }\n.ui-bdgc[data-tone=\"warning\"] { --bg: var(--ui-bdgc-warning-bg); --fg: var(--ui-bdgc-warning-fg); }\n.ui-bdgc[data-tone=\"danger\"] { --bg: var(--ui-bdgc-danger-bg); --fg: var(--ui-bdgc-danger-fg); }\n.ui-bdgc[data-tone=\"purple\"] { --bg: var(--ui-bdgc-purple-bg); --fg: var(--ui-bdgc-purple-fg); }\n\n/* Long text ends in … instead of pushing the row wider */\n.ui-bdgc__label {\n  min-width: 0;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.ui-bdgc__dot {\n  position: relative;\n  flex: none;\n  width: 6px;\n  height: 6px;\n  margin: 0 1px;\n  border-radius: 50%;\n  background: var(--dot);\n}\n/* Live: a soft halo grows out of the dot and fades, over and over */\n.ui-bdgc__dot[data-live]::after {\n  content: \"\";\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  background: var(--dot);\n  animation: ui-bdgc-live 1.8s cubic-bezier(0.22, 1, 0.36, 1) infinite;\n}\n@keyframes ui-bdgc-live {\n  0% { transform: scale(1); opacity: 0.5; }\n  70%, 100% { transform: scale(2.6); opacity: 0; }\n}\n\n.ui-bdgc__icon {\n  display: grid;\n  flex: none;\n  place-items: center;\n  width: var(--mark);\n  height: var(--mark);\n}\n.ui-bdgc__icon > svg,\n.ui-bdgc__icon > img {\n  display: block;\n  width: 100%;\n  height: 100%;\n}\n\n/* Count: round, at least as wide as it is high, the number centred */\n.ui-bdgc[data-count] {\n  justify-content: center;\n  min-width: var(--h);\n  padding: 0 5px;\n  border-radius: 999px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .ui-bdgc__dot[data-live]::after {\n    animation: none;\n    opacity: 0;\n  }\n}\n\n/* Dark mode: follows a .dark class or data-theme=\"dark\" on any ancestor, e.g. <html> */\n:where(.dark, [data-theme=\"dark\"]) .ui-bdgc {\n  --ui-bdgc-neutral-bg: var(--aui-subtle, rgba(255, 255, 255, 0.08));\n  --ui-bdgc-neutral-fg: rgba(255, 255, 255, 0.62);\n  --ui-bdgc-neutral-dot: rgba(255, 255, 255, 0.34);\n  --ui-bdgc-info-bg: var(--aui-info-soft, #0077ff3a);\n  --ui-bdgc-info-fg: var(--aui-info, #70b8ff);\n  --ui-bdgc-success-bg: var(--aui-success-soft, #00f04a1f);\n  --ui-bdgc-success-fg: var(--aui-success, #5fc88a);\n  --ui-bdgc-warning-bg: var(--aui-warning-soft, #fb6a0025);\n  --ui-bdgc-warning-fg: var(--aui-warning, #ffb35c);\n  --ui-bdgc-danger-bg: var(--aui-danger-soft, #ff35232b);\n  --ui-bdgc-danger-fg: var(--aui-danger, #ff8a7a);\n  --ui-bdgc-purple-bg: color-mix(in srgb, var(--aui-purple, #b98af0) 16%, transparent);\n  --ui-bdgc-purple-fg: #c9a2f5;\n}\n`;\n"}]}