{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "switch",
  "title": "Switch",
  "type": "registry:ui",
  "description": "On/off toggle — visual analog of a hardware switch. Use for binary settings where the change takes effect immediately, not for form fields that submit later (use Checkbox there).",
  "categories": [
    "form"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/switch/switch.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { styleMap } from 'lit/directives/style-map.js'\nimport { LoaderCircle } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { icon } from '../../lib/icon'\n\ntype Size = 'sm' | 'default' | 'lg'\n\nconst colorMap: Record<string, string> = {\n  primary: 'var(--primary)',\n  secondary: 'var(--secondary)',\n  success: 'var(--success)',\n  warning: 'var(--warning)',\n  error: 'var(--destructive)',\n  info: 'var(--info)',\n}\n\nconst thumbSizes = {\n  sm: 'size-3',\n  default: 'size-4',\n  lg: 'size-5',\n}\n\nconst thumbTranslate = {\n  sm: 'data-[state=checked]:translate-x-[calc(100%-2px)]',\n  default: 'data-[state=checked]:translate-x-[calc(100%-2px)]',\n  lg: 'data-[state=checked]:translate-x-[calc(100%-5px)]',\n}\n\nconst textSizes = {\n  sm: 'text-[0.5rem]',\n  default: 'text-xs',\n  lg: 'text-xs',\n}\n\nconst thumbIconSizes = {\n  sm: 'size-2',\n  default: 'size-3',\n  lg: 'size-3',\n}\n\n/**\n * <uip-switch> — the registry Switch as a web component.\n *\n * Class strings are React's `Switch` verbatim. The inner <button role=switch>\n * carries `data-state` / `aria-checked` like Radix's Switch.Root.\n *\n * Content (React's node props become named slots; string props stay attributes):\n *  - `checked-children` / `un-checked-children` attributes, or\n *    `slot=\"checked-children\"` / `slot=\"un-checked-children\"` for icons;\n *  - `slot=\"thumb\"` for custom thumb content (React's `thumb` render prop —\n *    read the host's `checked` / `data-state` to vary it).\n *\n * Form-associated: submits `name=value` (value defaults to \"on\", like Radix)\n * when checked, supports `required`, resets to the initial `checked`\n * attribute. A light-DOM `<label for>` pointing at the host toggles it and\n * names the inner switch.\n *\n * Events: `input`, `change` (read `.checked`), and `checked-change`\n * (React's `onCheckedChange`, `detail` = the new boolean).\n */\nexport class UipSwitch extends LitElement {\n  static formAssociated = true\n  static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: inline-flex; }`]\n\n  static properties = {\n    // `checked` attribute = initial/default state (like <input checked>);\n    // the `checked` property is the live state.\n    checked: { type: Boolean, attribute: false },\n    defaultChecked: { type: Boolean, attribute: 'checked' },\n    size: { reflect: true },\n    checkedChildren: { attribute: 'checked-children' },\n    unCheckedChildren: { attribute: 'un-checked-children' },\n    loading: { type: Boolean, reflect: true },\n    color: {},\n    disabled: { type: Boolean, reflect: true },\n    required: { type: Boolean, reflect: true },\n    name: { reflect: true },\n    value: {},\n    accessibleLabel: { attribute: 'aria-label' },\n    hasCheckedSlot: { state: true },\n    hasUncheckedSlot: { state: true },\n    labelText: { state: true },\n  }\n\n  /** Current state. The `checked` attribute is the initial (default) state. */\n  checked = false\n  defaultChecked = false\n  size: Size = 'default'\n  checkedChildren?: string\n  unCheckedChildren?: string\n  loading = false\n  color?: string\n  disabled = false\n  required = false\n  name?: string\n  value = 'on'\n  accessibleLabel?: string\n  private hasCheckedSlot = false\n  private hasUncheckedSlot = false\n  private labelText?: string\n\n  private internals = this.attachInternals()\n\n  constructor() {\n    super()\n    new ThemeController(this)\n    // One listener for both sources: a click on the inner button bubbles to\n    // the host, and a `<label for>` click is dispatched on the host itself.\n    this.addEventListener('click', () => this.toggle())\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'switch')\n  }\n\n  protected willUpdate(changed: Map<string, unknown>) {\n    // Attribute changes drive the state (so attribute-only frameworks work);\n    // on first render only a present attribute applies, so a `.checked = true`\n    // set before connect isn't overwritten by the absent attribute.\n    if (changed.has('defaultChecked') && (this.hasUpdated || this.defaultChecked)) this.checked = this.defaultChecked\n    this.setAttribute('data-state', this.checked ? 'checked' : 'unchecked')\n    if (changed.has('checked') || changed.has('required') || changed.has('value')) {\n      this.internals.setFormValue(this.checked ? this.value : null)\n      if (this.required && !this.checked) {\n        this.internals.setValidity({ valueMissing: true }, 'Please turn this on.', this.button ?? undefined)\n      } else {\n        this.internals.setValidity({})\n      }\n    }\n    // Labels associated with the host name the inner switch.\n    const labels = [...(this.internals.labels ?? [])] as HTMLElement[]\n    this.labelText = labels.map((l) => l.textContent?.trim()).filter(Boolean).join(' ') || undefined\n  }\n\n  formResetCallback() {\n    this.checked = this.defaultChecked\n  }\n  formDisabledCallback(disabled: boolean) {\n    this.disabled = disabled\n  }\n\n  private get button() {\n    return this.renderRoot?.querySelector<HTMLButtonElement>('[role=switch]')\n  }\n\n  private toggle() {\n    if (this.disabled || this.loading) return\n    this.checked = !this.checked\n    this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('checked-change', { detail: this.checked, bubbles: true, composed: true }))\n  }\n\n  private onSlotChange(e: Event) {\n    const slot = e.target as HTMLSlotElement\n    const has = slot.assignedNodes().length > 0\n    if (slot.name === 'checked-children') this.hasCheckedSlot = has\n    else this.hasUncheckedSlot = has\n  }\n\n  render() {\n    const size = this.size in thumbSizes ? this.size : 'default'\n    const hasChildren = Boolean(\n      this.checkedChildren || this.unCheckedChildren || this.hasCheckedSlot || this.hasUncheckedSlot,\n    )\n    const height = { sm: 'h-4', default: 'h-5', lg: 'h-6' }[size]\n    const width = hasChildren\n      ? { sm: 'min-w-8 w-fit', default: 'min-w-10 w-fit', lg: 'min-w-13 w-fit' }[size]\n      : { sm: 'w-6', default: 'w-8', lg: 'w-11' }[size]\n    const state = this.checked ? 'checked' : 'unchecked'\n    const isDisabled = this.disabled || this.loading\n    // Runtime value (any CSS colour) — the one dynamic style React sets too.\n    const trackStyle = {\n      '--switch-checked-bg': this.color ? colorMap[this.color] || this.color : 'var(--primary)',\n    }\n\n    return html`<button\n      part=\"base\"\n      type=\"button\"\n      role=\"switch\"\n      data-slot=\"switch\"\n      aria-checked=${this.checked ? 'true' : 'false'}\n      aria-required=${this.required ? 'true' : nothing}\n      aria-label=${this.accessibleLabel ?? this.labelText ?? nothing}\n      data-state=${state}\n      ?data-disabled=${isDisabled}\n      value=${this.value}\n      ?disabled=${isDisabled}\n      style=${styleMap(trackStyle)}\n      class=${cn(\n        'peer focus-visible:ring-ring/50 focus-visible:border-ring data-[state=unchecked]:bg-input dark:data-[state=unchecked]:bg-input/80 relative inline-flex shrink-0 items-center overflow-hidden rounded-full border border-transparent shadow-xs outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[var(--switch-checked-bg)]',\n        'touch-manipulation enabled:active:scale-[0.97] enabled:active:duration-100 motion-safe:transition-[background-color,border-color,box-shadow,transform,scale] motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',\n        `${height} ${width}`,\n      )}\n    >\n      <div\n        ?hidden=${!hasChildren}\n        class=${cn(\n          'pointer-events-none absolute inset-y-0 left-0 flex items-center pl-1 motion-safe:transition-[opacity,transform] motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',\n          this.checked ? 'translate-x-0 opacity-100' : '-translate-x-0.5 opacity-0',\n          textSizes[size],\n        )}\n      >\n        <span class=\"text-primary-foreground truncate font-medium\"\n          ><slot name=\"checked-children\" @slotchange=${this.onSlotChange}>${this.checkedChildren ?? nothing}</slot></span\n        >\n      </div>\n      <div\n        ?hidden=${!hasChildren}\n        class=${cn(\n          'pointer-events-none absolute inset-y-0 right-0 flex items-center pr-1 motion-safe:transition-[opacity,transform] motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none',\n          !this.checked ? 'translate-x-0 opacity-100' : 'translate-x-0.5 opacity-0',\n          textSizes[size],\n        )}\n      >\n        <span class=\"text-muted-foreground truncate font-medium\"\n          ><slot name=\"un-checked-children\" @slotchange=${this.onSlotChange}>${this.unCheckedChildren ?? nothing}</slot></span\n        >\n      </div>\n      <span\n        part=\"thumb\"\n        data-uipkge=\"\"\n        data-slot=\"switch-thumb\"\n        data-state=${state}\n        ?data-disabled=${isDisabled}\n        class=${cn(\n          'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none z-10 flex items-center justify-center rounded-full shadow-sm ring-0 data-[state=unchecked]:translate-x-0 motion-safe:transition-transform motion-safe:duration-200 motion-safe:ease-[cubic-bezier(0.22,1.15,0.36,1)] motion-reduce:transition-none',\n          thumbSizes[size],\n          thumbTranslate[size],\n        )}\n      >\n        ${this.loading\n          ? // lucide-react's Loader2 alias emits both lucide-loader-2 and lucide-loader-circle.\n            icon(LoaderCircle, 'loader-2', cn('lucide-loader-circle', thumbIconSizes[size], 'text-muted-foreground motion-safe:animate-spin'))\n          : html`<slot name=\"thumb\"></slot>`}\n      </span>\n    </button>`\n  }\n}\n\ncustomElements.get('uip-switch') || customElements.define('uip-switch', UipSwitch)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-switch': UipSwitch\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/switch/switch.ts"
    }
  ],
  "dependencies": [
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
