{
  "$schema": "https://uipkge.dev/schema/registry-item.json",
  "name": "collapsible",
  "title": "Collapsible",
  "type": "registry:ui",
  "description": "Headless single-region show/hide primitive. Use it when Accordion is overkill — a single toggle reveals one panel of content. Smooth height animation built in.",
  "categories": [
    "disclosure"
  ],
  "files": [
    {
      "path": "packages/elements-lit/src/components/collapsible/collapsible.ts",
      "content": "import { LitElement, css, html, nothing } from 'lit'\nimport { tailwind } from '../../lib/styles'\nimport { ThemeController } from '../../lib/theme'\n\nlet uid = 0\n\n/**\n * <uip-collapsible> — the registry Collapsible root (Radix Collapsible.Root).\n *\n * Light-DOM layout: put <uip-collapsible-trigger> and\n * <uip-collapsible-content> anywhere inside (a header row, after static rows…),\n * exactly like the React parts. Trigger and content are in the same tree, so\n * the trigger's `aria-controls` resolves to the content's id.\n *\n * `open` is the state (reflected); `default-open` sets the initial state.\n * Events: `open-change` (detail: { open }) — React's `onOpenChange`.\n */\nexport class UipCollapsible extends LitElement {\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    open: { type: Boolean, reflect: true },\n    defaultOpen: { type: Boolean, attribute: 'default-open' },\n    disabled: { type: Boolean, reflect: true },\n  }\n\n  open = false\n  defaultOpen = false\n  disabled = false\n  private initialised = false\n  private parts = new Set<LitElement>()\n  content?: UipCollapsibleContent\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', 'collapsible')\n    if (!this.initialised) {\n      this.initialised = true\n      if (this.defaultOpen && !this.hasAttribute('open')) this.open = true\n    }\n  }\n\n  /** Parts register so they re-render when the state changes. */\n  register(part: LitElement) {\n    this.parts.add(part)\n    if (part instanceof UipCollapsibleContent) this.content = part\n    this.parts.forEach((p) => p.requestUpdate())\n  }\n\n  unregister(part: LitElement) {\n    this.parts.delete(part)\n    if (this.content === part) this.content = undefined\n  }\n\n  toggle() {\n    if (this.disabled) return\n    this.open = !this.open\n    this.dispatchEvent(new CustomEvent('open-change', { detail: { open: this.open }, bubbles: true, composed: true }))\n  }\n\n  protected willUpdate() {\n    this.setAttribute('data-state', this.open ? 'open' : 'closed')\n    this.toggleAttribute('data-disabled', this.disabled)\n  }\n\n  protected updated() {\n    this.parts.forEach((p) => p.requestUpdate())\n  }\n\n  render() {\n    return html`<slot></slot>`\n  }\n}\n\n/** Shared registration with the closest <uip-collapsible>. */\nabstract class CollapsiblePart extends LitElement {\n  protected root?: UipCollapsible | null\n\n  constructor() {\n    super()\n    new ThemeController(this)\n  }\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-uipkge', '')\n    this.root = this.parentElement?.closest('uip-collapsible')\n    this.root?.register(this)\n  }\n\n  disconnectedCallback() {\n    super.disconnectedCallback()\n    this.root?.unregister(this)\n    this.root = undefined\n  }\n\n  protected get isOpen() {\n    return !!this.root?.open\n  }\n  protected get isDisabled() {\n    return !!this.root?.disabled\n  }\n}\n\n/**\n * <uip-collapsible-trigger> — Radix Collapsible.Trigger.\n *\n * Default: renders its own <button> around the slotted content. `as-child`\n * (React's `asChild`): renders nothing of its own and puts `aria-controls`,\n * `aria-expanded`, `data-state` (and `disabled`) on its first child element,\n * e.g. a <uip-button>. Clicking toggles the root.\n */\nexport class UipCollapsibleTrigger extends CollapsiblePart {\n  // display: contents so the button / child is laid out by the surrounding row.\n  static styles = [tailwind, css`:host { display: contents; }`]\n\n  static properties = {\n    asChild: { type: Boolean, attribute: 'as-child' },\n  }\n\n  asChild = false\n  private disabledChild?: Element\n\n  connectedCallback() {\n    super.connectedCallback()\n    this.setAttribute('data-slot', 'collapsible-trigger')\n  }\n\n  private get child() {\n    return this.firstElementChild ?? undefined\n  }\n\n  private onClick() {\n    this.root?.toggle()\n  }\n\n  protected updated() {\n    const open = this.isOpen\n    const state = open ? 'open' : 'closed'\n    this.setAttribute('data-state', state)\n    const contentId = this.root?.content?.id\n    if (this.asChild) {\n      const child = this.child\n      if (!child) return\n      child.setAttribute('aria-expanded', String(open))\n      child.setAttribute('data-state', state)\n      if (contentId) child.setAttribute('aria-controls', contentId)\n      child.toggleAttribute('data-disabled', this.isDisabled)\n      if (this.isDisabled && !child.hasAttribute('disabled')) {\n        child.setAttribute('disabled', '')\n        this.disabledChild = child\n      } else if (!this.isDisabled && this.disabledChild === child) {\n        child.removeAttribute('disabled')\n        this.disabledChild = undefined\n      }\n      return\n    }\n    // aria-controls can't point out of this shadow root by id; element\n    // reflection can (where supported).\n    const button = this.renderRoot.querySelector('button') as\n      | (HTMLButtonElement & { ariaControlsElements?: Element[] | null })\n      | null\n    if (button && 'ariaControlsElements' in button) {\n      button.ariaControlsElements = this.root?.content ? [this.root.content] : null\n    }\n  }\n\n  render() {\n    if (this.asChild) return html`<slot @click=${this.onClick} @slotchange=${() => this.requestUpdate()}></slot>`\n    const open = this.isOpen\n    return html`<button\n      part=\"base\"\n      type=\"button\"\n      data-uipkge=\"\"\n      data-slot=\"collapsible-trigger\"\n      aria-expanded=${open ? 'true' : 'false'}\n      data-state=${open ? 'open' : 'closed'}\n      ?data-disabled=${this.isDisabled}\n      ?disabled=${this.isDisabled}\n      @click=${this.onClick}\n    >\n      <slot></slot>\n    </button>`\n  }\n}\n\n/**\n * <uip-collapsible-content> — Radix Collapsible.Content. The host is the\n * content box (consumer classes such as `mt-1 space-y-1` apply to it) and is\n * `hidden` while closed, like Radix unmounting it.\n */\nexport class UipCollapsibleContent extends CollapsiblePart {\n  // Block while shown; the `hidden` attribute (closed) keeps the UA display:none.\n  static styles = [tailwind, css`:host(:not([hidden])) { display: block; }`]\n\n  connectedCallback() {\n    if (!this.id) this.id = `uip-collapsible-content-${++uid}`\n    super.connectedCallback()\n    this.setAttribute('data-slot', 'collapsible-content')\n  }\n\n  protected willUpdate() {\n    const open = this.isOpen\n    this.setAttribute('data-state', open ? 'open' : 'closed')\n    this.toggleAttribute('data-disabled', this.isDisabled)\n    this.hidden = !open\n  }\n\n  render() {\n    return this.isOpen ? html`<slot></slot>` : nothing\n  }\n}\n\ncustomElements.get('uip-collapsible') || customElements.define('uip-collapsible', UipCollapsible)\ncustomElements.get('uip-collapsible-trigger') || customElements.define('uip-collapsible-trigger', UipCollapsibleTrigger)\ncustomElements.get('uip-collapsible-content') || customElements.define('uip-collapsible-content', UipCollapsibleContent)\n\ndeclare global {\n  interface HTMLElementTagNameMap {\n    'uip-collapsible': UipCollapsible\n    'uip-collapsible-trigger': UipCollapsibleTrigger\n    'uip-collapsible-content': UipCollapsibleContent\n  }\n}\n",
      "type": "registry:file",
      "target": "src/uip/components/collapsible/collapsible.ts"
    }
  ],
  "dependencies": [
    "lit"
  ],
  "registryDependencies": [
    "https://uipkge.dev/r/lit/lit-core.json"
  ]
}
