{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "switch",
  "title": "Switch",
  "description": "Compact on/off setting control for changes that take effect immediately, with four sizes and form-associated behavior.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/switch",
    "source": "src/wc/components/Switch/Switch.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "stable",
      "summary": "Compact on/off setting control for changes that take effect immediately, with four sizes and form-associated behavior.",
      "useWhen": [
        "A binary setting takes effect immediately when the user turns it on or off.",
        "A settings, form, or menu row needs a compact on/off control.",
        "The checked value needs native form submission, required validation, reset, or external form association."
      ],
      "avoidWhen": [
        "The user is providing consent, acknowledging a statement, or selecting multiple independent choices.",
        "The user must choose one option from a mutually exclusive set.",
        "The interaction triggers a one-time action instead of maintaining an on/off state."
      ],
      "alternatives": [
        {
          "when": "The interaction represents consent, acknowledgment, or multi-select choices.",
          "component": "component:ds-checkbox",
          "reason": "Checkbox semantics communicate selection or agreement rather than an immediately applied setting."
        },
        {
          "when": "The user must choose one option from a mutually exclusive group.",
          "component": "component:ds-radio",
          "reason": "Radio communicates one-of-many selection."
        },
        {
          "when": "The control triggers a one-time command or submission.",
          "component": "component:ds-button-filled",
          "reason": "Button semantics communicate an action rather than persistent state."
        }
      ],
      "commonlyComposedWith": [
        "component:ds-field",
        "component:ds-menu",
        "component:ds-text"
      ],
      "accessibility": [
        "Every standalone switch requires an accessible name from a native label, aria-label, or aria-labelledby.",
        "Checked state is exposed through switch semantics and thumb position, so state does not rely on color alone.",
        "Read-only switches remain focusable and submitted but cannot change; inactive or disabled switches leave the tab order and are omitted from form submission.",
        "Required and invalid state are exposed explicitly on the switch role; focused, filled, dirty, touched, valid, and invalid data hooks support form diagnostics without imposing visual validation timing.",
        "Use presentation mode only when a composite owner such as a menuitemcheckbox provides the interaction and accessible state."
      ],
      "states": [
        "Activation updates checked immediately and emits dsChange with the new boolean value.",
        "The initial checked value is the form-reset baseline; unchecked switches submit nothing unless uncheckedValue is provided.",
        "Sizes are lg 40×24 with a 16px thumb and 4px body inset, md 32×20 with a 12px thumb and 4px inset, sm 24×16 with a 10px thumb and 3px inset, and xs 20×12 with an 8px thumb and 2px inset.",
        "Unchecked uses a transparent track with a density-specific tertiary-foreground inset stroke and a solid tertiary-foreground thumb without a border. Checked uses a brand track and primary-background thumb with no strokes.",
        "Presentation mode removes switch semantics, focus, interaction, and form submission while retaining the checked visual."
      ],
      "responsiveBehavior": [
        "Choose size from the density of the containing row; do not enlarge the switch to the full control height.",
        "Keep the switch from shrinking when adjacent labels wrap.",
        "Thumb movement is left-to-right; the design system does not author RTL-specific component behavior."
      ],
      "frameworkCaveats": {
        "customElements": [
          "Set checked as a property for application state updates and listen for dsChange.",
          "Use the form attribute to associate a switch rendered outside its owning form."
        ],
        "react": [
          "Use DsSwitch and handle onDsChange; checked is mutable and updates immediately when the component is activated."
        ],
        "vue": [
          "Use DsSwitch with v-model for checked, and listen for dsChange."
        ],
        "angular": [
          "Use DsSwitch with checked binding and dsChange, or the generated boolean value accessor in Angular forms."
        ]
      },
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/Switch/Switch.stories.ts"
        },
        {
          "label": "Cross-browser form and accessibility coverage",
          "path": "tests/e2e/forms.spec.ts"
        }
      ]
    },
    "api": {
      "props": {
        "checked": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "checked",
          "default": "false",
          "required": false,
          "mutable": true,
          "description": "Current on/off state. The initial value is restored when the owning form resets."
        },
        "disabled": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "disabled",
          "default": "false",
          "required": false,
          "mutable": false
        },
        "form": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "form",
          "required": false,
          "mutable": false,
          "description": "Associates the switch with a form by id when it is rendered outside that form."
        },
        "isInactive": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-inactive",
          "default": "false",
          "required": false,
          "mutable": false
        },
        "name": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "name",
          "required": false,
          "mutable": false
        },
        "presentation": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "presentation",
          "default": "false",
          "required": false,
          "mutable": false,
          "description": "Removes standalone control semantics so a composite owner such as a\n`menuitemcheckbox` can use the switch as an aria-hidden visual indicator."
        },
        "readOnly": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "read-only",
          "default": "false",
          "required": false,
          "mutable": false
        },
        "required": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "required",
          "default": "false",
          "required": false,
          "mutable": false
        },
        "requiredMessage": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "required-message",
          "default": "'This field is required.'",
          "required": false,
          "mutable": false
        },
        "size": {
          "type": "SwitchSize",
          "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
          "attribute": "size",
          "default": "'md'",
          "required": false,
          "mutable": false,
          "description": "Compact track size for placement inside controls, menus, and form rows."
        },
        "uncheckedValue": {
          "type": "string | undefined",
          "resolvedType": "string | undefined",
          "attribute": "unchecked-value",
          "required": false,
          "mutable": false,
          "description": "Optional value submitted when unchecked; omitted by default like a native checkbox."
        },
        "value": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "value",
          "default": "'on'",
          "required": false,
          "mutable": false
        }
      },
      "events": [
        {
          "name": "dsChange",
          "detail": "boolean",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        }
      ],
      "methods": [],
      "slots": []
    },
    "props": {
      "checked": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "checked",
        "default": "false",
        "required": false,
        "mutable": true,
        "description": "Current on/off state. The initial value is restored when the owning form resets."
      },
      "disabled": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "disabled",
        "default": "false",
        "required": false,
        "mutable": false
      },
      "form": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "form",
        "required": false,
        "mutable": false,
        "description": "Associates the switch with a form by id when it is rendered outside that form."
      },
      "isInactive": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-inactive",
        "default": "false",
        "required": false,
        "mutable": false
      },
      "name": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "name",
        "required": false,
        "mutable": false
      },
      "presentation": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "presentation",
        "default": "false",
        "required": false,
        "mutable": false,
        "description": "Removes standalone control semantics so a composite owner such as a\n`menuitemcheckbox` can use the switch as an aria-hidden visual indicator."
      },
      "readOnly": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "read-only",
        "default": "false",
        "required": false,
        "mutable": false
      },
      "required": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "required",
        "default": "false",
        "required": false,
        "mutable": false
      },
      "requiredMessage": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "required-message",
        "default": "'This field is required.'",
        "required": false,
        "mutable": false
      },
      "size": {
        "type": "SwitchSize",
        "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
        "attribute": "size",
        "default": "'md'",
        "required": false,
        "mutable": false,
        "description": "Compact track size for placement inside controls, menus, and form rows."
      },
      "uncheckedValue": {
        "type": "string | undefined",
        "resolvedType": "string | undefined",
        "attribute": "unchecked-value",
        "required": false,
        "mutable": false,
        "description": "Optional value submitted when unchecked; omitted by default like a native checkbox."
      },
      "value": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "value",
        "default": "'on'",
        "required": false,
        "mutable": false
      }
    },
    "events": [
      {
        "name": "dsChange",
        "detail": "boolean",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      }
    ],
    "methods": [],
    "slots": [],
    "exports": {
      "customElement": "ds-switch",
      "react": "DsSwitch",
      "vue": "DsSwitch",
      "angular": "DsSwitch"
    },
    "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-switch.js';",
        "example": "<ds-switch></ds-switch>"
      },
      "react": {
        "import": "import { DsSwitch } from '@ds-mo/ui/react';",
        "example": "<DsSwitch />"
      },
      "vue": {
        "import": "import { DsSwitch } from '@ds-mo/ui/vue';",
        "example": "<DsSwitch />"
      },
      "angular": {
        "import": "import { DsSwitch } from '@ds-mo/ui/angular/ds-switch';",
        "example": "<ds-switch></ds-switch>"
      },
      "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": [],
  "files": [
    {
      "path": "src/wc/components/Switch/Switch.css",
      "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n  --ds-switch-width: var(--dimension-size-400);\n  --ds-switch-height: var(--dimension-size-250);\n  --ds-switch-thumb-size: var(--dimension-size-150);\n  --ds-switch-gap: var(--dimension-space-050);\n  --ds-switch-stroke: var(--dimension-stroke-width-012);\n  --ds-switch-inset: var(--ds-switch-gap);\n  --ds-switch-thumb-hover: var(--color-interaction-hover);\n  --ds-switch-thumb-pressed: var(--color-interaction-pressed);\n  --ds-switch-travel: calc(\n    var(--ds-switch-width) - var(--ds-switch-thumb-size) - var(--ds-switch-inset) -\n      var(--ds-switch-inset)\n  );\n  position: relative;\n  display: inline-flex;\n  width: var(--ds-switch-width);\n  height: var(--ds-switch-height);\n  padding: 0;\n  border: none;\n  border-radius: var(--dimension-radius-half);\n  box-sizing: border-box;\n  flex-shrink: 0;\n  overflow: visible;\n  cursor: default;\n  user-select: none;\n  outline: none;\n  background-color: transparent;\n  box-shadow: inset 0 0 0 var(--ds-switch-stroke) var(--color-border-secondary);\n  transition:\n    background-color var(--effect-motion-short-3),\n    box-shadow var(--effect-motion-short-3);\n}\n\n:host(.switch--lg) {\n  --ds-switch-width: var(--dimension-size-500);\n  --ds-switch-height: var(--dimension-size-300);\n  --ds-switch-thumb-size: var(--dimension-iconography-sm);\n  --ds-switch-gap: var(--dimension-space-050);\n}\n\n:host(.switch--sm) {\n  --ds-switch-width: var(--dimension-size-300);\n  --ds-switch-height: var(--dimension-size-200);\n  --ds-switch-thumb-size: calc(var(--dimension-size-base) * 5 / 4);\n  --ds-switch-gap: calc(var(--dimension-space-025) + var(--dimension-space-012));\n}\n\n:host(.switch--xs) {\n  --ds-switch-width: var(--dimension-size-250);\n  --ds-switch-height: var(--dimension-size-150);\n  --ds-switch-thumb-size: var(--dimension-size-100);\n  --ds-switch-gap: var(--dimension-space-025);\n}\n\n:host(.checked) {\n  background-color: var(--color-background-bold-brand);\n  box-shadow: none;\n}\n\n:host(.switch--readonly),\n:host(.switch--presentation) {\n  cursor: default;\n}\n\n:host(.switch--presentation) {\n  pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.thumb {\n  position: absolute;\n  top: 50%;\n  left: var(--ds-switch-inset);\n  width: var(--ds-switch-thumb-size);\n  height: var(--ds-switch-thumb-size);\n  border-radius: var(--dimension-radius-half);\n  background-color: var(--color-foreground-tertiary);\n  box-shadow: none;\n  transform: translateY(-50%);\n  transition:\n    background-color var(--effect-motion-short-3),\n    transform var(--effect-motion-short-3);\n  pointer-events: none;\n}\n\n.thumb::after {\n  content: '';\n  position: absolute;\n  inset: 0;\n  border-radius: inherit;\n  background: transparent;\n  pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n  :host(:hover:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n    .thumb::after {\n    background: var(--ds-switch-thumb-hover);\n  }\n}\n\n:host(:active:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n  .thumb::after {\n  background: var(--ds-switch-thumb-pressed);\n}\n\n:host(.checked) .thumb {\n  background-color: var(--color-background-primary);\n  box-shadow: none;\n  transform: translate3d(var(--ds-switch-travel), -50%, 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n  :host {\n    transition: none;\n  }\n\n  .thumb {\n    transition: none;\n  }\n}\n\n@media (forced-colors: active) {\n  :host {\n    border: var(--ds-switch-stroke) solid var(--ds-forced-color-content);\n    box-shadow: none;\n  }\n\n  :host(.checked) {\n    background-color: var(--ds-forced-color-selected);\n    border-color: var(--ds-forced-color-selected);\n  }\n\n  .thumb {\n    background-color: var(--ds-forced-color-content);\n  }\n\n  :host(.checked) .thumb {\n    background-color: var(--ds-forced-color-on-selected);\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/Switch/Switch.tsx",
      "content": "import { AttachInternals, Component, Element, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setFormControlValue, setRequiredValidity } from '../../utils';\n\nexport type SwitchSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n  tag: 'ds-switch',\n  styleUrl: 'Switch.css',\n  shadow: true,\n  formAssociated: true,\n})\nexport class Switch {\n  @Element() el!: HTMLElement;\n  @AttachInternals() internals!: ElementInternals;\n\n  /** Current on/off state. The initial value is restored when the owning form resets. */\n  @Prop({ mutable: true }) checked: boolean = false;\n\n  @Prop({ reflect: true }) name: string | undefined;\n  @Prop() value: string = 'on';\n\n  /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n  @Prop() uncheckedValue: string | undefined;\n\n  /** Associates the switch with a form by id when it is rendered outside that form. */\n  @Prop({ reflect: true }) form: string | undefined;\n\n  @Prop({ reflect: true }) disabled: boolean = false;\n  @Prop({ reflect: true }) readOnly: boolean = false;\n  @Prop({ reflect: true }) required: boolean = false;\n  @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n  @Prop() isInactive: boolean = false;\n\n  /** Compact track size for placement inside controls, menus, and form rows. */\n  @Prop() size: SwitchSize = 'md';\n\n  /**\n   * Removes standalone control semantics so a composite owner such as a\n   * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n   */\n  @Prop() presentation: boolean = false;\n\n  @Event() dsChange!: EventEmitter<boolean>;\n\n  private initialChecked = false;\n  @State() private formDisabled = false;\n  @State() private focused = false;\n  @State() private touched = false;\n\n  componentWillLoad() {\n    this.initialChecked = this.checked;\n    this.syncFormValue();\n  }\n\n  componentDidLoad() {\n    this.syncNativeLabels();\n  }\n\n  @Watch('checked')\n  @Watch('value')\n  @Watch('uncheckedValue')\n  @Watch('disabled')\n  @Watch('isInactive')\n  @Watch('presentation')\n  @Watch('required')\n  syncFormValue() {\n    const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n    const submissionValue = this.checked ? this.value : this.uncheckedValue ?? null;\n    const state = this.checked ? 'checked' : 'unchecked';\n    setFormControlValue(this.internals, submissionValue, { inactive, state });\n\n    const missing = this.required && !inactive && !this.checked;\n    setRequiredValidity(this.internals, missing, this.requiredMessage);\n  }\n\n  formDisabledCallback(disabled: boolean) {\n    this.formDisabled = disabled;\n    this.syncFormValue();\n  }\n\n  formResetCallback() {\n    this.checked = this.initialChecked;\n    this.focused = false;\n    this.touched = false;\n  }\n\n  formStateRestoreCallback(state: string | File | FormData | null) {\n    this.checked = state === 'checked';\n  }\n\n  private syncNativeLabels() {\n    if (this.presentation) return;\n    if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n    const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n      if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n      return label.id;\n    });\n    if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n  }\n\n  private handleClick = () => {\n    if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n    this.checked = !this.checked;\n    this.dsChange.emit(this.checked);\n  };\n\n  private handleKeyDown = (event: KeyboardEvent) => {\n    if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n    if (event.key === ' ' || event.key === 'Enter') {\n      event.preventDefault();\n      this.handleClick();\n    }\n  };\n\n  private handleFocus = () => {\n    if (!this.presentation) this.focused = true;\n  };\n\n  private handleBlur = () => {\n    if (this.presentation) return;\n    this.focused = false;\n    this.touched = true;\n  };\n\n  render() {\n    const inactive = this.isInactive || this.disabled || this.formDisabled;\n    const invalid = this.required && !inactive && !this.checked;\n    const dirty = this.checked !== this.initialChecked;\n    // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n    return (\n      <Host\n        role={this.presentation ? undefined : 'switch'}\n        aria-checked={this.presentation ? undefined : String(this.checked)}\n        aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n        aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n        aria-required={!this.presentation && this.required ? 'true' : undefined}\n        aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n        aria-hidden={this.presentation ? 'true' : undefined}\n        data-checked={this.checked ? '' : undefined}\n        data-unchecked={!this.checked ? '' : undefined}\n        data-disabled={inactive ? '' : undefined}\n        data-readonly={this.readOnly ? '' : undefined}\n        data-required={this.required ? '' : undefined}\n        data-focused={!this.presentation && this.focused ? '' : undefined}\n        data-filled={!this.presentation && this.checked ? '' : undefined}\n        data-dirty={!this.presentation && dirty ? '' : undefined}\n        data-touched={!this.presentation && this.touched ? '' : undefined}\n        data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n        data-invalid={!this.presentation && invalid ? '' : undefined}\n        tabIndex={this.presentation || inactive ? -1 : 0}\n        class={{\n          switch: true,\n          checked: this.checked,\n          'switch--lg': this.size === 'lg',\n          'switch--md': this.size === 'md',\n          'switch--sm': this.size === 'sm',\n          'switch--xs': this.size === 'xs',\n          'switch--readonly': this.readOnly,\n          'switch--presentation': this.presentation,\n          'ds-focus-ring': true,\n          'ds-control-inactive': inactive,\n        }}\n        onClick={this.handleClick}\n        onKeyDown={this.handleKeyDown}\n        onFocus={this.handleFocus}\n        onBlur={this.handleBlur}\n      >\n        <span class=\"thumb\" />\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
