{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "toggle-group",
  "title": "Toggle Group",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/toggle-group/toggle-group.component.ts",
      "content": "import {\n  AfterViewInit,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  DestroyRef,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { toggleVariants, type ToggleVariants } from '@/ui/toggle/toggle.variants'\n\nexport type ToggleGroupType = 'single' | 'multiple'\nexport type ToggleGroupVariant = NonNullable<ToggleVariants['variant']>\nexport type ToggleGroupSize = NonNullable<ToggleVariants['size']>\nexport type ToggleGroupOrientation = 'horizontal' | 'vertical'\n\nconst ITEM_SELECTOR = '[data-slot=\"toggle-group-item\"]'\nconst EASE = 'cubic-bezier(0.22, 1, 0.36, 1)'\n\n/**\n * Angular port of UIPKGE ToggleGroup, behaving like the Radix ToggleGroup the React\n * component wraps: `type=\"single\"` (radio semantics, clicking the pressed item clears\n * the value to '') or `type=\"multiple\"` (pressed buttons), roving focus with arrow keys /\n * Home / End (looping), items reading variant / size / spacing / disabled from the group,\n * and the React sliding selection indicator for single-select. Class strings are\n * identical to the React source.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-toggle-group, [ui-toggle-group]',\n  standalone: true,\n  host: {\n    // Radix: single is a radiogroup, multiple a toolbar; roving root is tabbable and redirects focus.\n    '[attr.role]': 'type === \"multiple\" ? \"toolbar\" : \"radiogroup\"',\n    '[attr.tabindex]': 'rovingFocus ? (tabbingBackOut() || !enabledItems().length ? -1 : 0) : null',\n    '[attr.dir]': 'dir',\n    '[attr.data-orientation]': 'orientation ?? null',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"toggle-group\"',\n    '[attr.data-size]': 'size ?? null',\n    '[attr.data-variant]': 'variant ?? null',\n    '[attr.data-spacing]': 'spacing',\n    '[attr.data-animated]': 'indicatorActive ? \"true\" : \"false\"',\n    '[style.--gap]': 'spacing',\n    '[class]': 'hostClass',\n    '(mousedown)': 'clickFocus = true',\n    '(focus)': 'onRootFocus($event)',\n    '(focusout)': 'tabbingBackOut.set(false)',\n  },\n  template: `\n    @if (indicatorActive) {\n      <span\n        data-slot=\"toggle-group-indicator\"\n        aria-hidden=\"true\"\n        class=\"bg-accent pointer-events-none absolute top-0 left-0 z-0 shadow-xs will-change-transform\"\n        [style]=\"indicatorStyle()\"\n      ></span>\n    }\n    <ng-content />\n  `,\n})\nexport class UiToggleGroupComponent implements AfterViewInit, OnChanges, OnDestroy {\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef)\n\n  @Input() type: ToggleGroupType = 'single'\n  /** Controlled value: a string for `single`, a string[] for `multiple`. */\n  @Input() value?: string | string[]\n  @Input() defaultValue?: string | string[]\n  @Output() valueChange = new EventEmitter<any>()\n  @Input() variant?: ToggleGroupVariant\n  @Input() size?: ToggleGroupSize\n  @Input() spacing = 0\n  /** Sliding selection indicator for single-select (default true). Multi-select keeps item chrome. */\n  @Input({ transform: booleanAttribute }) animated = true\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input({ transform: booleanAttribute }) rovingFocus = true\n  @Input({ transform: booleanAttribute }) loop = true\n  @Input() orientation?: ToggleGroupOrientation\n  @Input() dir: 'ltr' | 'rtl' = 'ltr'\n  @Input('class') className?: string\n\n  private readonly _value = signal<string | string[] | undefined>(undefined)\n  /** Roving tab stop (Radix currentTabStopId): the item last focused inside the group. */\n  readonly tabStop = signal<string | null>(null)\n  /** Shift+Tab from an item: drop the root out of the tab order so focus can leave. */\n  readonly tabbingBackOut = signal(false)\n  clickFocus = false\n  readonly indicatorStyle = signal<Record<string, string>>({ opacity: '0' })\n  private firstPosition = true\n  private observers: { disconnect(): void }[] = []\n\n  get indicatorActive(): boolean {\n    return this.animated !== false && this.type !== 'multiple'\n  }\n\n  get hostClass(): string {\n    return cn(\n      'group/toggle-group relative flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs',\n      this.className,\n    )\n  }\n\n  /** Pressed values as an array, whichever `type` is in use. */\n  get values(): string[] {\n    const v = this.value !== undefined ? this.value : (this._value() ?? this.defaultValue)\n    if (v === undefined || v === null || v === '') return []\n    return Array.isArray(v) ? v : [v]\n  }\n\n  isPressed(value: string): boolean {\n    return this.values.includes(value)\n  }\n\n  toggle(value: string): void {\n    if (this.disabled) return\n    let next: string | string[]\n    if (this.type === 'multiple') {\n      const current = this.values\n      next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]\n    } else {\n      next = this.isPressed(value) ? '' : value\n    }\n    this._value.set(next)\n    this.valueChange.emit(next)\n  }\n\n  private readonly registry = signal<UiToggleGroupItemComponent[]>([])\n\n  register(item: UiToggleGroupItemComponent): void {\n    this.registry.update((list) => [...list, item])\n  }\n\n  unregister(item: UiToggleGroupItemComponent): void {\n    this.registry.update((list) => list.filter((i) => i !== item))\n  }\n\n  /** Focusable items in DOM order (roving focus order). */\n  enabledItems(): UiToggleGroupItemComponent[] {\n    return this.registry()\n      .filter((i) => !i.isDisabled)\n      .sort((a, b) => (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING ? 1 : -1))\n  }\n\n  /** Radix RovingFocusGroup keyboard: arrows (per orientation / dir), Home, End, looping. */\n  onItemKeydown(event: KeyboardEvent, current: UiToggleGroupItemComponent): void {\n    if (!this.rovingFocus) return\n    if (event.key === 'Tab' && event.shiftKey) {\n      this.tabbingBackOut.set(true)\n      return\n    }\n    const rtl = this.dir === 'rtl'\n    const keys: Record<string, 'prev' | 'next' | 'first' | 'last'> = {\n      Home: 'first',\n      End: 'last',\n      PageUp: 'first',\n      PageDown: 'last',\n    }\n    if (this.orientation !== 'vertical') {\n      keys['ArrowLeft'] = rtl ? 'next' : 'prev'\n      keys['ArrowRight'] = rtl ? 'prev' : 'next'\n    }\n    if (this.orientation !== 'horizontal') {\n      keys['ArrowUp'] = 'prev'\n      keys['ArrowDown'] = 'next'\n    }\n    const intent = keys[event.key]\n    if (!intent || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return\n    event.preventDefault()\n    const items = this.enabledItems()\n    const i = items.indexOf(current)\n    let target: UiToggleGroupItemComponent | undefined\n    if (intent === 'first') target = items[0]\n    else if (intent === 'last') target = items[items.length - 1]\n    else {\n      const next = intent === 'next' ? i + 1 : i - 1\n      target = this.loop ? items[(next + items.length) % items.length] : items[next]\n    }\n    target?.element.focus()\n  }\n\n  /** Radix entry focus: keyboard focus on the root moves to the pressed, last-focused or first item. */\n  onRootFocus(event: FocusEvent): void {\n    const keyboard = !this.clickFocus\n    this.clickFocus = false\n    if (event.target !== this.el.nativeElement || !keyboard || this.tabbingBackOut() || !this.rovingFocus) return\n    const items = this.enabledItems()\n    const target = items.find((i) => i.pressed) ?? items.find((i) => i.value === this.tabStop()) ?? items[0]\n    target?.element.focus()\n  }\n\n  ngOnChanges(): void {\n    this.firstPosition = true\n    this.updateIndicator()\n  }\n\n  ngAfterViewInit(): void {\n    const root = this.el.nativeElement\n    root.style.outline = 'none'\n    this.updateIndicator()\n    const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(() => this.updateIndicator()) : null\n    const observeItems = () => root.querySelectorAll(ITEM_SELECTOR).forEach((el) => ro?.observe(el))\n    ro?.observe(root)\n    observeItems()\n    if (ro) this.observers.push(ro)\n    if (typeof MutationObserver !== 'undefined') {\n      const mo = new MutationObserver((mutations) => {\n        if (mutations.some((m) => m.type === 'childList')) observeItems()\n        this.updateIndicator()\n      })\n      mo.observe(root, { attributes: true, attributeFilter: ['data-state'], subtree: true, childList: true })\n      this.observers.push(mo)\n    }\n  }\n\n  private updateIndicator(): void {\n    if (!this.indicatorActive || typeof window === 'undefined') return\n    const root = this.el.nativeElement\n    const active = root.querySelector<HTMLElement>(`${ITEM_SELECTOR}[data-state=\"on\"]`)\n    if (!active) {\n      this.indicatorStyle.set({ opacity: '0' })\n      return\n    }\n    const listRect = root.getBoundingClientRect()\n    const activeRect = active.getBoundingClientRect()\n    const left = activeRect.left - listRect.left + root.scrollLeft\n    const top = activeRect.top - listRect.top + root.scrollTop\n    const reduced = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches\n    const transition =\n      reduced || this.firstPosition\n        ? 'none'\n        : `transform 220ms ${EASE}, width 220ms ${EASE}, height 220ms ${EASE}, border-radius 220ms ${EASE}`\n    this.indicatorStyle.set({\n      width: `${activeRect.width}px`,\n      height: `${activeRect.height}px`,\n      transform: `translate3d(${left}px, ${top}px, 0)`,\n      'border-radius': getComputedStyle(active).borderRadius,\n      opacity: '1',\n      transition,\n    })\n    this.firstPosition = false\n  }\n\n  ngOnDestroy(): void {\n    this.observers.splice(0).forEach((o) => o.disconnect())\n  }\n}\n\n/**\n * One toggle in the group. Put it on a `<button>` (`<button ui-toggle-group-item value=\"x\">`)\n * for the Radix DOM; on the custom element it gets button semantics via role + keyboard.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-toggle-group-item, [ui-toggle-group-item]',\n  standalone: true,\n  host: {\n    '[attr.type]': 'isButton ? \"button\" : null',\n    '[attr.role]': 'group.type === \"single\" ? \"radio\" : isButton ? null : \"button\"',\n    '[attr.aria-checked]': 'group.type === \"single\" ? pressed : null',\n    '[attr.aria-pressed]': 'group.type === \"single\" ? null : pressed',\n    '[attr.data-state]': 'pressed ? \"on\" : \"off\"',\n    '[attr.data-disabled]': 'isDisabled ? \"\" : null',\n    '[attr.disabled]': 'isDisabled && isButton ? \"\" : null',\n    '[attr.tabindex]': 'tabIndex',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"toggle-group-item\"',\n    '[attr.data-variant]': 'resolvedVariant ?? null',\n    '[attr.data-size]': 'resolvedSize ?? null',\n    '[attr.data-spacing]': 'group.spacing',\n    '[class]': 'hostClass',\n    '(click)': 'onClick()',\n    '(keydown)': 'onKeydown($event)',\n    '(focus)': 'group.tabStop.set(value)',\n  },\n  template: `<ng-content />`,\n})\nexport class UiToggleGroupItemComponent {\n  readonly group = inject(UiToggleGroupComponent)\n  readonly element = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly isButton = this.element.tagName === 'BUTTON'\n\n  @Input({ required: true }) value!: string\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input() variant?: ToggleGroupVariant\n  @Input() size?: ToggleGroupSize\n  @Input('class') className?: string\n\n  get pressed(): boolean {\n    return this.group.isPressed(this.value)\n  }\n\n  get isDisabled(): boolean {\n    return this.group.disabled || this.disabled\n  }\n\n  get resolvedVariant(): ToggleGroupVariant | undefined {\n    return this.group.variant || this.variant\n  }\n\n  get resolvedSize(): ToggleGroupSize | undefined {\n    return this.group.size || this.size\n  }\n\n  /** Radix roving tabindex: only the current tab stop (the last focused item) is 0; the root takes Tab first. */\n  get tabIndex(): number | null {\n    if (!this.group.rovingFocus) return this.isButton ? null : 0\n    return this.group.tabStop() === this.value && !this.isDisabled ? 0 : -1\n  }\n\n  constructor() {\n    this.group.register(this)\n    inject(DestroyRef).onDestroy(() => this.group.unregister(this))\n  }\n\n  get hostClass(): string {\n    return cn(\n      toggleVariants({ variant: this.resolvedVariant, size: this.resolvedSize }),\n      // z-10 keeps label/icons above the sliding indicator. When the parent\n      // has data-animated=true (single-select), on-state surface lives on the\n      // indicator — suppress item bg so the pill can slide cleanly.\n      'relative z-10 w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10',\n      'group-data-[animated=true]/toggle-group:data-[state=on]:bg-transparent group-data-[animated=true]/toggle-group:data-[state=on]:hover:bg-transparent',\n      // first/last-of-type (not first/last-child): sliding indicator is a sibling span\n      // and must not steal end-cap rounding or the outline left border.\n      'data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first-of-type:rounded-l-md data-[spacing=0]:last-of-type:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first-of-type:border-l',\n      this.className,\n    )\n  }\n\n  onClick(): void {\n    if (this.isDisabled) return\n    this.group.toggle(this.value)\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (!this.isButton && (event.key === 'Enter' || event.key === ' ')) {\n      event.preventDefault()\n      this.onClick()\n      return\n    }\n    this.group.onItemKeydown(event, this)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/toggle-group/toggle-group.component.ts"
    },
    {
      "path": "packages/registry-angular/components/toggle-group/index.ts",
      "content": "export {\n  UiToggleGroupComponent,\n  UiToggleGroupItemComponent,\n  type ToggleGroupType,\n  type ToggleGroupVariant,\n  type ToggleGroupSize,\n  type ToggleGroupOrientation,\n} from './toggle-group.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/toggle-group/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/toggle.json"
  ],
  "description": "Group of `Toggle` buttons that act as a single-select or multi-select control (Radix ToggleGroup semantics: roving focus, radio / pressed aria, sliding single-select indicator). Use for view-mode pickers (grid/list), text-format toolbars, and any \"pick one of N\" button bar.",
  "categories": [
    "action"
  ]
}