{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "label",
  "title": "Label",
  "type": "registry:ui",
  "description": "Accessible label primitive — wraps text and binds to its child input via `htmlFor`. Disabled-state styling and proper screen-reader behavior.",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/label/label.ts",
      "content": "import { LitElement, css, html } from 'lit'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\n/** React's Label class string, shared with elements that render their own label (textarea). */\nexport const labelClasses =\n  'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50'\n\n/**\n * <uip-label> — the registry Label (Radix Label) as a web component.\n *\n * Renders a native <label> with React's class string. React's `htmlFor` is the\n * `for` attribute / `htmlFor` property. A <label for> inside a shadow root\n * can't reach an id in the page, so the element does what the browser would:\n *  - clicking it focuses and clicks the control with that id (same root);\n *  - it names that control: `aria-label` is set on the target from the\n *    label's text, unless the target already has its own aria-label /\n *    aria-labelledby. Every uipkge control forwards its host `aria-label` to\n *    its inner focusable element, so this names uip-input / uip-textarea too.\n * Like Radix, a double-click doesn't select the label's text.\n *\n * `peer-disabled:` / `group-data-[disabled=true]:` stay in the class string\n * but can't match from inside the shadow root. Class overrides (React's\n * `className`) target the label through `::part(base)`; colour and font\n * utilities on the host are inherited.\n */\nexport class UipLabel extends LitElement {\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    htmlFor: { attribute: 'for', reflect: true },\n  }\n\n  htmlFor?: string\n  private named?: { el: Element; text: string }\n\n  constructor() {\n    super()\n    new ThemeController(this)\n    this.addEventListener('click', this.onClick)\n    this.addEventListener('mousedown', this.onMouseDown)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'label')\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.unname()\n  }\n\n  /** The labelled control, looked up by id in the label's own root. */\n  get control(): HTMLElement | null {\n    if (!this.htmlFor) return null\n    const root = this.getRootNode() as Document | ShadowRoot\n    return root.getElementById?.(this.htmlFor) ?? null\n  }\n\n  protected updated() {\n    this.nameControl()\n  }\n\n  // The label's text, minus aria-hidden decoration (e.g. a required \"*\").\n  private text() {\n    const walk = (n: Node): string =>\n      n.nodeType === Node.TEXT_NODE\n        ? (n.textContent ?? '')\n        : n instanceof Element && n.getAttribute('aria-hidden') === 'true'\n          ? ''\n          : [...n.childNodes].map(walk).join('')\n    return walk(this).replace(/\\s+/g, ' ').trim()\n  }\n\n  private nameControl() {\n    const el = this.control\n    if (this.named && this.named.el !== el) this.unname()\n    if (!el) return\n    const text = this.text()\n    const current = el.getAttribute('aria-label')\n    const ours = this.named?.el === el && current === this.named.text\n    if ((current && !ours) || el.hasAttribute('aria-labelledby')) return\n    if (text) {\n      el.setAttribute('aria-label', text)\n      this.named = { el, text }\n    }\n  }\n\n  private unname() {\n    if (!this.named) return\n    const { el, text } = this.named\n    if (el.getAttribute('aria-label') === text) el.removeAttribute('aria-label')\n    this.named = undefined\n  }\n\n  private onClick = (e: MouseEvent) => {\n    const el = this.control\n    if (!el || (el as HTMLInputElement).disabled) return\n    // Clicks on interactive content inside the label (or on the control\n    // itself, when the label wraps it) keep their own behaviour.\n    const path = e.composedPath()\n    if (path.includes(el)) return\n    const interactive = path.find(\n      (n) => n instanceof HTMLElement && n !== this && n.matches('a[href], button, input, select, textarea, [tabindex]'),\n    )\n    if (interactive && this.contains(interactive as Node)) return\n    el.focus()\n    el.click()\n  }\n\n  // Radix Label: don't select the text on double-click.\n  private onMouseDown = (e: MouseEvent) => {\n    if (e.detail > 1) e.preventDefault()\n  }\n\n  render() {\n    return html`<label part=\"base\" data-slot=\"label\" class=${labelClasses}\n      ><slot @slotchange=${() => this.nameControl()}></slot\n    ></label>`\n  }\n}\n\ncustomElements.get('uip-label') || customElements.define('uip-label', UipLabel)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-label': UipLabel\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/label/label.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
