{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "collapsible",
  "title": "Collapsible",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/collapsible/collapsible.component.ts",
      "content": "import {\n  type AfterViewInit,\n  Component,\n  Directive,\n  ElementRef,\n  EventEmitter,\n  Input,\n  type OnDestroy,\n  Output,\n  booleanAttribute,\n  effect,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { afterExitAnimation, uniqueId } from '@/ui/popper/popper'\n\n/**\n * Angular port of UIPKGE Collapsible with Radix behaviour. Root and Trigger are\n * directives so they can sit on an existing element -- the `asChild` equivalent\n * (`<li ui-sidebar-menu-item ui-collapsible [defaultOpen]=\"true\">`). The trigger\n * toggles, carries aria-expanded / aria-controls / data-state (and type=\"button\" on\n * native buttons). Content is hidden with its children unmounted while\n * closed (kept through a data-state exit animation, like Radix Presence), and exposes\n * --radix-collapsible-content-height / -width (plus the --reka-* aliases) for height\n * animations.\n */\n@Directive({\n  selector: 'ui-collapsible, [ui-collapsible]',\n  standalone: true,\n  exportAs: 'uiCollapsible',\n  host: {\n    '[attr.data-slot]': '\"collapsible\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-state]': 'isOpen ? \"open\" : \"closed\"',\n    '[attr.data-disabled]': 'disabled ? \"\" : null',\n  },\n})\nexport class UiCollapsibleComponent {\n  @Input() set open(v: boolean | undefined) {\n    this._controlled.set(v)\n  }\n  get open(): boolean | undefined {\n    return this._controlled()\n  }\n  @Input({ transform: booleanAttribute }) defaultOpen = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Output() openChange = new EventEmitter<boolean>()\n\n  readonly contentId = uniqueId('collapsible-content')\n  private readonly _open = signal<boolean | null>(null)\n  private readonly _controlled = signal<boolean | undefined>(undefined)\n\n  get isOpen(): boolean {\n    const controlled = this._controlled()\n    if (controlled !== undefined) return controlled\n    return this._open() ?? this.defaultOpen\n  }\n\n  setOpen(value: boolean): void {\n    if (this.disabled || value === this.isOpen) return\n    this._open.set(value)\n    this.openChange.emit(value)\n  }\n\n  toggle(): void {\n    this.setOpen(!this.isOpen)\n  }\n}\n\n@Directive({\n  selector: 'ui-collapsible-trigger, [ui-collapsible-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': 'dataSlot',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.aria-expanded]': 'collapsible.isOpen',\n    '[attr.aria-controls]': 'collapsible.contentId',\n    '[attr.data-state]': 'collapsible.isOpen ? \"open\" : \"closed\"',\n    '[attr.data-disabled]': 'collapsible.disabled ? \"\" : null',\n    // Radix renders <button type=\"button\">; an explicit type on the host (or ui-button's) wins.\n    '[attr.type]': 'defaultType ? \"button\" : null',\n    '(click)': 'collapsible.toggle()',\n  },\n})\nexport class UiCollapsibleTriggerComponent {\n  // Radix Slot: the trigger's data-slot overrides a wrapped component's own (ui-button's\n  // \"button\"), but a data-slot written on the element itself wins.\n  readonly dataSlot: string = inject(ElementRef).nativeElement.getAttribute('data-slot') ?? 'collapsible-trigger'\n  readonly collapsible = inject(UiCollapsibleComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly defaultType =\n    this.el.tagName === 'BUTTON' && !this.el.hasAttribute('type') && !this.el.hasAttribute('ui-button')\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-collapsible-content, [ui-collapsible-content]',\n  standalone: true,\n  host: {\n    '[attr.data-slot]': '\"collapsible-content\"',\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.id]': 'collapsible.contentId',\n    '[attr.data-state]': 'collapsible.isOpen ? \"open\" : \"closed\"',\n    '[attr.data-disabled]': 'collapsible.disabled ? \"\" : null',\n    '[attr.hidden]': 'shown ? null : \"\"',\n    '[style.--radix-collapsible-content-height]': 'size().height',\n    '[style.--radix-collapsible-content-width]': 'size().width',\n    '[style.--reka-collapsible-content-height]': 'size().height',\n    '[style.--reka-collapsible-content-width]': 'size().width',\n    '[class]': 'hostClass',\n  },\n  template: `@if (shown) {\n    <ng-content />\n  }`,\n})\nexport class UiCollapsibleContentComponent implements AfterViewInit, OnDestroy {\n  readonly collapsible = inject(UiCollapsibleComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  @Input({ transform: booleanAttribute }) forceMount = false\n  @Input('class') className?: string\n\n  /** Measured open size, exposed as CSS vars (null until first measured). */\n  readonly size = signal<{ height: string | null; width: string | null }>({ height: null, width: null })\n  /** True while the closing exit animation plays (Radix Presence). */\n  private readonly exiting = signal(false)\n  private wasOpen: boolean | null = null\n  private viewReady = false\n  private destroyed = false\n\n  constructor() {\n    // Re-measure on every open, and keep the content present through its exit animation.\n    effect(() => {\n      const open = this.collapsible.isOpen\n      if (this.wasOpen === null) {\n        this.wasOpen = open\n        return\n      }\n      if (open === this.wasOpen) return\n      this.wasOpen = open\n      if (open) {\n        this.exiting.set(false)\n        queueMicrotask(() => this.measure(false))\n      } else {\n        this.exiting.set(true)\n        queueMicrotask(async () => {\n          await afterExitAnimation(this.el)\n          if (!this.destroyed && !this.collapsible.isOpen) this.exiting.set(false)\n        })\n      }\n    })\n  }\n\n  /** Radix: open, animating out, or force-mounted. */\n  get shown(): boolean {\n    return this.collapsible.isOpen || this.exiting() || this.forceMount\n  }\n\n  ngAfterViewInit(): void {\n    this.viewReady = true\n    // The initially-open content is measured without playing its mount animation.\n    if (this.collapsible.isOpen) this.measure(true)\n  }\n\n  private measure(initial: boolean): void {\n    if (!this.viewReady || this.destroyed || typeof getComputedStyle === 'undefined') return\n    const node = this.el\n    const style = node.style\n    const prev = { transitionDuration: style.transitionDuration, animationName: style.animationName }\n    // Block any animation so the full size is measured.\n    style.transitionDuration = '0s'\n    style.animationName = 'none'\n    const rect = node.getBoundingClientRect()\n    this.size.set({ height: `${rect.height}px`, width: `${rect.width}px` })\n    if (!initial) {\n      style.transitionDuration = prev.transitionDuration\n      style.animationName = prev.animationName\n    } else {\n      // Keep the mount animation blocked for the first paint, then restore.\n      const restore = () => {\n        style.transitionDuration = prev.transitionDuration\n        style.animationName = prev.animationName\n      }\n      if (typeof requestAnimationFrame === 'function') requestAnimationFrame(restore)\n      else restore()\n    }\n  }\n\n  ngOnDestroy(): void {\n    this.destroyed = true\n  }\n\n  get hostClass(): string {\n    return cn('block', this.className)\n  }\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/collapsible/collapsible.component.ts"
    },
    {
      "path": "packages/registry-angular/components/collapsible/index.ts",
      "content": "export {\n  UiCollapsibleComponent,\n  UiCollapsibleTriggerComponent,\n  UiCollapsibleContentComponent,\n} from './collapsible.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/collapsible/index.ts"
    }
  ],
  "dependencies": [],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Headless single-region show/hide primitive. Use it when Accordion is overkill — a single toggle reveals one panel of content. Root and trigger are directives, so they can sit on an existing element (asChild).",
  "categories": [
    "disclosure"
  ]
}