{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "badge",
  "title": "Badge",
  "description": "A status or count chip. Counts roll their digits; a count-only badge pops in above zero and leaves when cleared; a live dot breathes slowly and stops under reduced motion.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "https://eased.dev/r/eased-tokens.json",
    "https://eased.dev/r/eased-motion.json",
    "https://eased.dev/r/eased-number-roll.json"
  ],
  "files": [
    {
      "path": "src/registry/eased/badge/badge.tsx",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useInView } from \"motion/react\";\nimport { useRef, type ReactNode } from \"react\";\nimport { NumberRoll } from \"../_primitives/number-roll/number-roll\";\nimport { exitFast, reducedFade, snappy, useSafeReducedMotion } from \"../lib/motion\";\n\nexport type BadgeTone = \"neutral\" | \"brand\" | \"success\" | \"warning\" | \"danger\" | \"info\";\n\nexport type BadgeProps = {\n  /** The label, e.g. \"Beta\" or \"Operational\". Optional when the badge is only a count. */\n  children?: ReactNode;\n  /** A number that rolls when it changes. Shown after the label. */\n  count?: number;\n  /** Counts above this show as `max+`. Default 99. */\n  max?: number;\n  /**\n   * A count-only badge leaves (scale and fade) when its count is 0, and returns when it rises.\n   * Keep it mounted and let this drive presence. Default false.\n   */\n  showZero?: boolean;\n  /** A status dot before the label. `\"live\"` breathes slowly (2.4s) and stops under reduced motion. */\n  dot?: boolean | \"live\";\n  tone?: BadgeTone;\n  /** 20px (`sm`, default) or 24px (`md`) tall. */\n  size?: \"sm\" | \"md\";\n  locale?: Intl.LocalesArgument;\n  /** Spoken in place of the visible text, e.g. \"3 unread messages\". */\n  \"aria-label\"?: string;\n  className?: string;\n};\n\n// Status badges are subtle fill + state text (R31); the dot takes the text colour.\nconst TONES: Record<BadgeTone, string> = {\n  neutral: \"bg-surface-active text-text-secondary\",\n  brand: \"bg-brand-subtle text-brand-text\",\n  success: \"bg-success-subtle text-success-text\",\n  warning: \"bg-warning-subtle text-warning-text\",\n  danger: \"bg-danger-subtle text-danger-text\",\n  info: \"bg-info-subtle text-info-text\",\n};\n\nconst SIZES = {\n  sm: { box: \"h-5 gap-1 px-1.5 text-ui-xs\", count: \"min-w-5\", dot: \"size-1.5\" },\n  md: { box: \"h-6 gap-1.5 px-2 text-ui-sm\", count: \"min-w-6\", dot: \"size-2\" },\n} as const;\n\n/**\n * A status or count chip. Counts roll their digits in the direction of change; a count-only\n * badge pops in when it rises above zero and leaves when it is cleared; a live dot breathes.\n */\nexport function Badge({\n  children,\n  count,\n  max = 99,\n  showZero = false,\n  dot = false,\n  tone = \"neutral\",\n  size = \"sm\",\n  locale,\n  \"aria-label\": ariaLabel,\n  className = \"\",\n}: BadgeProps) {\n  const reduced = useSafeReducedMotion();\n  const s = SIZES[size];\n  const hasLabel = children !== undefined && children !== null && children !== false;\n  const countOnly = !hasLabel && count !== undefined;\n  const visible = !countOnly || showZero || (count ?? 0) > 0;\n  const shown = count === undefined ? undefined : Math.min(count, max);\n  const over = count !== undefined && count > max;\n\n  return (\n    <AnimatePresence initial={false}>\n      {visible && (\n        <motion.span\n          initial={reduced ? { opacity: 0 } : { opacity: 0, scale: 0.85 }}\n          animate={{ opacity: 1, scale: 1 }}\n          exit={reduced ? { opacity: 0, transition: reducedFade } : { opacity: 0, scale: 0.9, transition: exitFast }}\n          transition={reduced ? reducedFade : snappy}\n          className={`inline-flex shrink-0 items-center justify-center font-medium whitespace-nowrap tabular-nums ${s.box} ${\n            countOnly ? `${s.count} rounded-pill` : \"rounded-control-xs\"\n          } ${TONES[tone]} ${className}`}\n        >\n          {ariaLabel && <span className=\"sr-only\">{ariaLabel}</span>}\n          {dot && <Dot live={dot === \"live\"} className={s.dot} />}\n          {hasLabel && (\n            <span aria-hidden={ariaLabel ? true : undefined} className=\"min-w-0 truncate\">\n              {children}\n            </span>\n          )}\n          {shown !== undefined && (\n            <span aria-hidden={ariaLabel ? true : undefined} className=\"inline-flex\">\n              <NumberRoll value={shown} locale={locale} aria-hidden={!!ariaLabel} />\n              {over && \"+\"}\n            </span>\n          )}\n        </motion.span>\n      )}\n    </AnimatePresence>\n  );\n}\n\nfunction Dot({ live, className }: { live: boolean; className: string }) {\n  const ref = useRef<HTMLSpanElement>(null);\n  const inView = useInView(ref);\n  return (\n    <span ref={ref} aria-hidden className={`relative shrink-0 rounded-pill bg-current ${className}`}>\n      {live && (\n        <>\n          {/* A halo the dot's colour that swells and fades back; it pauses off-screen and under reduced motion. */}\n          <span\n            className=\"absolute inset-0 animate-[eased-breathe_2400ms_var(--eased-ease-in-out)_infinite] rounded-pill bg-current opacity-0 reduced:animate-none\"\n            style={{ animationPlayState: inView ? \"running\" : \"paused\" }}\n          />\n          <style href=\"eased-badge\" precedence=\"default\">\n            {\"@keyframes eased-breathe { 0%, 100% { opacity: 0; scale: 1; } 50% { opacity: 0.35; scale: 2.25; } }\"}\n          </style>\n        </>\n      )}\n    </span>\n  );\n}\n",
      "type": "registry:ui",
      "target": "@components/eased/badge/badge.tsx"
    }
  ],
  "docs": "Import Badge from components/eased/badge/badge. The Eased tokens were merged into your global CSS; nothing to import. Dark mode follows .dark or data-theme=\"dark\".",
  "type": "registry:ui"
}