{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "switch",
  "title": "Switch",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/switch/switch.component.ts",
      "content": "import {\n  ChangeDetectorRef,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  Output,\n  TemplateRef,\n  booleanAttribute,\n  forwardRef,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { NG_VALUE_ACCESSOR, type ControlValueAccessor } from '@angular/forms'\nimport { cn } from '@/lib/utils'\nimport { UiRenderTemplateDirective } from '@/ui/popper/popper'\n\nexport type SwitchSize = 'sm' | 'default' | 'lg'\nexport type SwitchColor = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | (string & {})\n/** Text, or an <ng-template> (icons), shown inside the track. */\nexport type SwitchChildren = string | TemplateRef<unknown> | null | undefined\n\nconst COLOR_MAP: 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 THUMB_SIZES: Record<SwitchSize, string> = {\n  sm: 'size-3',\n  default: 'size-4',\n  lg: 'size-5',\n}\n\nconst THUMB_TRANSLATE: Record<SwitchSize, string> = {\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 TEXT_SIZES: Record<SwitchSize, string> = {\n  sm: 'text-[0.5rem]',\n  default: 'text-xs',\n  lg: 'text-xs',\n}\n\nconst THUMB_ICON_SIZES: Record<SwitchSize, string> = {\n  sm: 'size-2',\n  default: 'size-3',\n  lg: 'size-3',\n}\n\nconst HEIGHTS: Record<SwitchSize, string> = { sm: 'h-4', default: 'h-5', lg: 'h-6' }\nconst WIDTHS: Record<SwitchSize, string> = { sm: 'w-6', default: 'w-8', lg: 'w-11' }\nconst WIDTHS_WITH_CHILDREN: Record<SwitchSize, string> = {\n  sm: 'min-w-8 w-fit',\n  default: 'min-w-10 w-fit',\n  lg: 'min-w-13 w-fit',\n}\n\n/**\n * Angular port of the React Switch (Radix Switch). Put it on a native button for exact\n * React DOM (`<button ui-switch>` -- labelable, native :disabled), or use `<ui-switch>`,\n * which becomes a focusable role=\"switch\" element with Enter / Space activation.\n *\n * Model is React's `checked` / `defaultChecked` / `checkedChange` (`[(checked)]`), plus\n * NG_VALUE_ACCESSOR for formControl / ngModel. `checkedChildren` / `unCheckedChildren`\n * take text or an <ng-template>; `thumb` takes an <ng-template> rendered inside the thumb\n * (read the state through `#sw=\"uiSwitch\"` -> `sw.checked`).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-switch, [ui-switch]',\n  standalone: true,\n  exportAs: 'uiSwitch',\n  imports: [UiRenderTemplateDirective],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiSwitchComponent), multi: true }],\n  host: {\n    '[attr.type]': 'isNativeButton ? \"button\" : null',\n    '[attr.role]': '\"switch\"',\n    '[attr.aria-checked]': 'checked',\n    '[attr.aria-required]': 'required || null',\n    '[attr.aria-disabled]': '!isNativeButton && isDisabled ? \"true\" : null',\n    '[attr.data-state]': 'dataState',\n    '[attr.data-disabled]': 'isDisabled ? \"\" : null',\n    '[attr.disabled]': 'isNativeButton && isDisabled ? \"\" : null',\n    '[attr.tabindex]': '!isNativeButton ? (isDisabled ? null : 0) : null',\n    '[attr.value]': 'value',\n    '[attr.data-slot]': '\"switch\"',\n    '[class]': 'hostClass',\n    '[style.--switch-checked-bg]': 'checkedBg',\n    '(click)': 'toggle()',\n    '(keydown)': 'onKeydown($event)',\n    '(blur)': 'onTouched()',\n  },\n  template: `\n    @if (hasChildren) {\n      <div [class]=\"checkedChildrenClass\">\n        <span class=\"text-primary-foreground truncate font-medium\">\n          @if (isTemplate(checkedChildren)) {\n            <ng-container [uiRenderTemplate]=\"checkedChildren\" />\n          } @else {\n            {{ checkedChildren }}\n          }\n        </span>\n      </div>\n      <div [class]=\"unCheckedChildrenClass\">\n        <span class=\"text-muted-foreground truncate font-medium\">\n          @if (isTemplate(unCheckedChildren)) {\n            <ng-container [uiRenderTemplate]=\"unCheckedChildren\" />\n          } @else {\n            {{ unCheckedChildren }}\n          }\n        </span>\n      </div>\n    }\n    <span\n      data-uipkge=\"\"\n      data-slot=\"switch-thumb\"\n      [attr.data-state]=\"dataState\"\n      [attr.data-disabled]=\"isDisabled ? '' : null\"\n      [class]=\"thumbClass\"\n    >\n      @if (loading) {\n        <svg\n          xmlns=\"http://www.w3.org/2000/svg\"\n          width=\"24\"\n          height=\"24\"\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke=\"currentColor\"\n          stroke-width=\"2\"\n          stroke-linecap=\"round\"\n          stroke-linejoin=\"round\"\n          [attr.class]=\"loaderClass\"\n          aria-hidden=\"true\"\n        >\n          <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n        </svg>\n      } @else if (thumb) {\n        <ng-container [uiRenderTemplate]=\"thumb\" />\n      }\n    </span>\n  `,\n})\nexport class UiSwitchComponent implements ControlValueAccessor {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\n  readonly isNativeButton = inject(ElementRef).nativeElement.tagName === 'BUTTON'\n\n  private readonly _checkedProp = signal<boolean | undefined>(undefined)\n  private readonly _internal = signal<boolean | null>(null)\n\n  /** Controlled checked state (React `checked`). Reading it returns the resolved state. */\n  @Input({ transform: (v: unknown) => (v === undefined || v === null ? undefined : booleanAttribute(v)) })\n  set checked(v: boolean | undefined) {\n    this._checkedProp.set(v)\n  }\n  get checked(): boolean {\n    return this._checkedProp() ?? this._internal() ?? this.defaultChecked\n  }\n  @Input({ transform: booleanAttribute }) defaultChecked = false\n  @Output() checkedChange = new EventEmitter<boolean>()\n\n  @Input() size: SwitchSize = 'default'\n  @Input() checkedChildren: SwitchChildren\n  @Input() unCheckedChildren: SwitchChildren\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input() color?: SwitchColor\n  @Input() thumb?: TemplateRef<unknown> | null\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) required = false\n  @Input() name?: string\n  /** Form value when checked (Radix default 'on'). */\n  @Input() value = 'on'\n  @Input('class') className?: string\n\n  private onChange: (v: boolean) => void = () => {}\n  onTouched: () => void = () => {}\n\n  get isDisabled(): boolean {\n    return this.disabled || this.loading\n  }\n\n  get dataState(): 'checked' | 'unchecked' {\n    return this.checked ? 'checked' : 'unchecked'\n  }\n\n  get hasChildren(): boolean {\n    return Boolean(this.checkedChildren || this.unCheckedChildren)\n  }\n\n  get checkedBg(): string {\n    return this.color ? COLOR_MAP[this.color] || this.color : 'var(--primary)'\n  }\n\n  isTemplate(v: SwitchChildren): v is TemplateRef<unknown> {\n    return v instanceof TemplateRef\n  }\n\n  get hostClass(): string {\n    const width = this.hasChildren ? WIDTHS_WITH_CHILDREN[this.size] : WIDTHS[this.size]\n    return 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      `${HEIGHTS[this.size]} ${width}`,\n      // A custom-element host never matches :disabled, so mirror the disabled look via data-disabled.\n      !this.isNativeButton && 'data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50',\n      this.className,\n    )\n  }\n\n  get checkedChildrenClass(): string {\n    return 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      TEXT_SIZES[this.size],\n    )\n  }\n\n  get unCheckedChildrenClass(): string {\n    return 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      TEXT_SIZES[this.size],\n    )\n  }\n\n  get thumbClass(): string {\n    return 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      THUMB_SIZES[this.size],\n      THUMB_TRANSLATE[this.size],\n    )\n  }\n\n  get loaderClass(): string {\n    return cn(\n      'lucide lucide-loader-circle',\n      THUMB_ICON_SIZES[this.size],\n      'text-muted-foreground motion-safe:animate-spin',\n    )\n  }\n\n  toggle(): void {\n    if (this.isDisabled) return\n    this.setChecked(!this.checked)\n  }\n\n  /** Native buttons activate on Enter / Space themselves; the custom-element host needs it. */\n  onKeydown(event: KeyboardEvent): void {\n    if (this.isNativeButton) return\n    if (event.key === 'Enter' || event.key === ' ') {\n      event.preventDefault()\n      this.toggle()\n    }\n  }\n\n  setChecked(next: boolean): void {\n    if (next === this.checked) return\n    this._internal.set(next)\n    this.onChange(next)\n    this.checkedChange.emit(next)\n  }\n\n  writeValue(v: boolean | null): void {\n    this._internal.set(!!v)\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: boolean) => void): void {\n    this.onChange = fn\n  }\n\n  registerOnTouched(fn: () => void): void {\n    this.onTouched = fn\n  }\n\n  setDisabledState(isDisabled: boolean): void {\n    this.disabled = isDisabled\n    this.cdr.markForCheck()\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/switch/switch.component.ts"
    },
    {
      "path": "packages/registry-angular/components/switch/index.ts",
      "content": "export { UiSwitchComponent, type SwitchSize, type SwitchColor, type SwitchChildren } from './switch.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/switch/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "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"
  ]
}