{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chart-legend",
  "title": "ChartLegend",
  "description": "External chart legend with swatches, labels, optional values and percentages, and explicit deep links.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/chart-legend",
    "source": "src/wc/components/ChartLegend/ChartLegend.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "specialized",
      "status": "stable",
      "summary": "External chart legend with swatches, labels, optional values and percentages, and explicit deep links.",
      "useWhen": [
        "A chart needs the standard swatch, label, and optional numeric-detail legend.",
        "Polar slices or Cartesian series need a continuously visible key beside or below the visualization.",
        "A category has an explicit deep-link destination and should render as an accessible link."
      ],
      "avoidWhen": [
        "The design calls for chips, bare swatches, external stat callouts, or another custom legend treatment.",
        "Rows need to toggle visibility, filter data, or represent selection.",
        "The content is a command menu or navigation list unrelated to chart data."
      ],
      "alternatives": [
        {
          "when": "The product needs a nonstandard legend treatment.",
          "component": "component:ds-text",
          "reason": "Compose product-owned semantic markup from the same chart data rather than forcing it into this row recipe."
        },
        {
          "when": "A chart and static series key belong in the canonical dashboard card.",
          "component": "component:ds-card-chart",
          "reason": "The chart variant places the external legend and disables decorative local hover highlighting."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-chart",
        "component:ds-card-chart"
      ],
      "accessibility": [
        "Rows remain non-interactive unless the item supplies an actual link destination.",
        "Optional highlight changes opacity only and never moves focus or announces selection.",
        "Keep full labels available when visible text truncates, and retain stable tabular numeric columns.",
        "Do not rely on swatch color alone; every entry requires a meaningful visible label."
      ],
      "states": [
        "Local pointer or focus interaction and external chart highlight remain separate; both may dim peers, but only a real link row receives navigation affordances.",
        "Disable local hover highlighting for static keys such as a line-chart legend; externally controlled activeLabel highlighting and genuine link affordances remain available.",
        "Hover synchronization is temporary emphasis, never filtering, toggling, or selected state.",
        "Percentage values use one consistent precision across the complete legend.",
        "The list owns pointer leave so moving across row gaps does not clear and reapply highlight."
      ],
      "responsiveBehavior": [
        "Vertical legends let the label yield width and truncate before right-aligned value and percentage columns.",
        "Horizontal legends wrap complete entries and suit compact series keys below a chart.",
        "The parent constrains width and decides when a custom condensed treatment is necessary."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Assign items as a JavaScript property and set href only for genuine navigation."
        ],
        "react": [
          "Intercept the item-click event only when application routing must replace native link navigation."
        ],
        "angular": [
          "Intercept the item-click event only when application routing must replace native link navigation."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/ChartLegend/ChartLegend.stories.ts"
        },
        {
          "label": "Rendered interaction tests",
          "path": "tests/e2e/chart-legend.spec.ts"
        }
      ]
    },
    "api": {
      "props": {
        "activeLabel": {
          "type": "string | null",
          "resolvedType": "null | string",
          "attribute": "active-label",
          "default": "null",
          "required": false,
          "mutable": false,
          "description": "Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\nchart's `dsSliceHover` event to keep chart and legend hover in sync. Only dims the other\nrows' opacity (like a chart dimming its other slices) — it never shows the hover-fill,\nsince that's reserved for a real pointer/keyboard interaction on this row."
        },
        "direction": {
          "type": "ChartLegendDirection",
          "resolvedType": "\"horizontal\" | \"vertical\"",
          "attribute": "direction",
          "default": "'vertical'",
          "required": false,
          "mutable": false
        },
        "highlightOnHover": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "highlight-on-hover",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Let local pointer/focus movement highlight one item and emit `dsItemHover`."
        },
        "items": {
          "type": "ChartLegendItem[]",
          "resolvedType": "ChartLegendItem[]",
          "default": "[]",
          "required": false,
          "mutable": false,
          "description": "Legend entries. Set as a JS property (not an HTML attribute)."
        },
        "locale": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "locale",
          "required": false,
          "mutable": false
        },
        "percentageDecimals": {
          "type": "ChartLegendPercentageDecimals",
          "resolvedType": "1 | 2",
          "attribute": "percentage-decimals",
          "default": "1",
          "required": false,
          "mutable": false,
          "description": "Fixed number of decimal places shown for percentages."
        },
        "showPercentage": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "show-percentage",
          "default": "true",
          "required": false,
          "mutable": false,
          "description": "Show each item's share of the total (of items with a `value`) alongside its count."
        }
      },
      "events": [
        {
          "name": "dsItemClick",
          "detail": "{ item: ChartLegendItem; originalEvent: MouseEvent }",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Fires when a deep-linkable row (`item.href` set) is activated."
        },
        {
          "name": "dsItemHover",
          "detail": "ChartLegendItem | null",
          "bubbles": true,
          "cancelable": true,
          "composed": true,
          "description": "Fires on row hover/focus with the item, or `null` on leave/blur."
        }
      ],
      "methods": [],
      "slots": []
    },
    "props": {
      "activeLabel": {
        "type": "string | null",
        "resolvedType": "null | string",
        "attribute": "active-label",
        "default": "null",
        "required": false,
        "mutable": false,
        "description": "Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\nchart's `dsSliceHover` event to keep chart and legend hover in sync. Only dims the other\nrows' opacity (like a chart dimming its other slices) — it never shows the hover-fill,\nsince that's reserved for a real pointer/keyboard interaction on this row."
      },
      "direction": {
        "type": "ChartLegendDirection",
        "resolvedType": "\"horizontal\" | \"vertical\"",
        "attribute": "direction",
        "default": "'vertical'",
        "required": false,
        "mutable": false
      },
      "highlightOnHover": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "highlight-on-hover",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Let local pointer/focus movement highlight one item and emit `dsItemHover`."
      },
      "items": {
        "type": "ChartLegendItem[]",
        "resolvedType": "ChartLegendItem[]",
        "default": "[]",
        "required": false,
        "mutable": false,
        "description": "Legend entries. Set as a JS property (not an HTML attribute)."
      },
      "locale": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "locale",
        "required": false,
        "mutable": false
      },
      "percentageDecimals": {
        "type": "ChartLegendPercentageDecimals",
        "resolvedType": "1 | 2",
        "attribute": "percentage-decimals",
        "default": "1",
        "required": false,
        "mutable": false,
        "description": "Fixed number of decimal places shown for percentages."
      },
      "showPercentage": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "show-percentage",
        "default": "true",
        "required": false,
        "mutable": false,
        "description": "Show each item's share of the total (of items with a `value`) alongside its count."
      }
    },
    "events": [
      {
        "name": "dsItemClick",
        "detail": "{ item: ChartLegendItem; originalEvent: MouseEvent }",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Fires when a deep-linkable row (`item.href` set) is activated."
      },
      {
        "name": "dsItemHover",
        "detail": "ChartLegendItem | null",
        "bubbles": true,
        "cancelable": true,
        "composed": true,
        "description": "Fires on row hover/focus with the item, or `null` on leave/blur."
      }
    ],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-chart-legend",
      "react": "DsChartLegend",
      "vue": "DsChartLegend",
      "angular": "DsChartLegend"
    },
    "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-chart-legend.js';",
        "example": "<ds-chart-legend></ds-chart-legend>"
      },
      "react": {
        "import": "import { DsChartLegend } from '@ds-mo/ui/react';",
        "example": "<DsChartLegend />"
      },
      "vue": {
        "import": "import { DsChartLegend } from '@ds-mo/ui/vue';",
        "example": "<DsChartLegend />"
      },
      "angular": {
        "import": "import { DsChartLegend } from '@ds-mo/ui/angular/ds-chart-legend';",
        "example": "<ds-chart-legend></ds-chart-legend>"
      },
      "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
      "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/ChartLegend/ChartLegend.css",
      "content": "@import '../../utils/control-density.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n  display: block;\n  font-family: var(--typography-font-family-ui);\n  user-select: none;\n}\n\n.chart-legend__list {\n  margin: 0;\n  padding: 0;\n  list-style: none;\n}\n\n/* Non-visual wrapper — the real box is .chart-legend__item so optional highlight/link\n   affordances cover the whole row while children align to the shared grid columns. */\n.chart-legend__list-item {\n  display: contents;\n}\n\n:host(.chart-legend--vertical) .chart-legend__list {\n  display: grid;\n  grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600);\n  column-gap: var(--dimension-space-075);\n  row-gap: var(--dimension-space-050);\n}\n\n:host(.chart-legend--vertical.chart-legend--show-percentage) .chart-legend__list {\n  grid-template-columns: auto minmax(0, 1fr) var(--dimension-size-600) var(--dimension-size-800);\n}\n\n:host(.chart-legend--vertical) .chart-legend__item {\n  grid-column: 1 / -1;\n  display: grid;\n  grid-template-columns: subgrid;\n}\n\n:host(.chart-legend--horizontal) .chart-legend__list {\n  display: flex;\n  flex-wrap: wrap;\n  gap: var(--dimension-space-200);\n}\n\n:host(.chart-legend--horizontal) .chart-legend__item {\n  display: flex;\n  gap: var(--dimension-space-100);\n}\n\n/*\n * Density: md control recipe (height / pad / radius / label inset).\n * Hover/press: `.ds-interaction-fill` (token overlay on ::after).\n */\n.chart-legend__item {\n  box-sizing: border-box;\n  height: var(--ds-control-height, var(--dimension-size-400));\n  align-items: center;\n  padding: 0 var(--ds-control-padding-inline, var(--dimension-space-075));\n  border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n  text-decoration: none;\n  color: inherit;\n  transition: opacity var(--effect-motion-short-2);\n}\n\n.chart-legend__item--dimmed {\n  opacity: var(--effect-opacity-medium);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  .chart-legend__item {\n    transition: none;\n  }\n}\n\n.chart-legend__item--interactive {\n  cursor: pointer;\n}\n\n/* Raise row content above interaction fills (positive z-index on util layers). */\n.chart-legend__item > * {\n  position: relative;\n  z-index: 2;\n}\n\n.chart-legend__swatch-box {\n  width: var(--dimension-size-250);\n  height: var(--dimension-size-250);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n\n.chart-legend__swatch {\n  width: var(--dimension-space-150);\n  height: var(--dimension-space-150);\n  border-radius: var(--dimension-radius-025);\n  flex-shrink: 0;\n}\n\n.chart-legend__label {\n  min-width: 0;\n}\n\n.chart-legend__percentage {\n  width: 100%;\n  min-width: 0;\n  text-align: right;\n}\n\n.chart-legend__value {\n  width: 100%;\n  text-align: right;\n  min-width: 0;\n}\n\n@media (forced-colors: active) {\n  .chart-legend__swatch {\n    forced-color-adjust: none;\n    outline: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/ChartLegend/ChartLegend.tsx",
      "content": "import { Component, Prop, State, Event, EventEmitter, h, Host, Watch } from '@stencil/core';\nimport { categoryColor } from '../../utils/chart-colors';\nimport { formatCompactNumber, formatPercentage, resolveSafeUrl } from '../../utils';\nimport type { ChartLegendItem } from '../../utils/chart-types';\n\nexport type ChartLegendDirection = 'vertical' | 'horizontal';\nexport type ChartLegendPercentageDecimals = 1 | 2;\n\n/**\n * Base legend for `ds-chart-*` components. Webapp's Overview widgets each style\n * legends differently (list w/ values, compact chips, bare swatches, external\n * stat callouts) — this covers the common \"swatch + label + optional value\" case.\n * Consumers who need a different treatment skip this component entirely and\n * render their own markup against the same `ChartLegendItem[]` data.\n */\n@Component({\n  tag: 'ds-chart-legend',\n  styleUrl: 'ChartLegend.css',\n  scoped: true,\n})\nexport class ChartLegend {\n  @Prop() locale: string | undefined;\n  /** Legend entries. Set as a JS property (not an HTML attribute). */\n  @Prop() items: ChartLegendItem[] = [];\n  @Prop() direction: ChartLegendDirection = 'vertical';\n  /** Show each item's share of the total (of items with a `value`) alongside its count. */\n  @Prop() showPercentage: boolean = true;\n  /** Fixed number of decimal places shown for percentages. */\n  @Prop() percentageDecimals: ChartLegendPercentageDecimals = 1;\n  /** Let local pointer/focus movement highlight one item and emit `dsItemHover`. */\n  @Prop() highlightOnHover: boolean = true;\n  /**\n   * Externally controlled highlight, matched by `label` — e.g. drive this from a sibling\n   * chart's `dsSliceHover` event to keep chart and legend hover in sync. Only dims the other\n   * rows' opacity (like a chart dimming its other slices) — it never shows the hover-fill,\n   * since that's reserved for a real pointer/keyboard interaction on this row.\n   */\n  @Prop() activeLabel: string | null = null;\n\n  /** Own pointer/focus hover, kept separate from `activeLabel` so the hover-fill (a \"you can\n   * click here\" affordance) never shows just because an external row was synced in. */\n  @State() private hoveredLabel: string | null = null;\n\n  /** Fires on row hover/focus with the item, or `null` on leave/blur. */\n  @Event() dsItemHover!: EventEmitter<ChartLegendItem | null>;\n  /** Fires when a deep-linkable row (`item.href` set) is activated. */\n  @Event() dsItemClick!: EventEmitter<{ item: ChartLegendItem; originalEvent: MouseEvent }>;\n\n  @Watch('highlightOnHover')\n  handleHighlightOnHoverChange(enabled: boolean) {\n    if (!enabled) this.hoveredLabel = null;\n  }\n\n  private handleHover(item: ChartLegendItem | null) {\n    this.hoveredLabel = item?.label ?? null;\n    this.dsItemHover.emit(item);\n  }\n\n  private handleClick = (item: ChartLegendItem, originalEvent: MouseEvent) => {\n    this.dsItemClick.emit({ item, originalEvent });\n  };\n\n  render() {\n    const total = this.items.reduce((sum, item) => sum + (item.value ?? 0), 0);\n    // Highlight/dim from either this component's own hover or an externally-synced label\n    // (e.g. an application-owned chart focus state).\n    const highlightLabel = this.activeLabel ?? (this.highlightOnHover ? this.hoveredLabel : null);\n\n    return (\n      <Host\n        class={{\n          'chart-legend': true,\n          [`chart-legend--${this.direction}`]: true,\n          'chart-legend--show-percentage': this.showPercentage,\n        }}\n      >\n        {/* mouseleave lives on the list, not each row — the row-gap between items is still\n            inside the list's box, so crossing it while moving between rows never fires this.\n            Only actually leaving the whole list does, and that clears with no delay. */}\n        <ul\n          class=\"chart-legend__list\"\n          onMouseLeave={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n        >\n          {this.items.map((item, i) => {\n            const isDimmed = highlightLabel != null && item.label !== highlightLabel;\n            const href = resolveSafeUrl(item.href);\n            const RowTag = href ? 'a' : 'div';\n            const percentage = item.value != null\n              ? formatPercentage(total ? item.value / total : 0, this.percentageDecimals, this.locale)\n              : '';\n\n            return (\n              <li class=\"chart-legend__list-item\" key={item.label}>\n                <RowTag\n                  class={{\n                    'chart-legend__item': true,\n                    'chart-legend__item--interactive': !!href,\n                    'chart-legend__item--dimmed': isDimmed,\n                    'ds-control--md': true,\n                    'ds-interaction-fill': this.highlightOnHover || !!href,\n                    'ds-focus-ring-inset': !!href,\n                  }}\n                  href={href}\n                  onClick={href ? (e: MouseEvent) => this.handleClick(item, e) : undefined}\n                  onMouseEnter={\n                    this.highlightOnHover ? () => this.handleHover(item) : undefined\n                  }\n                  onFocus={this.highlightOnHover ? () => this.handleHover(item) : undefined}\n                  onBlur={this.highlightOnHover ? () => this.handleHover(null) : undefined}\n                >\n                  <span class=\"chart-legend__swatch-box\">\n                    <span class=\"chart-legend__swatch\" style={{ backgroundColor: item.color ?? categoryColor(i) }} />\n                  </span>\n                  <ds-text\n                    class=\"chart-legend__label\"\n                    as=\"span\"\n                    variant=\"text-body-medium\"\n                    color=\"secondary\"\n                    lineTruncation={1}\n                    title={item.label}\n                  >\n                    {item.label}\n                  </ds-text>\n                  {item.value != null && (\n                    <ds-text\n                      class=\"chart-legend__value\"\n                      as=\"span\"\n                      variant=\"text-body-medium\"\n                      color=\"primary\"\n                      lineTruncation={1}\n                      align=\"right\"\n                      fontFeature=\"tabular-nums\"\n                      title={String(item.value)}\n                    >\n                      {formatCompactNumber(item.value, this.locale)}\n                    </ds-text>\n                  )}\n                  {item.value != null && this.showPercentage && (\n                    <ds-text\n                      class=\"chart-legend__percentage\"\n                      as=\"span\"\n                      variant=\"text-body-medium\"\n                      color=\"primary\"\n                      lineTruncation={1}\n                      align=\"right\"\n                      fontFeature=\"tabular-nums\"\n                      title={percentage}\n                    >\n                      {percentage}\n                    </ds-text>\n                  )}\n                </RowTag>\n              </li>\n            );\n          })}\n        </ul>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
