# useResizable

## Usage

Hook for adding drag-to-resize behavior to layout regions. Supports single-region and multi-region configurations with snap points, collapsible panels, localStorage persistence, and cascade resize ordering.

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

## Best practices

| Guidance | Practices |
| --- | --- |
| Do | Use `percent(40, { min: pixel(333) })` for a 40% size with a 333px floor, or `percent(10, { max: pixel(400) })` for a 10% size with a 400px ceiling. The options argument is required and carries a floor XOR a ceiling. |
| Do | A structured default is an initial choice only; a structured `minSize` or `maxSize` remains live. State, persistence, callbacks, `resize()`, paint, and ARIA all use resolved pixel numbers. |
| Do | Import `percent` from `@astryx-vue/core` and Table's `pixel` from `@astryx-vue/core/Table`. They are the same pixel binding, exposed once from each entry. |
| Do | Use with `Layout` or `AppShell` sidebar for resizable navigation panels. |
| Do | Set `autoSaveId` to persist user-chosen sizes across page reloads. |
| Don't | Set `minSize` too small; content becomes unreadable. Prefer `collapsible` for panels that can hide entirely. |

## Examples

Common configurations, variations, and states.

### Resizable

Horizontal resizable split with a draggable handle between two panels.

```vue
<script setup lang="ts">
import { useResizable } from '@astryx-vue/core'
import { Card } from '@astryx-vue/core/Card'
import { Heading } from '@astryx-vue/core/Heading'
import { Layout, LayoutContent, LayoutPanel } from '@astryx-vue/core/Layout'
import { ResizeHandle } from '@astryx-vue/core/Resizable'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const sidebar = useResizable({
  defaultSize: 200,
  minSize: 120,
  maxSize: 400,
})
</script>

<template>
  <Card :height="280" variant="muted" :width="600">
    <Layout height="fill">
      <template #start>
        <LayoutPanel :has-divider="false" :width="sidebar.size">
          <VStack :gap="2">
            <Heading :level="4">
              Sidebar
            </Heading>
            <Text color="secondary">
              {{ Math.round(sidebar.size) }}px wide
            </Text>
          </VStack>
        </LayoutPanel>
        <ResizeHandle direction="horizontal" has-divider label="Resize sidebar" :resizable="sidebar.props" />
      </template>
      <LayoutContent>
        <VStack :gap="2">
          <Heading :level="4">
            Content
          </Heading>
          <Text color="secondary">
            Drag the handle to resize the sidebar.
          </Text>
        </VStack>
      </LayoutContent>
    </Layout>
  </Card>
</template>
```

### Resizable — Collapsible with snap points

A collapsible sidebar with snap points, driven by useResizable. Dragging snaps to preset widths, dragging past the minimum collapses the panel, and the expand method restores it programmatically.

```vue
<script setup lang="ts">
import { useResizable } from '@astryx-vue/core'
import { Button } from '@astryx-vue/core/Button'
import { Card } from '@astryx-vue/core/Card'
import { Layout, LayoutContent, LayoutPanel } from '@astryx-vue/core/Layout'
import { ResizeHandle } from '@astryx-vue/core/Resizable'
import { VStack } from '@astryx-vue/core/Stack'
import { Text } from '@astryx-vue/core/Text'

const sidebar = useResizable({
  defaultSize: 240,
  minSize: 160,
  maxSize: 360,
  collapsible: true,
  snaps: [200, 280],
})
</script>

<template>
  <Card :height="240" variant="muted" :width="500">
    <Layout height="fill">
      <template #start>
        <LayoutPanel :has-divider="false" :width="sidebar.size">
          <Text v-if="!sidebar.isCollapsed" color="secondary">
            {{ Math.round(sidebar.size) }}px wide
          </Text>
        </LayoutPanel>
        <ResizeHandle
          direction="horizontal"
          has-divider
          label="Resize sidebar"
          :resizable="sidebar.props"
        />
      </template>
      <LayoutContent>
        <VStack :gap="2">
          <Text color="secondary">
            Drag the handle — it snaps at 200px and 280px. Drag all the way left to collapse the sidebar.
          </Text>
          <Button
            v-if="sidebar.isCollapsed"
            label="Expand sidebar"
            size="sm"
            variant="secondary"
            @click="sidebar.expand()"
          />
        </VStack>
      </LayoutContent>
    </Layout>
  </Card>
</template>
```

