{"name":"switch","type":"registry:component","title":"Switch","description":"A control that allows the user to toggle between checked and not checked. Built with Lit and styled using Tailwind CSS. Supports controlled and uncontrolled modes with form integration.","categories":["ui","form","switch","web-component"],"author":"Lloyd Richards <lloyd.d.richards@gmail.com>","dependencies":[],"files":[{"path":"registry/ui/switch/switch.ts","type":"registry:ui","content":"import { html, LitElement, nothing, type PropertyValues } from \"lit\";\nimport { customElement, property, state } from \"lit/decorators.js\";\nimport { TW } from \"@/registry/lib/tailwindMixin\";\n\nexport interface SwitchChangeEvent extends CustomEvent {\n  detail: { checked: boolean };\n}\n\nexport interface SwitchProperties {\n  disabled?: boolean;\n  required?: boolean;\n  name?: string;\n  value?: string;\n  checked?: boolean;\n  defaultChecked?: boolean;\n}\n\n@customElement(\"ui-switch\")\nexport class Switch extends TW(LitElement) implements SwitchProperties {\n  static formAssociated = true;\n  private internals: ElementInternals;\n\n  @property({ type: Boolean }) disabled = false;\n  @property({ type: Boolean }) required = false;\n  @property({ type: String }) name = \"\";\n  @property({ type: String }) value = \"on\";\n\n  @property({ type: Boolean }) checked: boolean | undefined = undefined;\n  @property({ type: Boolean, attribute: \"default-checked\" })\n  defaultChecked = false;\n\n  @property({ type: String, attribute: \"aria-label\" }) accessor ariaLabel:\n    | string\n    | null = null;\n  @property({ type: String, attribute: \"aria-labelledby\" })\n  accessor ariaLabelledby: string | null = null;\n  @property({ type: String, attribute: \"aria-describedby\" })\n  accessor ariaDescribedby: string | null = null;\n\n  @state() private _checked = false;\n\n  private _labelClickHandler = (e: Event) => {\n    const label = e.currentTarget as HTMLLabelElement;\n    if (label.htmlFor === this.id && !this.disabled) {\n      e.preventDefault();\n      this.handleClick();\n    }\n  };\n\n  constructor() {\n    super();\n    this.internals = this.attachInternals();\n  }\n\n  override connectedCallback() {\n    super.connectedCallback();\n    if (this.checked === undefined) {\n      this._checked = this.defaultChecked;\n    }\n    this._setupLabelDelegation();\n  }\n\n  override disconnectedCallback() {\n    super.disconnectedCallback();\n    this._cleanupLabelDelegation();\n  }\n\n  private _setupLabelDelegation() {\n    if (!this.id) return;\n    const root = this.getRootNode() as Document | ShadowRoot;\n    const labels = root.querySelectorAll(\n      `label[for=\"${this.id}\"]`,\n    ) as NodeListOf<HTMLLabelElement>;\n    labels.forEach((label) => {\n      label.addEventListener(\"click\", this._labelClickHandler);\n    });\n  }\n\n  private _cleanupLabelDelegation() {\n    if (!this.id) return;\n    const root = this.getRootNode() as Document | ShadowRoot;\n    const labels = root.querySelectorAll(\n      `label[for=\"${this.id}\"]`,\n    ) as NodeListOf<HTMLLabelElement>;\n    labels.forEach((label) => {\n      label.removeEventListener(\"click\", this._labelClickHandler);\n    });\n  }\n\n  private get isChecked(): boolean {\n    return this.checked !== undefined ? this.checked : this._checked;\n  }\n\n  override updated(changedProperties: PropertyValues) {\n    super.updated(changedProperties);\n\n    if (\n      changedProperties.has(\"checked\") ||\n      changedProperties.has(\"_checked\") ||\n      changedProperties.has(\"disabled\")\n    ) {\n      const state = this.isChecked ? \"checked\" : \"unchecked\";\n      this.setAttribute(\"data-state\", state);\n      this.setAttribute(\"aria-checked\", String(this.isChecked));\n\n      if (this.disabled) {\n        this.setAttribute(\"data-disabled\", \"\");\n      } else {\n        this.removeAttribute(\"data-disabled\");\n      }\n\n      this.internals.setFormValue(this.isChecked ? this.value : null);\n    }\n  }\n\n  override attributeChangedCallback(\n    name: string,\n    _old: string | null,\n    value: string | null,\n  ) {\n    super.attributeChangedCallback(name, _old, value);\n    if (name === \"id\" && _old !== value) {\n      this._cleanupLabelDelegation();\n      this._setupLabelDelegation();\n    }\n  }\n\n  private handleClick() {\n    if (this.disabled) return;\n\n    const newChecked =\n      this.checked !== undefined ? !this.checked : !this._checked;\n\n    if (this.checked === undefined) {\n      this._checked = newChecked;\n    }\n\n    this.dispatchEvent(\n      new CustomEvent(\"change\", {\n        detail: {\n          checked: newChecked,\n        },\n        bubbles: true,\n        composed: true,\n      }),\n    );\n  }\n\n  private handleKeyDown(e: KeyboardEvent) {\n    if (this.disabled) return;\n    if (e.key === \" \" || e.key === \"Enter\") {\n      e.preventDefault();\n      this.handleClick();\n    }\n  }\n\n  override render() {\n    return html`\n      <button\n        type=\"button\"\n        role=\"switch\"\n        class=\"peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50\"\n        ?disabled=${this.disabled}\n        ?required=${this.required}\n        aria-checked=${this.isChecked}\n        aria-label=${this.ariaLabel || nothing}\n        aria-labelledby=${this.ariaLabelledby || nothing}\n        aria-describedby=${this.ariaDescribedby || nothing}\n        data-state=${this.isChecked ? \"checked\" : \"unchecked\"}\n        @click=${this.handleClick}\n        @keydown=${this.handleKeyDown}\n      >\n        <span\n          class=\"bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0\"\n          data-state=${this.isChecked ? \"checked\" : \"unchecked\"}\n        ></span>\n      </button>\n    `;\n  }\n}\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    \"ui-switch\": Switch;\n  }\n\n  interface HTMLElementEventMap {\n    change: SwitchChangeEvent;\n  }\n}\n"},{"path":"registry/ui/switch/switch.stories.ts","type":"registry:ui","content":"import \"./switch\";\nimport type { Meta, StoryObj } from \"@storybook/web-components-vite\";\nimport { html } from \"lit\";\nimport type { SwitchProperties } from \"./switch\";\n\ntype SwitchArgs = SwitchProperties & {\n  label?: string;\n};\n\n/**\n * A control that allows the user to toggle between checked and not checked.\n */\nconst meta: Meta<SwitchArgs> = {\n  title: \"ui/Switch\",\n  component: \"ui-switch\",\n  tags: [\"autodocs\"],\n  parameters: {\n    layout: \"centered\",\n  },\n  argTypes: {\n    disabled: { control: \"boolean\" },\n    required: { control: \"boolean\" },\n    checked: { control: \"boolean\" },\n    defaultChecked: { control: \"boolean\" },\n    name: { control: \"text\" },\n    value: { control: \"text\" },\n    label: { control: \"text\" },\n  },\n  args: {\n    disabled: false,\n    required: false,\n    defaultChecked: false,\n    name: \"\",\n    value: \"on\",\n    label: \"Airplane Mode\",\n  },\n  render: (args) => html`\n    <div class=\"flex items-center gap-2\">\n      <ui-switch\n        id=\"switch\"\n        .disabled=${args.disabled || false}\n        .required=${args.required || false}\n        .checked=${args.checked}\n        .defaultChecked=${args.defaultChecked || false}\n        .name=${args.name || \"\"}\n        .value=${args.value || \"on\"}\n      ></ui-switch>\n      <label for=\"switch\" class=\"ui-label\">${args.label}</label>\n    </div>\n  `,\n};\n\nexport default meta;\n\ntype Story = StoryObj<SwitchArgs>;\n\n/**\n * The default form of the switch.\n */\nexport const Default: Story = {};\n\n/**\n * Use the `disabled` prop to disable the switch.\n */\nexport const Disabled: Story = {\n  args: {\n    disabled: true,\n  },\n};\n\n/**\n * Switch with default checked state.\n */\nexport const Checked: Story = {\n  args: {\n    defaultChecked: true,\n  },\n};\n"}]}