{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "badge",
  "title": "Badge",
  "description": "Non-interactive brand counter or dot that supplements an owning control or label with unread notification information.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/badge",
    "source": "src/wc/components/Badge/Badge.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Non-interactive brand counter or dot that supplements an owning control or label with unread notification information.",
      "useWhen": [
        "An existing control, navigation item, or label needs a compact unread notification count.",
        "An owning element needs a small visual dot indicating that supplemental notification information exists.",
        "When the badge overlaps an icon or content, the immediate backing surface can be identified so its separation ring matches."
      ],
      "avoidWhen": [
        "The status must stand alone without an owning label or control.",
        "The mark needs semantic positive, caution, negative, or neutral color; Badge intentionally uses one brand treatment.",
        "The compact content is metadata or taxonomy; use Tag or Chip according to whether it is removable.",
        "The badge itself would receive pointer or keyboard interaction; put interaction on the owning control."
      ],
      "alternatives": [
        {
          "when": "A compact static label communicates taxonomy or a semantic status.",
          "component": "component:ds-tag",
          "reason": "Tag owns static metadata labels and semantic intent coloring."
        },
        {
          "when": "A metadata value must be individually removable.",
          "component": "component:ds-chip",
          "reason": "Chip owns dismissible metadata values."
        },
        {
          "when": "The count is primary content rather than supplemental notification chrome.",
          "component": "component:ds-text",
          "reason": "Text preserves the quantity as normal readable content."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-button-unfilled",
        "component:ds-bar-nav",
        "component:ds-panel-nav",
        "component:ds-menu"
      ],
      "accessibility": [
        "The owning control or content provides the primary accessible name; Badge contributes only supplemental unread or notification meaning.",
        "When a dot is announced, provide contextual text that explains its meaning. Otherwise mark it aria-hidden from the owning composition.",
        "When a counter is announced, provide contextual text such as “5 unread notifications”; a bare number is insufficient.",
        "Avoid duplicating information already included in the owning control's accessible name.",
        "Badge never receives focus and never owns an interaction."
      ],
      "states": [
        "Counter represents an unread or notification count, while dot communicates presence without quantity.",
        "Non-positive counter values hide the badge.",
        "Counts above the configured compact limit display the limit with a plus suffix while accessible text retains meaningful context.",
        "Badge keeps one brand treatment; semantic severity belongs to another component or adjacent content.",
        "Use the ring only when Badge overlaps an icon or other content. Disable it in a reserved safe-area slot.",
        "When enabled, the ring matches the immediate backing surface for both variants. Use a direct ring override only for a component-local fill that no surface preset represents.",
        "Enabled shell gradient rings align automatically inside active ShellApp gradient chrome and may be explicitly enabled or disabled when composition requires it."
      ],
      "responsiveBehavior": [
        "The owning control or layout positions Badge and decides whether notification indicators remain visible, condense, or move at responsive breakpoints.",
        "Badge does not anchor itself or automatically switch between counter and dot variants."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Position Badge from the owning element's layout and synchronize its contextual accessibility text with the owner."
        ],
        "react": [
          "Keep count, visibility, and contextual accessibility text derived from the same unread-notification state as the owning control."
        ],
        "angular": [
          "Keep count, visibility, and contextual accessibility text derived from the same unread-notification state as the owning control."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/Badge/Badge.stories.ts"
        }
      ]
    },
    "api": {
      "props": {
        "background": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "background",
          "required": false,
          "mutable": false,
          "description": "Direct ring background override for component-local surfaces."
        },
        "count": {
          "type": "number",
          "resolvedType": "number",
          "attribute": "count",
          "default": "0",
          "required": false,
          "mutable": false,
          "description": "Count shown for counter badges. Count 0 hides the badge."
        },
        "gradientBackground": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "gradient-background",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Ring samples the shell gradient stack (base fill + wash) instead of a flat\n`box-shadow`. Auto-enabled under an ShellApp with an active gradient preset;\nset `gradient-background` to opt in/out explicitly.\n\nThe attribute must NOT start with `on` — Stencil's setAccessor routes any\nunknown `on*` member down the event-listener path during attribute\nreflection, calling addEventListener with a non-listener and throwing."
        },
        "hasRing": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "has-ring",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Show the separation ring when the badge overlaps an icon or other content."
        },
        "label": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "label",
          "required": false,
          "mutable": false,
          "description": "Contextual supplemental text. Omit when the owner hides the badge from assistive technology."
        },
        "max": {
          "type": "number",
          "resolvedType": "number",
          "attribute": "max",
          "default": "9",
          "required": false,
          "mutable": false,
          "description": "Highest count shown before compacting to \"{max}+\"."
        },
        "surface": {
          "type": "BadgeSurface",
          "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"navigation\" | \"primary\" | \"secondary\" | \"strong\" | \"translucent\"",
          "attribute": "surface",
          "default": "'primary'",
          "required": false,
          "mutable": false,
          "description": "Immediate backing surface matched by the ring around either variant."
        },
        "variant": {
          "type": "BadgeVariant",
          "resolvedType": "\"counter\" | \"dot\"",
          "attribute": "variant",
          "default": "'counter'",
          "required": false,
          "mutable": false,
          "description": "Render as a compact counter or notification dot."
        }
      },
      "events": [],
      "methods": [],
      "slots": []
    },
    "props": {
      "background": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "background",
        "required": false,
        "mutable": false,
        "description": "Direct ring background override for component-local surfaces."
      },
      "count": {
        "type": "number",
        "resolvedType": "number",
        "attribute": "count",
        "default": "0",
        "required": false,
        "mutable": false,
        "description": "Count shown for counter badges. Count 0 hides the badge."
      },
      "gradientBackground": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "gradient-background",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Ring samples the shell gradient stack (base fill + wash) instead of a flat\n`box-shadow`. Auto-enabled under an ShellApp with an active gradient preset;\nset `gradient-background` to opt in/out explicitly.\n\nThe attribute must NOT start with `on` — Stencil's setAccessor routes any\nunknown `on*` member down the event-listener path during attribute\nreflection, calling addEventListener with a non-listener and throwing."
      },
      "hasRing": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "has-ring",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Show the separation ring when the badge overlaps an icon or other content."
      },
      "label": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "label",
        "required": false,
        "mutable": false,
        "description": "Contextual supplemental text. Omit when the owner hides the badge from assistive technology."
      },
      "max": {
        "type": "number",
        "resolvedType": "number",
        "attribute": "max",
        "default": "9",
        "required": false,
        "mutable": false,
        "description": "Highest count shown before compacting to \"{max}+\"."
      },
      "surface": {
        "type": "BadgeSurface",
        "resolvedType": "\"always-dark\" | \"bold\" | \"faint\" | \"inverted\" | \"media\" | \"medium\" | \"navigation\" | \"primary\" | \"secondary\" | \"strong\" | \"translucent\"",
        "attribute": "surface",
        "default": "'primary'",
        "required": false,
        "mutable": false,
        "description": "Immediate backing surface matched by the ring around either variant."
      },
      "variant": {
        "type": "BadgeVariant",
        "resolvedType": "\"counter\" | \"dot\"",
        "attribute": "variant",
        "default": "'counter'",
        "required": false,
        "mutable": false,
        "description": "Render as a compact counter or notification dot."
      }
    },
    "events": [],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-badge",
      "react": "DsBadge",
      "vue": "DsBadge",
      "angular": "DsBadge"
    },
    "consumption": {
      "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
      "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
      "customElements": {
        "import": "import '@ds-mo/ui/dist/components/ds-badge.js';",
        "example": "<ds-badge></ds-badge>"
      },
      "react": {
        "import": "import { DsBadge } from '@ds-mo/ui/react';",
        "example": "<DsBadge />"
      },
      "vue": {
        "import": "import { DsBadge } from '@ds-mo/ui/vue';",
        "example": "<DsBadge />"
      },
      "angular": {
        "import": "import { DsBadge } from '@ds-mo/ui/angular/ds-badge';",
        "example": "<ds-badge></ds-badge>"
      },
      "peerDependencies": {
        "required": [
          "@ds-mo/tokens ^6.5.0",
          "@ds-mo/icons ^7.0.0"
        ],
        "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
      }
    }
  },
  "dependencies": [
    "@ds-mo/ui",
    "@ds-mo/tokens"
  ],
  "registryDependencies": [
    "text"
  ],
  "files": [
    {
      "path": "src/wc/components/Badge/Badge.css",
      "content": "@import '../../utils/visually-hidden.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  pointer-events: none;\n  vertical-align: middle;\n\n  --_badge-bg: var(--color-background-bold-brand);\n  --_badge-fg: var(--color-foreground-on-bold-background-primary);\n  --_badge-ring: var(--color-background-secondary);\n  --_badge-ring-width: var(--dimension-stroke-width-018);\n}\n\n.badge__mark {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n  box-sizing: border-box;\n  color: var(--_badge-fg);\n  background: var(--_badge-bg);\n  box-shadow: 0 0 0 var(--_badge-ring-width) var(--_badge-ring);\n  white-space: nowrap;\n}\n\n:host(.badge--no-ring) {\n  --_badge-ring-width: 0;\n}\n\n\n:host(.badge--dot) .badge__mark {\n  width: var(--dimension-size-075);\n  height: var(--dimension-size-075);\n  border-radius: var(--dimension-radius-half);\n}\n\n:host(.badge--counter) .badge__mark {\n  width: auto;\n  min-width: var(--dimension-size-150);\n  height: var(--dimension-size-150);\n  padding: 0 var(--dimension-space-025);\n  border-radius: var(--dimension-radius-half);\n}\n\n/* Shell gradient ring — base fill + wash donut (option 3). */\n:host(.badge--on-gradient-background) .badge__mark {\n  position: relative;\n  z-index: 0;\n  box-shadow: none;\n  isolation: isolate;\n}\n\n:host(.badge--on-gradient-background) .badge__mark::before,\n:host(.badge--on-gradient-background) .badge__mark::after {\n  content: '';\n  position: absolute;\n  inset: calc(-1 * var(--_badge-ring-width));\n  border-radius: inherit;\n  box-sizing: border-box;\n  padding: var(--_badge-ring-width);\n  pointer-events: none;\n  /* stylelint-disable color-no-hex -- mask luminance stops for ring cutout, not theme colors */\n  -webkit-mask:\n    linear-gradient(#fff 0 0) content-box,\n    linear-gradient(#fff 0 0);\n  -webkit-mask-composite: xor;\n  mask:\n    linear-gradient(#fff 0 0) content-box,\n    linear-gradient(#fff 0 0);\n  /* stylelint-enable color-no-hex */\n  mask-composite: exclude;\n}\n\n:host(.badge--on-gradient-background) .badge__mark::before {\n  z-index: -2;\n  background-color: var(--_badge-ring);\n}\n\n:host(.badge--on-gradient-background) .badge__mark::after {\n  z-index: -1;\n  background-image: var(--ds-shell-gradient-image, none);\n  background-size: var(--ds-shell-gradient-size, auto);\n  background-position: 0 0;\n  background-repeat: no-repeat;\n  background-attachment: fixed;\n  opacity: var(--ds-shell-gradient-opacity, 1);\n}\n\n@media (forced-colors: active) {\n  .badge__mark {\n    color: var(--ds-forced-color-on-selected);\n    background: var(--ds-forced-color-selected);\n    box-shadow: none;\n    outline: var(--_badge-ring-width) solid var(--ds-forced-color-surface);\n  }\n\n  :host(.badge--on-gradient-background) .badge__mark::before,\n  :host(.badge--on-gradient-background) .badge__mark::after {\n    display: none;\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Badge/Badge.tsx",
      "content": "import { Component, Prop, Element, State, Watch, h, Host } from '@stencil/core';\nimport {\n  BADGE_GRADIENT_POSITION_VAR,\n  isShellGradientActive,\n} from '../../shell/badge-gradient-ring';\n\nexport type BadgeVariant = 'counter' | 'dot';\nexport type BadgeSurface =\n  | 'primary'\n  | 'secondary'\n  | 'faint'\n  | 'medium'\n  | 'bold'\n  | 'strong'\n  | 'translucent'\n  | 'inverted'\n  | 'media'\n  | 'navigation'\n  | 'always-dark';\n\nconst SURFACE_RING: Record<BadgeSurface, string> = {\n  primary: 'var(--color-background-primary)',\n  secondary: 'var(--color-background-secondary)',\n  faint: 'var(--color-background-faint-neutral)',\n  medium: 'var(--color-background-medium-neutral)',\n  bold: 'var(--color-background-bold-neutral)',\n  strong: 'var(--color-background-strong-neutral)',\n  translucent: 'var(--color-translucent-translucent)',\n  inverted: 'var(--color-inverted-background)',\n  media: 'var(--color-media-background)',\n  navigation: 'var(--color-navigation-background)',\n  'always-dark': 'var(--color-always-dark-background)',\n};\n\n@Component({\n  tag: 'ds-badge',\n  styleUrl: 'Badge.css',\n  scoped: true,\n})\nexport class Badge {\n  @Element() el!: HTMLElement;\n\n  /** Render as a compact counter or notification dot. */\n  @Prop() variant: BadgeVariant = 'counter';\n\n  /** Count shown for counter badges. Count 0 hides the badge. */\n  @Prop() count: number = 0;\n\n  /** Highest count shown before compacting to \"{max}+\". */\n  @Prop() max: number = 9;\n\n  /** Immediate backing surface matched by the ring around either variant. */\n  @Prop() surface: BadgeSurface = 'primary';\n\n  /** Direct ring background override for component-local surfaces. */\n  @Prop() background: string | undefined;\n\n  /** Show the separation ring when the badge overlaps an icon or other content. */\n  @Prop() hasRing: boolean = true;\n\n  /**\n   * Ring samples the shell gradient stack (base fill + wash) instead of a flat\n   * `box-shadow`. Auto-enabled under an ShellApp with an active gradient preset;\n   * set `gradient-background` to opt in/out explicitly.\n   *\n   * The attribute must NOT start with `on` — Stencil's setAccessor routes any\n   * unknown `on*` member down the event-listener path during attribute\n   * reflection, calling addEventListener with a non-listener and throwing.\n   */\n  @Prop({ attribute: 'gradient-background', reflect: true }) gradientBackground: boolean = false;\n\n  /** Contextual supplemental text. Omit when the owner hides the badge from assistive technology. */\n  @Prop() label: string | undefined;\n\n  /** Bumps on resize/layout so render recomputes gradient ring position. */\n  @State() private gradientLayoutVersion = 0;\n\n  private gradientObserver: ResizeObserver | null = null;\n  private gradientWindowListener: (() => void) | null = null;\n\n  componentDidLoad() {\n    this.enableShellGradientRingIfNeeded();\n  }\n\n  disconnectedCallback() {\n    this.unbindGradientRingSync();\n  }\n\n  @Watch('gradientBackground')\n  @Watch('hasRing')\n  gradientBackgroundChanged() {\n    if (!this.hasRing) {\n      this.unbindGradientRingSync();\n      return;\n    }\n    if (!this.gradientBackground && isShellGradientActive(this.el)) {\n      this.gradientBackground = true;\n      return;\n    }\n    this.bindGradientRingSync();\n  }\n\n  /** Imperative opt-in avoids Stencil aborting parent render for nested badges. */\n  private enableShellGradientRingIfNeeded() {\n    if (!this.hasRing) return;\n\n    if (!this.gradientBackground && isShellGradientActive(this.el)) {\n      this.gradientBackground = true;\n    }\n\n    if (this.gradientBackground) {\n      this.bindGradientRingSync();\n    }\n  }\n\n  private bindGradientRingSync() {\n    this.unbindGradientRingSync();\n\n    if (!this.gradientBackground) return;\n\n    const update = () => {\n      this.gradientLayoutVersion += 1;\n    };\n    update();\n\n    this.gradientWindowListener = update;\n    window.addEventListener('resize', update);\n\n    if (typeof ResizeObserver !== 'undefined') {\n      this.gradientObserver = new ResizeObserver(update);\n      this.gradientObserver.observe(this.el);\n\n      const shell = this.el.closest('ds-shell-app');\n      if (shell) this.gradientObserver.observe(shell);\n\n      const bar = this.el.closest('ds-bar-nav');\n      if (bar) this.gradientObserver.observe(bar);\n\n      const panel = this.el.closest('ds-panel-nav');\n      if (panel) this.gradientObserver.observe(panel);\n    }\n  }\n\n  private unbindGradientRingSync() {\n    if (this.gradientWindowListener) {\n      window.removeEventListener('resize', this.gradientWindowListener);\n      this.gradientWindowListener = null;\n    }\n\n    this.gradientObserver?.disconnect();\n    this.gradientObserver = null;\n  }\n\n  private ringHostStyle(ring: string): Record<string, string> {\n    void this.gradientLayoutVersion;\n\n    const style: Record<string, string> = { '--_badge-ring': ring };\n    if (!this.gradientBackground || !isShellGradientActive(this.el)) return style;\n\n    /* Shell chrome wash uses background-attachment: fixed at viewport origin. */\n    style[BADGE_GRADIENT_POSITION_VAR] = '0 0';\n    return style;\n  }\n\n  render() {\n    const isDot = this.variant === 'dot';\n\n    if (!isDot && this.count <= 0) return <Host style={{ display: 'none' }} />;\n\n    const display = this.count > this.max ? `${this.max}+` : String(this.count);\n    const ring = this.background ?? SURFACE_RING[this.surface];\n\n    return (\n      <Host\n        class={{\n          badge: true,\n          'badge--counter': !isDot,\n          'badge--dot': isDot,\n          'badge--no-ring': !this.hasRing,\n          'badge--on-gradient-background': this.hasRing && this.gradientBackground,\n        }}\n        style={this.ringHostStyle(ring)}\n      >\n        <span class=\"badge__mark\" aria-hidden=\"true\">\n          {!isDot && (\n            <ds-text as=\"span\" variant=\"text-caption\" emphasis color=\"inherit\">\n              {display}\n            </ds-text>\n          )}\n        </span>\n        {this.label && <span class=\"badge__a11y ds-visually-hidden\">{this.label}</span>}\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
