{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "checkbox",
  "title": "Checkbox",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/checkbox/checkbox.component.ts",
      "content": "import {\n  ChangeDetectorRef,\n  Component,\n  EventEmitter,\n  Input,\n  Output,\n  ViewEncapsulation,\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 { uniqueId } from '@/ui/popper/popper'\n\n// Copied verbatim from the React checkbox (injected there as a global <style>). Unscoped\n// (ViewEncapsulation.None) so it reaches the indicator; Angular adds it once.\nconst CHECKBOX_MOTION_STYLES = `\n@keyframes checkbox-check-in {\n  0% { opacity: 0; transform: scale(0.55); }\n  70% { opacity: 1; transform: scale(1.08); }\n  100% { opacity: 1; transform: scale(1); }\n}\n[data-slot='checkbox-indicator'] .checkbox-indicator-icon {\n  animation: checkbox-check-in 200ms cubic-bezier(0.22, 1.2, 0.36, 1) both;\n}\n@media (prefers-reduced-motion: reduce) {\n  [data-slot='checkbox-indicator'] .checkbox-indicator-icon {\n    animation: none !important;\n  }\n}\n`\n\nexport type CheckedState = boolean | 'indeterminate'\nexport type CheckboxSize = 'sm' | 'md' | 'lg'\nexport type CheckboxColor = 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | (string & {})\nexport type CheckboxDensity = 'compact' | 'default' | 'comfortable'\nexport interface CheckboxOption {\n  label: string\n  value: string\n  disabled?: boolean\n}\n\nconst SIZE_CLASSES: Record<CheckboxSize, string> = {\n  sm: 'size-3.5',\n  md: 'size-4',\n  lg: 'size-5',\n}\n\nconst ICON_SIZES: Record<CheckboxSize, string> = {\n  sm: 'size-2.5',\n  md: 'size-3.5',\n  lg: 'size-4',\n}\n\nconst COLOR_CLASSES: Record<string, string> = {\n  primary:\n    'data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary',\n  secondary:\n    'data-[state=checked]:bg-secondary data-[state=checked]:border-secondary data-[state=indeterminate]:bg-secondary data-[state=indeterminate]:border-secondary',\n  success:\n    'data-[state=checked]:bg-success data-[state=checked]:border-success data-[state=indeterminate]:bg-success data-[state=indeterminate]:border-success',\n  warning:\n    'data-[state=checked]:bg-warning data-[state=checked]:border-warning data-[state=indeterminate]:bg-warning data-[state=indeterminate]:border-warning',\n  error:\n    'data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:border-destructive',\n  info: 'data-[state=checked]:bg-info data-[state=checked]:border-info data-[state=indeterminate]:bg-info data-[state=indeterminate]:border-info',\n}\n\nconst DENSITY_CLASSES: Record<CheckboxDensity, string> = {\n  compact: 'gap-1',\n  default: 'gap-2',\n  comfortable: 'gap-3',\n}\n\nfunction hasErrors(error: boolean, errorMessages: string | string[] | undefined): boolean {\n  return (\n    error || Boolean(errorMessages && (typeof errorMessages === 'string' ? errorMessages : errorMessages.length > 0))\n  )\n}\n\nfunction toList(errorMessages: string | string[] | undefined): string[] {\n  if (!errorMessages) return []\n  return typeof errorMessages === 'string' ? [errorMessages] : errorMessages\n}\n\nfunction optionalChecked(v: unknown): CheckedState | undefined {\n  return v === undefined || v === null ? undefined : v === 'indeterminate' ? 'indeterminate' : booleanAttribute(v)\n}\n\n/**\n * Angular port of the React Checkbox (Radix Checkbox). The host is React's layout wrapper;\n * inside it the control is a real `<button role=\"checkbox\">` (so `<label for>`, :disabled,\n * Space activation and focus behave natively), with the check / minus indicator, label\n * before / after, hint and error messages.\n *\n * Model is React's `checked` / `defaultChecked` / `checkedChange` (`[(checked)]`,\n * `true | false | 'indeterminate'`) plus NG_VALUE_ACCESSOR. `value` is the form value\n * (Radix default 'on'). `class` / `id` go to the button, as React's className / id do.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-checkbox, [ui-checkbox]',\n  standalone: true,\n  exportAs: 'uiCheckbox',\n  encapsulation: ViewEncapsulation.None,\n  styles: [CHECKBOX_MOTION_STYLES],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiCheckboxComponent), multi: true }],\n  host: {\n    // [attr.class], not [class]: [class] merges in the consumer's static class attribute, so\n    // it would land on the wrapper as well as the button.\n    '[attr.class]': 'wrapperClass',\n    // id / class / aria-label / tabindex belong to the button; keep static attributes off the host.\n    '[attr.id]': 'null',\n    '[attr.aria-label]': 'null',\n    '[attr.tabindex]': 'null',\n  },\n  template: `\n    @if (label && labelPosition === 'before') {\n      <label [attr.for]=\"resolvedId\" [class]=\"labelClass('mr-2')\">{{ label }}</label>\n    }\n    <div class=\"flex items-center\">\n      <button\n        type=\"button\"\n        role=\"checkbox\"\n        data-slot=\"checkbox\"\n        [attr.id]=\"resolvedId\"\n        [attr.value]=\"value\"\n        [attr.name]=\"name\"\n        [attr.aria-label]=\"ariaLabel || null\"\n        [attr.tabindex]=\"tabIndex ?? null\"\n        [attr.aria-checked]=\"state === 'indeterminate' ? 'mixed' : state\"\n        [attr.aria-required]=\"required || null\"\n        [attr.aria-invalid]=\"hasError || null\"\n        [attr.data-state]=\"dataState\"\n        [attr.data-disabled]=\"disabled ? '' : null\"\n        [disabled]=\"disabled\"\n        [class]=\"checkboxClass\"\n        (click)=\"toggle()\"\n        (keydown)=\"onKeydown($event)\"\n        (blur)=\"onTouched()\"\n      >\n        @if (dataState !== 'unchecked' || forceMountIndicator) {\n          <span\n            data-uipkge=\"\"\n            data-slot=\"checkbox-indicator\"\n            [attr.data-state]=\"dataState\"\n            [attr.data-disabled]=\"disabled ? '' : null\"\n            class=\"grid place-content-center text-current transition-none\"\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]=\"iconClass('lucide lucide-loader-circle', 'animate-spin')\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M21 12a9 9 0 1 1-6.219-8.56\" />\n              </svg>\n            } @else if (isIndeterminate) {\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]=\"iconClass('lucide lucide-minus', 'checkbox-indicator-icon')\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M5 12h14\" />\n              </svg>\n            } @else if (!hideIcon) {\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]=\"iconClass('lucide lucide-check', 'checkbox-indicator-icon')\"\n                aria-hidden=\"true\"\n              >\n                <path d=\"M20 6 9 17l-5-5\" />\n              </svg>\n            }\n          </span>\n        }\n      </button>\n      @if (label && labelPosition === 'after') {\n        <label [attr.for]=\"resolvedId\" [class]=\"labelClass('ml-2')\">{{ label }}</label>\n      }\n    </div>\n    @if (hint && !hasError) {\n      <p class=\"text-muted-foreground mt-1 text-xs\">{{ hint }}</p>\n    }\n    @if (hasError) {\n      <div class=\"mt-1 flex flex-col gap-0.5\">\n        @for (msg of errorList; track $index) {\n          <p class=\"text-destructive text-xs\">{{ msg }}</p>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiCheckboxComponent implements ControlValueAccessor {\n  // Zoneless-safe: form writes happen outside template events, so schedule a repaint.\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly autoId = uniqueId('checkbox')\n\n  private readonly _checkedProp = signal<CheckedState | undefined>(undefined)\n  private readonly _internal = signal<CheckedState | null>(null)\n\n  /** Controlled state (React `checked`). Reading it returns the resolved state. */\n  @Input({ transform: optionalChecked })\n  set checked(v: CheckedState | undefined) {\n    this._checkedProp.set(v)\n  }\n  get checked(): CheckedState {\n    return this._checkedProp() ?? this._internal() ?? this.defaultChecked\n  }\n  @Input({ transform: (v: unknown) => optionalChecked(v) ?? false }) defaultChecked: CheckedState = false\n  @Output() checkedChange = new EventEmitter<CheckedState>()\n\n  /** Form value submitted when checked (Radix default 'on'). */\n  @Input() value = 'on'\n  @Input() id?: string\n  @Input() size: CheckboxSize = 'md'\n  @Input() color: CheckboxColor = 'primary'\n  @Input() label?: string\n  @Input() hint?: string\n  @Input() errorMessages?: string | string[]\n  @Input({ transform: booleanAttribute }) error = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) indeterminate = false\n  @Input() density: CheckboxDensity = 'default'\n  @Input({ transform: booleanAttribute }) hideIcon = false\n  @Input({ transform: booleanAttribute }) loading = false\n  @Input() labelPosition: 'before' | 'after' = 'after'\n  @Input({ transform: booleanAttribute }) flat = false\n  @Input({ transform: booleanAttribute }) inline = false\n  @Input({ transform: booleanAttribute }) required = false\n  @Input() name?: string\n  @Input('aria-label') ariaLabel?: string\n  @Input('class') className?: string\n  /** React's `tabIndex` on the Radix button, e.g. -1 inside a clickable row. */\n  @Input('tabindex') tabIndex?: number | string\n\n  private onChange: (v: CheckedState) => void = () => {}\n  onTouched: () => void = () => {}\n\n  get isIndeterminate(): boolean {\n    return this.indeterminate || this.checked === 'indeterminate'\n  }\n\n  /** What Radix Root receives: the `indeterminate` prop forces the mixed state. */\n  get state(): CheckedState {\n    return this.isIndeterminate ? 'indeterminate' : this.checked\n  }\n\n  get dataState(): 'checked' | 'unchecked' | 'indeterminate' {\n    return this.state === 'indeterminate' ? 'indeterminate' : this.state ? 'checked' : 'unchecked'\n  }\n\n  /** React force-mounts the Indicator when indeterminate or hideIcon. */\n  get forceMountIndicator(): boolean {\n    return this.isIndeterminate || this.hideIcon\n  }\n\n  get resolvedId(): string | undefined {\n    return this.id ?? (this.label ? this.autoId : undefined)\n  }\n\n  get hasError(): boolean {\n    return hasErrors(this.error, this.errorMessages)\n  }\n\n  get errorList(): string[] {\n    return toList(this.errorMessages)\n  }\n\n  get wrapperClass(): string {\n    return cn('flex items-start', DENSITY_CLASSES[this.density], this.inline ? 'inline-flex' : 'flex-col')\n  }\n\n  get checkboxClass(): string {\n    return cn(\n      'peer border-input data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shrink-0 rounded-[4px] border shadow-xs transition-colors duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',\n      SIZE_CLASSES[this.size],\n      COLOR_CLASSES[this.color] || COLOR_CLASSES['primary'],\n      this.hasError &&\n        'border-destructive data-[state=checked]:!bg-destructive data-[state=checked]:!border-destructive data-[state=indeterminate]:!bg-destructive data-[state=indeterminate]:!border-destructive',\n      this.flat && 'shadow-none',\n      this.className,\n    )\n  }\n\n  labelClass(side: string): string {\n    return cn(\n      side,\n      cn(\n        'cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70',\n        this.hasError ? 'text-destructive' : '',\n        this.disabled && 'cursor-not-allowed opacity-50',\n      ),\n    )\n  }\n\n  iconClass(lucide: string, extra: string): string {\n    return cn(lucide, ICON_SIZES[this.size], extra)\n  }\n\n  /** Radix: indeterminate -> checked, otherwise flip. */\n  toggle(): void {\n    if (this.disabled) return\n    this.setChecked(this.state === 'indeterminate' ? true : !this.state)\n  }\n\n  /** WAI-ARIA: checkboxes don't activate on Enter (Radix prevents it). */\n  onKeydown(event: KeyboardEvent): void {\n    if (event.key === 'Enter') event.preventDefault()\n  }\n\n  setChecked(next: CheckedState): void {\n    this._internal.set(next)\n    this.onChange(next)\n    this.checkedChange.emit(next)\n  }\n\n  writeValue(v: CheckedState | null): void {\n    this._internal.set(v ?? false)\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: CheckedState) => 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\nconst normalizeOption = (option: string | CheckboxOption): CheckboxOption =>\n  typeof option === 'string' ? { label: option, value: option } : option\n\n/**\n * Angular port of the React CheckboxGroup. Renders `options` as Checkboxes bound to the\n * selected `value` array (`[(value)]` / `defaultValue` / NG_VALUE_ACCESSOR); projected\n * children render as-is, exactly like React (they are not wired to the group's value).\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-checkbox-group, [ui-checkbox-group]',\n  standalone: true,\n  imports: [UiCheckboxComponent],\n  providers: [{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => UiCheckboxGroupComponent), multi: true }],\n  host: {\n    '[attr.data-slot]': '\"checkbox-group\"',\n    '[attr.role]': '\"group\"',\n    '[attr.data-orientation]': 'actualOrientation',\n    '[class]': 'hostClass',\n  },\n  template: `\n    @if (label) {\n      <label class=\"text-sm font-medium\">{{ label }}</label>\n    }\n    @if (hint && !error) {\n      <p class=\"text-muted-foreground text-xs\">{{ hint }}</p>\n    }\n    <div [class]=\"itemsClass\">\n      @if (normalizedOptions.length > 0) {\n        @for (opt of normalizedOptions; track $index) {\n          <ui-checkbox\n            [value]=\"opt.value\"\n            [label]=\"opt.label\"\n            [disabled]=\"disabled || !!opt.disabled\"\n            [name]=\"name\"\n            [density]=\"density\"\n            [checked]=\"selected.includes(opt.value)\"\n            (checkedChange)=\"toggle(opt.value, $event)\"\n          />\n        }\n      } @else {\n        <ng-content />\n      }\n    </div>\n    @if (error || errorMessages) {\n      <div class=\"flex flex-col gap-0.5\">\n        @for (msg of errorList; track $index) {\n          <p class=\"text-destructive text-xs\">{{ msg }}</p>\n        }\n      </div>\n    }\n  `,\n})\nexport class UiCheckboxGroupComponent implements ControlValueAccessor {\n  private readonly cdr = inject(ChangeDetectorRef)\n  private readonly _valueProp = signal<string[] | undefined>(undefined)\n  private readonly _internal = signal<string[] | null>(null)\n\n  /** Controlled selection (React `value`). Reading it returns the resolved selection. */\n  @Input()\n  set value(v: string[] | undefined | null) {\n    this._valueProp.set(v ?? undefined)\n  }\n  get value(): string[] {\n    return this.selected\n  }\n  @Input() defaultValue?: string[]\n  @Output() valueChange = new EventEmitter<string[]>()\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) error = false\n  @Input() errorMessages?: string | string[]\n  @Input() label?: string\n  @Input() hint?: string\n  @Input() orientation: 'horizontal' | 'vertical' = 'vertical'\n  @Input({ transform: booleanAttribute }) bordered = false\n  @Input() density: CheckboxDensity = 'default'\n  @Input() options?: (string | CheckboxOption)[]\n  @Input() name?: string\n  @Input({ transform: booleanAttribute }) inline = false\n  @Input('class') className?: string\n\n  private onChange: (v: string[]) => void = () => {}\n  private onTouched: () => void = () => {}\n\n  get selected(): string[] {\n    return this._valueProp() ?? this._internal() ?? this.defaultValue ?? []\n  }\n\n  get actualOrientation(): 'horizontal' | 'vertical' {\n    return this.inline ? 'horizontal' : this.orientation\n  }\n\n  get normalizedOptions(): CheckboxOption[] {\n    return (this.options ?? []).map(normalizeOption)\n  }\n\n  get errorList(): string[] {\n    return toList(this.errorMessages)\n  }\n\n  get hostClass(): string {\n    return cn(\n      'flex flex-col gap-2',\n      this.actualOrientation === 'horizontal' ? 'flex-row items-center' : 'flex-col',\n      this.bordered && 'rounded-lg border p-4',\n      this.className,\n    )\n  }\n\n  get itemsClass(): string {\n    return cn('flex gap-4', this.actualOrientation === 'horizontal' ? 'flex-row flex-wrap items-center' : 'flex-col')\n  }\n\n  toggle(optionValue: string, checked: CheckedState): void {\n    const next = checked === true ? [...this.selected, optionValue] : this.selected.filter((v) => v !== optionValue)\n    this._internal.set(next)\n    this.onChange(next)\n    this.onTouched()\n    this.valueChange.emit(next)\n  }\n\n  writeValue(v: string[] | null): void {\n    this._internal.set(v ?? [])\n    this.cdr.markForCheck()\n  }\n\n  registerOnChange(fn: (v: string[]) => 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/checkbox/checkbox.component.ts"
    },
    {
      "path": "packages/registry-angular/components/checkbox/index.ts",
      "content": "export {\n  UiCheckboxComponent,\n  UiCheckboxGroupComponent,\n  type CheckedState,\n  type CheckboxSize,\n  type CheckboxColor,\n  type CheckboxDensity,\n  type CheckboxOption,\n} from './checkbox.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/checkbox/index.ts"
    }
  ],
  "dependencies": [
    "@angular/forms"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Standalone or in-form binary toggle with Radix Checkbox semantics (button role=checkbox, Space toggles, Enter ignored). Supports indeterminate state, sizes, colors, label / hint / error parts and a CheckboxGroup driven by an options array. Pair with Label for clickable text.",
  "categories": [
    "form"
  ]
}