{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "accordion",
  "title": "Accordion",
  "type": "registry:ui",
  "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. Built on Radix UI with smooth animation and full keyboard support.",
  "categories": [
    "disclosure"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/accordion/accordion.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { ChevronDown } from 'lucide'\nimport { cn } from '../../lib/utils'\nimport { icon } from '../../lib/icon'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\nimport { accordionItemVariants, accordionTriggerVariants, accordionVariants } from './accordion.variants'\n\ntype Variant = 'default' | 'separated' | 'ghost'\n\ninterface Item {\n  value: string\n  disabled: boolean\n  trigger: Element\n  content?: Element\n  header?: Element\n}\n\nlet uid = 0\n\nconst parseList = (v: string | null) => (v ? v.split(',').map((s) => s.trim()).filter(Boolean) : [])\n\n/**\n * <uip-accordion> — the registry Accordion as ONE web component.\n *\n * Each trigger's aria-controls points at its panel and each panel's\n * aria-labelledby at its trigger. Those ids must live in one shadow root, so\n * items are NOT separate elements: they are flat light-DOM children, paired\n * by `data-value`, that the accordion slots into its own item markup\n * (manual slot assignment):\n *\n *   <uip-accordion type=\"single\" collapsible>\n *     <span slot=\"trigger\" data-value=\"a\">Is it accessible?</span>\n *     <div slot=\"content\" data-value=\"a\">Yes.</div>\n *     <span slot=\"header\" data-value=\"a\">$12/mo</span>   (optional: extra\n *        content in AccordionHeader after the trigger)\n *   </uip-accordion>\n *\n * Like React, the trigger sits directly in the item unless the item has a\n * `header` child, which renders AccordionHeader (h3.flex) around trigger +\n * header content. The trigger child is `display: contents`, so its content\n * lays out as the trigger button's own flex items.\n *\n * `data-disabled` on a trigger child disables that item (AccordionItem\n * `disabled`). Item order is trigger order.\n *\n * Props (React names): type ('single' | 'multiple'), collapsible, value,\n * default-value, disabled, variant ('default' | 'separated' | 'ghost').\n * For type=\"multiple\" `value` is a string[] (attribute: comma-separated).\n *\n * React's per-part `className` → style the shadow parts from outside:\n * `part=\"item\" | \"header\" | \"trigger\" | \"content\"`, e.g.\n * `class=\"[&::part(content)]:px-4\"`.\n *\n * Keyboard: Enter/Space toggle (native button); ArrowDown/ArrowUp move\n * between triggers (wrapping), Home/End jump to first/last.\n * Events: `input`, `change` and `value-change` (detail: { value }).\n */\nexport class UipAccordion extends LitElement {\n  static shadowRootOptions: ShadowRootInit = { ...LitElement.shadowRootOptions, slotAssignment: 'manual' }\n  // :host display can't be a utility class (the host has no template of its own).\n  static styles = [tailwind, css`:host { display: block; }`]\n\n  static properties = {\n    type: { reflect: true },\n    collapsible: { type: Boolean },\n    value: {},\n    defaultValue: { attribute: 'default-value' },\n    disabled: { type: Boolean, reflect: true },\n    variant: { reflect: true },\n    items: { state: true },\n  }\n\n  type: 'single' | 'multiple' = 'single'\n  collapsible = false\n  value: string | string[] = ''\n  defaultValue?: string\n  disabled = false\n  variant: Variant = 'default'\n  private items: Item[] = []\n  private readonly uidBase = `uip-accordion-${++uid}`\n  private closing = new Set<string>()\n  private interacted = false\n  private childObserver?: MutationObserver\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.setAttribute('data-slot', 'accordion')\n    this.setAttribute('data-orientation', 'vertical')\n    if (this.defaultValue != null && !this.openValues.length) this.value = this.defaultValue\n    this.readItems()\n    this.childObserver = new MutationObserver(() => this.readItems())\n    this.childObserver.observe(this, {\n      childList: true,\n      attributes: true,\n      subtree: false,\n      attributeFilter: ['slot', 'data-value', 'data-disabled'],\n    })\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.childObserver?.disconnect()\n  }\n\n  willUpdate(changed: Map<string, unknown>) {\n    this.setAttribute('data-variant', this.variant)\n    if (changed.has('value') && this.type === 'multiple' && typeof this.value === 'string') {\n      this.value = parseList(this.value)\n    }\n  }\n\n  private get openValues(): string[] {\n    const v = this.value\n    if (Array.isArray(v)) return v\n    return this.type === 'multiple' ? parseList(v) : v ? [v] : []\n  }\n\n  private readItems() {\n    const kids = [...this.children]\n    const byValue = (slot: string, value: string) =>\n      kids.find((k) => k.getAttribute('slot') === slot && k.getAttribute('data-value') === value)\n    this.items = kids\n      .filter((k) => k.getAttribute('slot') === 'trigger')\n      .map((trigger) => {\n        const value = trigger.getAttribute('data-value') ?? ''\n        return {\n          value,\n          disabled: trigger.hasAttribute('data-disabled'),\n          trigger,\n          content: byValue('content', value),\n          header: byValue('header', value),\n        }\n      })\n  }\n\n  private panel(i: number) {\n    return this.renderRoot.querySelector<HTMLElement>(`#${this.uidBase}-content-${i}`)\n  }\n\n  private measure(el: HTMLElement) {\n    // Radix: drop the animation while measuring, then set the size vars the\n    // accordion-up/-down keyframes read.\n    el.style.animationName = 'none'\n    const rect = el.getBoundingClientRect()\n    el.style.setProperty('--radix-accordion-content-height', `${el.scrollHeight}px`)\n    el.style.setProperty('--radix-accordion-content-width', `${rect.width}px`)\n    if (this.interacted) el.style.animationName = ''\n  }\n\n  private toggle(i: number) {\n    const item = this.items[i]\n    if (!item || item.disabled || this.disabled) return\n    const open = this.openValues.includes(item.value)\n    let next: string[]\n    if (this.type === 'multiple') {\n      next = open ? this.openValues.filter((v) => v !== item.value) : [...this.openValues, item.value]\n    } else {\n      if (open && !this.collapsible) return\n      next = open ? [] : [item.value]\n    }\n    this.interacted = true\n    // Panels about to close: capture their open height for accordion-up.\n    this.items.forEach((it, j) => {\n      if (this.openValues.includes(it.value) && !next.includes(it.value)) {\n        const el = this.panel(j)\n        if (el) this.measure(el)\n        this.closing.add(it.value)\n        setTimeout(() => this.finishClose(it.value), 400)\n      }\n    })\n    this.value = this.type === 'multiple' ? next : (next[0] ?? '')\n    this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n    this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n    this.dispatchEvent(new CustomEvent('value-change', { detail: { value: this.value }, bubbles: true, composed: true }))\n  }\n\n  private finishClose(value: string) {\n    if (!this.closing.delete(value)) return\n    this.requestUpdate()\n  }\n\n  private onAnimationEnd(e: AnimationEvent, value: string) {\n    if (e.target !== e.currentTarget) return\n    if (!this.openValues.includes(value)) this.finishClose(value)\n  }\n\n  private onKeyDown(e: KeyboardEvent) {\n    const triggers = [...this.renderRoot.querySelectorAll<HTMLButtonElement>('[data-slot=accordion-trigger]')].filter(\n      (t) => !t.disabled,\n    )\n    const i = triggers.indexOf(e.currentTarget as HTMLButtonElement)\n    if (i < 0) return\n    const n = triggers.length\n    const to = { ArrowDown: (i + 1) % n, ArrowUp: (i - 1 + n) % n, Home: 0, End: n - 1 }[e.key]\n    if (to === undefined) return\n    e.preventDefault()\n    triggers[to].focus()\n  }\n\n  protected updated() {\n    // Manual slot assignment: each item's light-DOM parts go into its slots.\n    this.items.forEach((it, i) => {\n      const find = (part: string) =>\n        this.renderRoot.querySelector<HTMLSlotElement>(`slot[data-part=\"${part}\"][data-index=\"${i}\"]`)\n      find('trigger')?.assign(it.trigger)\n      if (it.content) find('content')?.assign(it.content)\n      if (it.header) find('header')?.assign(it.header)\n    })\n    // Newly opened panels: measure now that they are visible.\n    this.items.forEach((it, i) => {\n      const el = this.panel(i)\n      if (!el || !this.openValues.includes(it.value)) return\n      if (el.dataset.measured !== 'open') {\n        el.dataset.measured = 'open'\n        this.measure(el)\n      }\n    })\n    this.items.forEach((it, i) => {\n      const el = this.panel(i)\n      if (el && !this.openValues.includes(it.value)) delete el.dataset.measured\n    })\n  }\n\n  render() {\n    const open = this.openValues\n    const v = this.variant\n    return html`<div part=\"root\" class=${cn(accordionVariants({ variant: v }))}>\n      ${this.items.map((it, i) => {\n        const isOpen = open.includes(it.value)\n        const state = isOpen ? 'open' : 'closed'\n        const disabled = it.disabled || this.disabled\n        const locked = isOpen && this.type === 'single' && !this.collapsible\n        const triggerId = `${this.uidBase}-trigger-${i}`\n        const contentId = `${this.uidBase}-content-${i}`\n        const trigger = html`<button\n          part=\"trigger\"\n          type=\"button\"\n          id=${triggerId}\n          aria-controls=${contentId}\n          aria-expanded=${isOpen ? 'true' : 'false'}\n          aria-disabled=${locked ? 'true' : nothing}\n          data-state=${state}\n          data-orientation=\"vertical\"\n          ?data-disabled=${disabled}\n          ?disabled=${disabled}\n          data-slot=\"accordion-trigger\"\n          class=${cn(accordionTriggerVariants({ variant: v }))}\n          @click=${() => this.toggle(i)}\n          @keydown=${this.onKeyDown}\n        >\n          <slot data-part=\"trigger\" data-index=${i} class=\"[&::slotted(*)]:contents\"></slot>\n          ${icon(\n            ChevronDown,\n            'chevron-down',\n            '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          )}\n        </button>`\n        return html`<div\n          part=\"item\"\n          data-slot=\"accordion-item\"\n          data-state=${state}\n          data-orientation=\"vertical\"\n          ?data-disabled=${disabled}\n          class=${cn(accordionItemVariants({ variant: v }))}\n        >\n          ${it.header\n            ? html`<h3\n                part=\"header\"\n                data-slot=\"accordion-header\"\n                data-state=${state}\n                data-orientation=\"vertical\"\n                ?data-disabled=${disabled}\n                class=\"flex\"\n              >\n                ${trigger}<slot data-part=\"header\" data-index=${i}></slot>\n              </h3>`\n            : trigger}\n          <div\n            part=\"content\"\n            id=${contentId}\n            role=\"region\"\n            aria-labelledby=${triggerId}\n            data-state=${state}\n            data-orientation=\"vertical\"\n            ?data-disabled=${disabled}\n            data-slot=\"accordion-content\"\n            ?hidden=${!isOpen && !this.closing.has(it.value)}\n            class=${cn(\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            )}\n            @animationend=${(e: AnimationEvent) => this.onAnimationEnd(e, it.value)}\n          >\n            <div class=\"pt-0 pb-4\"><slot data-part=\"content\" data-index=${i}></slot></div>\n          </div>\n        </div>`\n      })}\n    </div>`\n  }\n}\n\ncustomElements.get('uip-accordion') || customElements.define('uip-accordion', UipAccordion)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-accordion': UipAccordion\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/accordion/accordion.ts"
    },
    {
      "path": "packages/elements-lit/src/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:file",
      "target": "src/uip/components/accordion/accordion.variants.ts"
    }
  ],
  "dependencies": [
    "class-variance-authority",
    "lit",
    "lucide"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