## Parameters

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultSize` | `ResizableSize \| string` | `250` | Initial size. Numbers, exact `"Npx"`, and `pixel(value)` are pixels. Exact `"N%"` has no additional pixel bound. `percent(value, { min: pixel(value) })` or `percent(value, { max: pixel(value) })` adds one pixel floor or ceiling. A percentage resolves once into pixels, against `containerRef` when supplied and against the viewport otherwise, and does not track its basis afterwards. |
| `minSize` | `ResizableSize \| string` | `50` | Minimum size. Numbers, exact `"Npx"`, and `pixel(value)` remain pixels; exact `"N%"` has no additional pixel bound; `percent()` adds exactly one. Percentage minimums re-resolve when their basis changes and clamp the current pixel selection. |
| `maxSize` | `ResizableSize \| string` | `Infinity` | Maximum size. Numbers, exact `"Npx"`, and `pixel(value)` remain pixels; exact `"N%"` has no additional pixel bound; `percent()` adds exactly one. Percentage maximums re-resolve when their basis changes and clamp the current pixel selection. |
| `containerRef` | `Ref` | — | The element a percentage is a share of. Caller-owned: the hook never infers one. Omitted, percentages use the viewport. The ref may point at a different element over time and the basis follows it. Until that element is laid out (not yet mounted, `display:none`, detached) percentages use a temporary 1200px basis and nothing is persisted from it. |
| `direction` | `'horizontal' \| 'vertical'` | `'horizontal'` | Which axis this region resizes along. Selects the container's inline or block content-box size as the percentage basis, and must match the direction given to `ResizeHandle`. |
| `collapsible` | `boolean` | `false` | Whether dragging below the collapsed threshold collapses the region to zero. |
| `collapsedSize` | `number` | `40` | Pixel threshold below which a collapsible region snaps to zero. |
| `snaps` | `number[]` | `[]` | The size stops only on these pixel points. An equal distance stays on the earlier point. |
| `autoSaveId` | `string` | — | Key for localStorage persistence of size and collapse state across sessions, stored under `astryx-resizable:`. |
| `defaultIsCollapsed` | `boolean` | `false` | Initial collapse state (uncontrolled). A persisted entry wins over it. |
| `isCollapsed` | `boolean` | — | Controlled collapse state. `collapse()`, `expand()` and a drag past the threshold then report through `onCollapseChange` instead of changing state internally. |
| `onSizeChange` | `(size: number) => void` | — | Called with the resolved pixel size after every drag, resize, or collapse. |
| `onCollapseChange` | `(isCollapsed: boolean) => void` | — | Called once per collapse state change, via drag or programmatically. |

`useResizable({ regions })` instead takes <code>regions</code> (a map of key to <code>ResizableRegionConfig</code>) plus <code>direction</code>, <code>containerRef</code>, and <code>autoSaveId</code>. Each region accepts the size, collapse, and snap fields above, and <code>shrinkOrder</code>: a lower number gives up space first when the container is narrower than the regions.

## Returns

| Name | Type | Description |
| --- | --- | --- |
| `size` | `number` | Current size in pixels. Reactive. |
| `isCollapsed` | `boolean` | Whether the region is currently collapsed. Reactive. |
| `collapse` | `() => void` | Programmatically collapse the region. |
| `expand` | `() => void` | Expand from collapsed state. |
| `resize` | `(size: number) => void` | Resize to a specific pixel value. |
| `props` | `ResizableProps` | Props to spread on the resizable component or pass to `ResizeHandle`. |

With a <code>regions</code> map, the return value is <code>Record&lt;string, ResizableRegion&gt;</code>: one object of the same shape per key.
