# useCollapsible

## Usage

Reusable hook that encapsulates the collapsible state machine. Supports three modes: group-controlled (inside `CollapsibleGroup`), controlled (`isOpen` + `onOpenChange`), and uncontrolled (self-managed with `defaultIsOpen`). Used internally by `Collapsible`.

```ts
import { useCollapsible } from '@astryx-vue/core/Collapsible'
```

Call it in `setup()` and read `isOpen` / `isEnabled`, or bind `isOpen` to `v-show`. The hook only owns state; the trigger markup stays yours.

```vue
<script setup lang="ts">
import { useCollapsible } from '@astryx-vue/core/Collapsible'

const { isOpen, toggle } = useCollapsible({ isCollapsible: true })
</script>

<template>
  <button type="button" :aria-expanded="isOpen" @click="toggle">
    Details
  </button>
  <div v-show="isOpen">
    Hidden while collapsed.
  </div>
</template>
```

Pass an object to `isCollapsible` for controlled state. `isOpen` is the model ref, usually `v-model:isOpen`:

```ts
const isOpen = ref<boolean | undefined>(false)
const collapsible = useCollapsible({
  isCollapsible: { isOpen, onOpenChange: (next) => { isOpen.value = next } },
})
```

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use the hook directly when building custom collapsible components that need Astryx collapsible behavior without the `Collapsible` wrapper. |
| Do | For accordion behavior, wrap items in `CollapsibleGroup` and pass unique `value` props. |
| Don't | Pass local `isOpen`, `defaultIsOpen`, or `onOpenChange` expecting them to run for a `value`-bound group item; the group owns that state and notification path. |
| Don't | Implement your own open/close state when `useCollapsible` already provides it; the hook handles group coordination automatically. |

## Examples

Custom disclosure UI built directly with `useCollapsible` for headless open/close state.

### useCollapsible — Custom Disclosure

Custom disclosure UI built directly with useCollapsible for headless open/close state.

```vue
<script setup lang="ts">
import { useCollapsible } from '@astryx-vue/core/Collapsible'

const { isOpen, isEnabled, toggle } = useCollapsible({ isCollapsible: true })
</script>

<template>
  <div class="hook-collapsible">
    <button
      type="button"
      class="hook-collapsible__trigger"
      :aria-expanded="isOpen"
      aria-controls="use-collapsible-demo-panel"
      @click="toggle"
    >
      <span>Release checklist</span>
      <span class="hook-collapsible__chevron" :data-open="isOpen || undefined" aria-hidden="true" />
    </button>
    <div v-show="isOpen" id="use-collapsible-demo-panel" class="hook-collapsible__panel">
      <p>Review docs, run visual checks, and confirm keyboard behavior before shipping the component update.</p>
      <p class="hook-collapsible__state">
        isEnabled: {{ isEnabled }} · isOpen: {{ isOpen }}
      </p>
    </div>
  </div>
</template>

<style scoped>
.hook-collapsible {
  box-sizing: border-box;
  width: 22rem;
  max-width: 100%;
  border: 1px solid var(--astryx-color-border, #e5e5e5);
  border-radius: var(--astryx-radius-container, 12px);
  color: var(--astryx-color-text-primary, #171717);
  font-size: 0.875rem;
  overflow: hidden;
}

.hook-collapsible__trigger {
  display: flex;
  gap: 12px;
  align-items: center;
  justify-content: space-between;
  box-sizing: border-box;
  width: 100%;
  padding: 12px 16px;
  border: 0;
  background: var(--astryx-color-background-surface, #fff);
  color: inherit;
  font: inherit;
  font-weight: 600;
  text-align: start;
  cursor: pointer;
}

.hook-collapsible__chevron {
  width: 8px;
  height: 8px;
  border-right: 2px solid currentcolor;
  border-bottom: 2px solid currentcolor;
  transform: rotate(45deg);
  transition: transform 150ms ease;
}

.hook-collapsible__chevron[data-open] {
  transform: rotate(225deg);
}

.hook-collapsible__panel {
  padding: 0 16px 16px;
  border-top: 1px solid var(--astryx-color-border, #e5e5e5);
}

.hook-collapsible__panel p {
  margin: 12px 0 0;
  line-height: 1.5;
}

.hook-collapsible__state {
  color: var(--astryx-color-text-secondary, #525252);
  font-family: var(--astryx-font-family-code, ui-monospace, monospace);
  font-size: 0.75rem;
}
</style>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `isCollapsible` | `boolean \| CollapsibleConfig \| MaybeRefOrGetter` | `true` | Whether collapsible behavior is active. `true` manages its own state and starts open. `false` disables folding. Pass a config object for controlled mode or a custom default. |
| `value` | `string \| undefined \| MaybeRefOrGetter` | — | Unique identifier within a `CollapsibleGroup`. When present and inside a group, the group owns the state. |
| `group` | `CollapsibleGroupContextValue \| null` | injected | Group context. Read from the nearest `CollapsibleGroup` when omitted; pass it explicitly only when you are outside that tree. |

### CollapsibleConfig

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `isOpen` | `Ref<boolean \| undefined>` | — | Controlled open state, usually the `v-model:isOpen` model ref. Leave it out for uncontrolled state. |
| `defaultIsOpen` | `boolean \| MaybeRefOrGetter` | `true` | Open state while `isOpen` stays unbound. |
| `onOpenChange` | `(isOpen: boolean) => void` | — | Called with the requested state. In controlled usage it is the request; in uncontrolled usage it is the report. |

## Returns

| Field | Type | Description |
| --- | --- | --- |
| `isEnabled` | `ComputedRef<boolean>` | Whether collapsible behavior is active. False when `isCollapsible` is `false`. |
| `isOpen` | `ComputedRef<boolean>` | Whether the content is currently expanded. |
| `toggle` | `() => void` | Toggle open/closed state. Dispatches to the group, the controlled ref, or internal state. |
