{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "field",
  "title": "Field",
  "description": "Accessible form-field composition that gives one Input or Select a persistent label, optional guidance, shared error presentation, stable IDs, and observable interaction state.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/field",
    "source": "src/wc/components/Field/Field.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Accessible form-field composition that gives one Input or Select a persistent label, optional guidance, shared error presentation, stable IDs, and observable interaction state.",
      "useWhen": [
        "An Input or Select needs a persistent visible label in a form.",
        "Help text or an error message must be programmatically associated with one control.",
        "A form layout needs one consistent label-control-message rhythm."
      ],
      "avoidWhen": [
        "The component already owns its visible label and complete selection semantics, such as Checkbox, Radio, or Switch.",
        "Several controls belong to one question; use a semantic fieldset and legend instead.",
        "Text describes a region or group rather than one form control."
      ],
      "alternatives": [
        {
          "when": "Several related controls answer one question.",
          "component": "component:ds-text",
          "reason": "Use native fieldset and legend semantics, then compose Text only for supporting content."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-input",
        "component:ds-select",
        "component:ds-text"
      ],
      "accessibility": [
        "Slot exactly one form control. Field assigns its stable control ID and connects the visible label automatically.",
        "The visible description or error ID is merged with any authored aria-describedby references; a rendered error replaces the description.",
        "The error prop forwards aria-invalid and supported design-system control error styling; put visible error text on Field, not again on the child control.",
        "Clicking the label focuses the slotted design-system control without opening a Select popup.",
        "Do not use placeholder text as the only label. Keep label text concise and put format or consequence guidance in description."
      ],
      "states": [
        "Label, control, description, and error content follow the shared 4px vertical field flow without changing the slotted control's width or semantics.",
        "Field exposes data-focused, data-filled, data-dirty, data-touched, data-invalid, data-disabled, and data-required hooks while the slotted control remains the source of value and interaction behavior.",
        "A rendered error replaces the description; preserve essential guidance in the error message so users retain what they need to correct the field.",
        "Field owns validation presentation but not validation timing; the form decides when to set error and errorMessage.",
        "The label uses primary body-small emphasis, description uses secondary body-small text, and error uses negative body-small text."
      ],
      "responsiveBehavior": [
        "Field fills the width supplied by its parent and makes the slotted control fill that field width.",
        "The form layout owns column count, wrapping, and responsive placement; Field keeps the vertical label-control-message composition intact."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Set fieldId only when another element must reference a predictable control ID; otherwise use the generated association.",
          "Assign child component values and option arrays through their normal JavaScript properties."
        ],
        "react": [
          "Compose one DsInput or DsSelect child and control validation timing in the form owner."
        ],
        "angular": [
          "Compose the generated Field adapter around the control adapter and its matching Angular value accessor."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/Field/Field.stories.ts"
        },
        {
          "label": "Cross-browser form and accessibility coverage",
          "path": "tests/e2e/forms.spec.ts"
        }
      ]
    },
    "api": {
      "props": {
        "description": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "description",
          "required": false,
          "mutable": false,
          "description": "Optional guidance associated with the slotted control while no visible error is shown."
        },
        "error": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "error",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Invalid visual and accessible state forwarded to supported ds controls."
        },
        "errorMessage": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "error-message",
          "required": false,
          "mutable": false,
          "description": "Visible error associated with the slotted control while error is true."
        },
        "fieldId": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "field-id",
          "required": false,
          "mutable": false,
          "description": "Explicit ID for the slotted control; generated when omitted."
        },
        "label": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "label",
          "required": true,
          "mutable": false,
          "description": "Persistent visible label for the single slotted control."
        }
      },
      "events": [],
      "methods": [],
      "slots": []
    },
    "props": {
      "description": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "description",
        "required": false,
        "mutable": false,
        "description": "Optional guidance associated with the slotted control while no visible error is shown."
      },
      "error": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "error",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Invalid visual and accessible state forwarded to supported ds controls."
      },
      "errorMessage": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "error-message",
        "required": false,
        "mutable": false,
        "description": "Visible error associated with the slotted control while error is true."
      },
      "fieldId": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "field-id",
        "required": false,
        "mutable": false,
        "description": "Explicit ID for the slotted control; generated when omitted."
      },
      "label": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "label",
        "required": true,
        "mutable": false,
        "description": "Persistent visible label for the single slotted control."
      }
    },
    "events": [],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-field",
      "react": "DsField",
      "vue": "DsField",
      "angular": "DsField"
    },
    "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-field.js';",
        "example": "<ds-field></ds-field>"
      },
      "react": {
        "import": "import { DsField } from '@ds-mo/ui/react';",
        "example": "<DsField />"
      },
      "vue": {
        "import": "import { DsField } from '@ds-mo/ui/vue';",
        "example": "<DsField />"
      },
      "angular": {
        "import": "import { DsField } from '@ds-mo/ui/angular/ds-field';",
        "example": "<ds-field></ds-field>"
      },
      "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/Field/Field.css",
      "content": "@import '../../utils/field-stack.css';\n\n:host {\n  display: block;\n}\n\n.field__control {\n  display: flex;\n  min-width: 0;\n}\n\n.field__control > * {\n  width: 100%;\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Field/Field.tsx",
      "content": "import { Component, Element, Prop, State, Watch, h, Host } from '@stencil/core';\n\ninterface FieldControl extends HTMLElement {\n  value?: string | string[];\n  inputId?: string;\n  error?: boolean;\n  disabled?: boolean;\n  isInactive?: boolean;\n  required?: boolean;\n  setFocus?: () => Promise<void> | void;\n}\n\ninterface AuthoredAria {\n  labelledby: string[];\n  describedby: string[];\n}\n\nlet fieldCounter = 0;\n\nconst tokens = (value: string | null | undefined): string[] =>\n  value?.split(/\\s+/).filter(Boolean) ?? [];\n\nconst uniqueTokens = (...groups: string[][]): string[] => [...new Set(groups.flat())];\n\n@Component({\n  tag: 'ds-field',\n  styleUrl: 'Field.css',\n  scoped: true,\n})\nexport class Field {\n  @Element() el!: HTMLElement;\n\n  /** Persistent visible label for the single slotted control. */\n  @Prop() label!: string;\n  /** Explicit ID for the slotted control; generated when omitted. */\n  @Prop() fieldId: string | undefined;\n  /** Optional guidance associated with the slotted control while no visible error is shown. */\n  @Prop() description: string | undefined;\n  /** Invalid visual and accessible state forwarded to supported ds controls. */\n  @Prop() error: boolean = false;\n  /** Visible error associated with the slotted control while error is true. */\n  @Prop() errorMessage: string | undefined;\n\n  @State() private focused = false;\n  @State() private filled = false;\n  @State() private dirty = false;\n  @State() private touched = false;\n  @State() private controlDisabled = false;\n  @State() private controlRequired = false;\n\n  private readonly generatedId = `ds-field-${++fieldCounter}`;\n  private controlContainer?: HTMLDivElement;\n  private control?: FieldControl;\n  private initialValue = '';\n  private authoredAria = new WeakMap<FieldControl, AuthoredAria>();\n\n  componentDidLoad() {\n    this.syncControl();\n    requestAnimationFrame(() => this.syncControl());\n  }\n\n  componentDidRender() {\n    this.syncControl();\n  }\n\n  @Watch('fieldId')\n  @Watch('description')\n  @Watch('error')\n  @Watch('errorMessage')\n  onFieldContractChange() {\n    this.syncControl();\n  }\n\n  private get controlId(): string {\n    return this.fieldId || this.generatedId;\n  }\n\n  private get labelId(): string {\n    return `${this.controlId}-label`;\n  }\n\n  private get descriptionId(): string {\n    return `${this.controlId}-description`;\n  }\n\n  private get errorId(): string {\n    return `${this.controlId}-error`;\n  }\n\n  private get renderedDescription(): boolean {\n    return Boolean(this.description) && !this.renderedError;\n  }\n\n  private get renderedError(): boolean {\n    return this.error && Boolean(this.errorMessage);\n  }\n\n  private readValue(control: FieldControl): string {\n    const value = control.value;\n    return Array.isArray(value) ? value.join('\\u001f') : String(value ?? '');\n  }\n\n  private updateControlAttribute(control: FieldControl, name: string, value: string | undefined) {\n    if (value === undefined) {\n      if (control.hasAttribute(name)) control.removeAttribute(name);\n      return;\n    }\n    if (control.getAttribute(name) !== value) control.setAttribute(name, value);\n  }\n\n  private findControl(): FieldControl | undefined {\n    return this.controlContainer?.querySelector<FieldControl>(':scope > :not(slot)') ?? undefined;\n  }\n\n  private syncControl = () => {\n    const control = this.findControl();\n    if (!control) return;\n\n    if (control !== this.control) {\n      this.control = control;\n      this.initialValue = this.readValue(control);\n      this.authoredAria.set(control, {\n        labelledby: tokens(control.getAttribute('aria-labelledby')),\n        describedby: tokens(control.getAttribute('aria-describedby')),\n      });\n    }\n\n    const isDsControl = control.tagName.startsWith('DS-');\n    if (isDsControl && 'inputId' in control) control.inputId = this.controlId;\n    else this.updateControlAttribute(control, 'id', this.controlId);\n\n    const authored = this.authoredAria.get(control) ?? { labelledby: [], describedby: [] };\n    const labelledby = uniqueTokens(authored.labelledby, [this.labelId]).join(' ');\n    const describedby = uniqueTokens(\n      authored.describedby,\n      this.renderedDescription ? [this.descriptionId] : [],\n      this.renderedError ? [this.errorId] : [],\n    ).join(' ');\n\n    this.updateControlAttribute(control, 'aria-labelledby', labelledby);\n    this.updateControlAttribute(control, 'aria-describedby', describedby || undefined);\n    this.updateControlAttribute(control, 'aria-invalid', this.error ? 'true' : undefined);\n\n    if (isDsControl && 'error' in control && control.error !== this.error) control.error = this.error;\n\n    const value = this.readValue(control);\n    this.filled = value.length > 0;\n    this.dirty = value !== this.initialValue;\n    this.controlDisabled = Boolean(control.disabled || control.isInactive || control.hasAttribute('disabled'));\n    this.controlRequired = Boolean(control.required || control.hasAttribute('required'));\n  };\n\n  private handleLabelClick = (event: MouseEvent) => {\n    if (!this.control?.tagName.startsWith('DS-')) return;\n    event.preventDefault();\n    this.control.setFocus?.();\n  };\n\n  private handleFocusIn = () => {\n    this.focused = true;\n  };\n\n  private handleFocusOut = (event: FocusEvent) => {\n    if (event.relatedTarget instanceof Node && this.el.contains(event.relatedTarget)) return;\n    this.focused = false;\n    this.touched = true;\n  };\n\n  private handleValueChange = () => {\n    this.syncControl();\n  };\n\n  render() {\n    return (\n      <Host\n        data-focused={this.focused ? '' : undefined}\n        data-filled={this.filled ? '' : undefined}\n        data-dirty={this.dirty ? '' : undefined}\n        data-touched={this.touched ? '' : undefined}\n        data-invalid={this.error ? '' : undefined}\n        data-disabled={this.controlDisabled ? '' : undefined}\n        data-required={this.controlRequired ? '' : undefined}\n        onFocusin={this.handleFocusIn}\n        onFocusout={this.handleFocusOut}\n        onInput={this.handleValueChange}\n        onChange={this.handleValueChange}\n        onDsChange={this.handleValueChange}\n      >\n        <div class=\"field ds-field-stack\">\n          <ds-text\n            class=\"field__label\"\n            as=\"label\"\n            variant=\"text-body-small\"\n            color=\"primary\"\n            emphasis\n            for={this.controlId}\n            textId={this.labelId}\n            onClick={this.handleLabelClick}\n          >\n            {this.label}\n          </ds-text>\n          <div class=\"field__control\" ref={el => (this.controlContainer = el)}>\n            <slot onSlotchange={this.syncControl} />\n          </div>\n          {this.renderedDescription && (\n            <ds-text\n              class=\"field__description\"\n              as=\"div\"\n              variant=\"text-body-small\"\n              color=\"secondary\"\n              textId={this.descriptionId}\n            >\n              {this.description}\n            </ds-text>\n          )}\n          {this.renderedError && (\n            <ds-text\n              class=\"field__error\"\n              as=\"div\"\n              variant=\"text-body-small\"\n              color=\"negative\"\n              textId={this.errorId}\n              role=\"alert\"\n            >\n              {this.errorMessage}\n            </ds-text>\n          )}\n        </div>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
