{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "accordion",
  "title": "Accordion",
  "type": "registry:ui",
  "files": [
    {
      "path": "packages/registry-angular/components/accordion/accordion.component.ts",
      "content": "import {\n  AfterViewChecked,\n  Component,\n  ElementRef,\n  EventEmitter,\n  Input,\n  OnChanges,\n  OnDestroy,\n  Output,\n  SimpleChanges,\n  booleanAttribute,\n  inject,\n  signal,\n  ChangeDetectionStrategy,\n} from '@angular/core'\nimport { cn } from '@/lib/utils'\nimport { afterExitAnimation, uniqueId } from '@/ui/popper/popper'\nimport {\n  accordionVariants,\n  accordionItemVariants,\n  accordionTriggerVariants,\n  type AccordionVariants,\n  type AccordionItemVariants,\n  type AccordionTriggerVariants,\n} from './accordion.variants'\n\nexport type AccordionVariant = NonNullable<AccordionVariants['variant']>\nexport type AccordionType = 'single' | 'multiple'\nexport type AccordionOrientation = 'horizontal' | 'vertical'\n\n/**\n * Angular port of UIPKGE Accordion with Radix Accordion behaviour: `type` single /\n * multiple, controlled `value` or uncontrolled `defaultValue` + `valueChange`,\n * `collapsible`, `disabled`, `orientation`, `dir`. Items read the root's variant\n * (React's AccordionVariantContext), triggers toggle their item and rove focus with\n * Arrow / Home / End, and content measures itself into\n * `--radix-accordion-content-height` so `animate-accordion-down/up` run like React,\n * staying mounted until the close animation ends. Class strings match React.\n */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-accordion, [ui-accordion]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"accordion\"',\n    '[attr.data-variant]': 'variant',\n    '[attr.data-orientation]': 'orientation',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiAccordionComponent implements OnChanges {\n  @Input('class') className?: string\n  @Input() variant: AccordionVariant = 'default'\n  @Input() type: AccordionType = 'single'\n  /** Controlled value: a string for type=\"single\", a string[] for type=\"multiple\". */\n  @Input() value?: string | string[]\n  @Input() defaultValue?: string | string[]\n  @Input({ transform: booleanAttribute }) collapsible = false\n  @Input({ transform: booleanAttribute }) disabled = false\n  @Input() orientation: AccordionOrientation = 'vertical'\n  @Input() dir: 'ltr' | 'rtl' = 'ltr'\n  @Output() valueChange = new EventEmitter<string | string[]>()\n\n  readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  private readonly _value = signal<string[] | null>(null)\n  readonly items = new Set<UiAccordionItemComponent>()\n\n  /** Currently expanded item values. */\n  get openItems(): string[] {\n    if (this.value !== undefined) return toList(this.value)\n    return this._value() ?? toList(this.defaultValue)\n  }\n\n  ngOnChanges(changes: SimpleChanges): void {\n    const change = changes['value']\n    if (change && !change.firstChange) this.notify(toList(change.previousValue), toList(change.currentValue))\n  }\n\n  isOpen(value: string): boolean {\n    return this.openItems.includes(value)\n  }\n\n  /** Radix onItemOpen / onItemClose: the value an item click produces. */\n  toggle(value: string): void {\n    const current = this.openItems\n    let next: string[]\n    if (this.type === 'multiple') {\n      next = current.includes(value) ? current.filter((v) => v !== value) : [...current, value]\n    } else if (current.includes(value)) {\n      if (!this.collapsible) return\n      next = []\n    } else {\n      next = [value]\n    }\n    this._value.set(next)\n    this.valueChange.emit(this.type === 'multiple' ? next : (next[0] ?? ''))\n    if (this.value === undefined) this.notify(current, next)\n  }\n\n  /** Items that just closed keep their content mounted for the exit animation. */\n  private notify(prev: string[], next: string[]): void {\n    for (const item of this.items) {\n      if (prev.includes(item.value) && !next.includes(item.value)) item.content?.beginExit()\n    }\n  }\n\n  /** Radix handleKeyDown: Arrow keys (per orientation / dir), Home and End rove across enabled triggers. */\n  onTriggerKeydown(event: KeyboardEvent, trigger: HTMLElement): void {\n    const triggers = [...this.el.querySelectorAll<HTMLElement>('[data-slot=\"accordion-trigger\"]')].filter(\n      (t) => t.closest('[data-slot=\"accordion\"]') === this.el && !t.hasAttribute('data-disabled'),\n    )\n    const index = triggers.indexOf(trigger)\n    if (index < 0 || !triggers.length) return\n    const last = triggers.length - 1\n    const forward = this.dir === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n    const backward = this.dir === 'rtl' ? 'ArrowRight' : 'ArrowLeft'\n    let next: number\n    if (event.key === 'Home') next = 0\n    else if (event.key === 'End') next = last\n    else if (this.orientation === 'vertical' && event.key === 'ArrowDown') next = index + 1\n    else if (this.orientation === 'vertical' && event.key === 'ArrowUp') next = index - 1\n    else if (this.orientation === 'horizontal' && event.key === forward) next = index + 1\n    else if (this.orientation === 'horizontal' && event.key === backward) next = index - 1\n    else return\n    event.preventDefault()\n    triggers[(next + triggers.length) % triggers.length]!.focus()\n  }\n\n  get hostClass(): string {\n    return cn('block', accordionVariants({ variant: this.variant }), this.className)\n  }\n}\n\nfunction toList(value: string | string[] | undefined | null): string[] {\n  if (Array.isArray(value)) return value\n  return value ? [value] : []\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-accordion-item, [ui-accordion-item]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"accordion-item\"',\n    '[attr.data-state]': 'state',\n    '[attr.data-disabled]': 'isDisabled ? \"\" : null',\n    '[attr.data-orientation]': 'accordion.orientation',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiAccordionItemComponent implements OnDestroy {\n  readonly accordion = inject(UiAccordionComponent)\n  @Input('class') className?: string\n  @Input({ required: true }) value!: string\n  @Input({ transform: booleanAttribute }) disabled = false\n\n  readonly triggerId = uniqueId('accordion-trigger')\n  readonly contentId = uniqueId('accordion-content')\n  content?: UiAccordionContentComponent\n\n  constructor() {\n    this.accordion.items.add(this)\n  }\n\n  ngOnDestroy(): void {\n    this.accordion.items.delete(this)\n  }\n\n  get isOpen(): boolean {\n    return this.accordion.isOpen(this.value)\n  }\n\n  get isDisabled(): boolean {\n    return this.accordion.disabled || this.disabled\n  }\n\n  get state(): 'open' | 'closed' {\n    return this.isOpen ? 'open' : 'closed'\n  }\n\n  toggle(): void {\n    if (!this.isDisabled) this.accordion.toggle(this.value)\n  }\n\n  get hostClass(): string {\n    return cn('block', accordionItemVariants({ variant: this.accordion.variant }), this.className)\n  }\n}\n\n/** Radix renders the header as an <h3>: use `<h3 ui-accordion-header>`; the element form gets heading semantics. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-accordion-header, [ui-accordion-header]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"accordion-header\"',\n    '[attr.data-state]': 'item.state',\n    '[attr.data-disabled]': 'item.isDisabled ? \"\" : null',\n    '[attr.data-orientation]': 'item.accordion.orientation',\n    '[attr.role]': 'isHeading ? null : \"heading\"',\n    '[attr.aria-level]': 'isHeading ? null : 3',\n    '[class]': 'hostClass',\n  },\n  template: `<ng-content />`,\n})\nexport class UiAccordionHeaderComponent {\n  readonly item = inject(UiAccordionItemComponent)\n  readonly isHeading = /^H[1-6]$/.test(inject<ElementRef<HTMLElement>>(ElementRef).nativeElement.tagName)\n  @Input('class') className?: string\n\n  get hostClass(): string {\n    return cn('flex', this.className)\n  }\n}\n\n/** Use `<button ui-accordion-trigger>` (Radix renders a button); other hosts get button semantics. */\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-accordion-trigger, [ui-accordion-trigger]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"accordion-trigger\"',\n    '[attr.id]': 'item.triggerId',\n    '[attr.type]': 'isNativeButton ? \"button\" : null',\n    '[attr.role]': 'isNativeButton ? null : \"button\"',\n    '[attr.tabindex]': 'isNativeButton ? null : (item.isDisabled ? -1 : 0)',\n    '[attr.aria-controls]': 'item.contentId',\n    '[attr.aria-expanded]': 'item.isOpen',\n    '[attr.aria-disabled]': 'ariaDisabled',\n    '[attr.disabled]': 'isNativeButton && item.isDisabled ? \"\" : null',\n    '[attr.data-state]': 'item.state',\n    '[attr.data-disabled]': 'item.isDisabled ? \"\" : null',\n    '[attr.data-orientation]': 'item.accordion.orientation',\n    '[class]': 'hostClass',\n    '(click)': 'item.toggle()',\n    '(keydown)': 'onKeydown($event)',\n  },\n  template: `<ng-content /><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      class=\"lucide lucide-chevron-down text-muted-foreground size-4 shrink-0 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-[state=open]/accordion-trigger:rotate-180 motion-reduce:transition-none\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m6 9 6 6 6-6\" />\n    </svg>`,\n})\nexport class UiAccordionTriggerComponent {\n  readonly item = inject(UiAccordionItemComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  readonly isNativeButton = this.el.tagName === 'BUTTON'\n  @Input('class') className?: string\n\n  /** Radix: an open item that cannot collapse (single, collapsible=false) reports aria-disabled. */\n  get ariaDisabled(): 'true' | null {\n    const a = this.item.accordion\n    return this.item.isOpen && a.type === 'single' && !a.collapsible ? 'true' : null\n  }\n\n  onKeydown(event: KeyboardEvent): void {\n    if (!this.isNativeButton && (event.key === 'Enter' || event.key === ' ')) {\n      event.preventDefault()\n      this.item.toggle()\n      return\n    }\n    this.item.accordion.onTriggerKeydown(event, this.el)\n  }\n\n  get hostClass(): string {\n    return cn(accordionTriggerVariants({ variant: this.item.accordion.variant }), this.className)\n  }\n}\n\n@Component({\n  changeDetection: ChangeDetectionStrategy.Eager,\n  selector: 'ui-accordion-content, [ui-accordion-content]',\n  standalone: true,\n  host: {\n    '[attr.data-uipkge]': '\"\"',\n    '[attr.data-slot]': '\"accordion-content\"',\n    '[attr.id]': 'item.contentId',\n    '[attr.role]': '\"region\"',\n    '[attr.aria-labelledby]': 'item.triggerId',\n    '[attr.data-state]': 'item.state',\n    '[attr.data-disabled]': 'item.isDisabled ? \"\" : null',\n    '[attr.data-orientation]': 'item.accordion.orientation',\n    '[attr.hidden]': 'present ? null : \"\"',\n    '[class]': 'hostClass',\n  },\n  template: `@if (present) {\n    <div class=\"pt-0 pb-4\"><ng-content /></div>\n  }`,\n})\nexport class UiAccordionContentComponent implements AfterViewChecked, OnDestroy {\n  readonly item = inject(UiAccordionItemComponent)\n  private readonly el = inject<ElementRef<HTMLElement>>(ElementRef).nativeElement\n  @Input('class') className?: string\n  @Input({ transform: booleanAttribute }) forceMount = false\n\n  /** True while the close animation runs (Radix Presence keeps the node mounted). */\n  private readonly exiting = signal(false)\n  private wasOpen: boolean | null = null\n  private exitToken = 0\n  private originalAnimation = ''\n\n  constructor() {\n    this.item.content = this\n  }\n\n  get present(): boolean {\n    return this.forceMount || this.item.isOpen || this.exiting()\n  }\n\n  beginExit(): void {\n    this.exiting.set(true)\n  }\n\n  /** Radix CollapsibleContent: measure the natural size into CSS vars before the animation paints. */\n  ngAfterViewChecked(): void {\n    const open = this.item.isOpen\n    if (open === this.wasOpen) return\n    const first = this.wasOpen === null\n    this.wasOpen = open\n    const style = this.el.style\n    if (first) this.originalAnimation = style.animationName\n    // Measure without the animation. On first paint Radix leaves it suppressed, so a\n    // pre-opened item does not animate in; the next open / close restores it.\n    style.animationName = 'none'\n    const rect = this.el.getBoundingClientRect()\n    for (const lib of ['radix', 'reka']) {\n      style.setProperty(`--${lib}-accordion-content-height`, `${rect.height}px`)\n      style.setProperty(`--${lib}-accordion-content-width`, `${rect.width}px`)\n    }\n    if (!first) style.animationName = this.originalAnimation\n    if (!open && !first) {\n      const token = ++this.exitToken\n      void afterExitAnimation(this.el).then(() => {\n        if (token === this.exitToken && !this.item.isOpen) this.exiting.set(false)\n      })\n    } else if (open) {\n      this.exitToken++\n      this.exiting.set(false)\n    }\n  }\n\n  ngOnDestroy(): void {\n    if (this.item.content === this) this.item.content = undefined\n  }\n\n  get hostClass(): string {\n    return cn(\n      'block',\n      'text-muted-foreground overflow-hidden text-sm',\n      'data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down',\n      'duration-200 ease-[cubic-bezier(0.32,0.72,0,1)]',\n      'motion-reduce:animate-none',\n      this.className,\n    )\n  }\n}\n\nexport {\n  accordionVariants,\n  accordionItemVariants,\n  accordionTriggerVariants,\n  type AccordionVariants,\n  type AccordionItemVariants,\n  type AccordionTriggerVariants,\n}\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/accordion/accordion.component.ts"
    },
    {
      "path": "packages/registry-angular/components/accordion/accordion.variants.ts",
      "content": "import type { VariantProps } from 'class-variance-authority'\nimport { cva } from 'class-variance-authority'\n\n/**\n * Variant definitions live in their own file (rather than the package\n * `index.ts`) so consuming Vue SFCs can import without creating a circular\n * dependency through the index. See card.variants.ts for the canonical\n * example + the SSR symptom that motivated the split.\n */\n\nexport const accordionVariants = cva('w-full', {\n  variants: {\n    variant: {\n      // Bottom-border between items (the classic shadcn look).\n      default: '',\n      // Each item is its own bordered card with a small gap between them.\n      separated: 'space-y-2',\n      // Borderless. Use when the parent container already provides framing.\n      ghost: '',\n    },\n  },\n  defaultVariants: {\n    variant: 'default',\n  },\n})\n\nexport const accordionItemVariants = cva('', {\n  variants: {\n    variant: {\n      default: 'border-b border-border last:border-b-0',\n      separated: 'rounded-md border border-border bg-card overflow-hidden',\n      ghost: '',\n    },\n  },\n  defaultVariants: {\n    variant: 'default',\n  },\n})\n\nexport const accordionTriggerVariants = cva(\n  'group/accordion-trigger flex w-full items-center justify-between gap-3 text-sm font-medium text-foreground outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/40 hover:text-primary disabled:pointer-events-none disabled:opacity-50',\n  {\n    variants: {\n      variant: {\n        default: 'py-4',\n        separated: 'px-4 py-3 hover:bg-muted/50',\n        ghost: 'py-3',\n      },\n    },\n    defaultVariants: {\n      variant: 'default',\n    },\n  },\n)\n\nexport type AccordionVariants = VariantProps<typeof accordionVariants>\n\nexport type AccordionItemVariants = VariantProps<typeof accordionItemVariants>\n\nexport type AccordionTriggerVariants = VariantProps<typeof accordionTriggerVariants>\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/accordion/accordion.variants.ts"
    },
    {
      "path": "packages/registry-angular/components/accordion/index.ts",
      "content": "export {\n  UiAccordionComponent,\n  UiAccordionItemComponent,\n  UiAccordionHeaderComponent,\n  UiAccordionTriggerComponent,\n  UiAccordionContentComponent,\n  accordionVariants,\n  accordionItemVariants,\n  accordionTriggerVariants,\n  type AccordionVariants,\n  type AccordionItemVariants,\n  type AccordionTriggerVariants,\n  type AccordionVariant,\n  type AccordionType,\n  type AccordionOrientation,\n} from './accordion.component'\n",
      "type": "registry:ui",
      "target": "~/src/app/components/ui/accordion/index.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority"
  ],
  "devDependencies": [],
  "registryDependencies": [
    "https://uipkge.dev/r/angular/popper.json"
  ],
  "description": "Vertically stacked, collapsible panels — one or many open at a time. Use for FAQs, settings groups, and any place where space is tight but content needs to stay browsable. Radix Accordion behaviour: single / multiple, collapsible, arrow-key navigation between triggers, and a measured --radix-accordion-content-height for the open / close animation.",
  "categories": [
    "disclosure"
  ]
}