---
title: Resizable
description: Arrange panels with draggable and keyboard-operated size boundaries.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Resizable

> Arrange panels with draggable and keyboard-operated size boundaries.

Use Resizable to divide space into draggable, keyboard-accessible panels for multi-pane layouts such as IDEs, email clients, and split dashboards. For single expandable disclosures or collapsible drawers, see [Collapsible](https://moraine.subf.dev/components/collapsible.md) or [Sheet](https://moraine.subf.dev/components/sheet.md).

The root manages the normalized size vector across all panels, while `Resizable.Panel` declares minimum, maximum, and collapsible constraints. Place `Resizable.Handle` between panels to enable pointer dragging, keyboard resizing, and collapse toggles.

## Basic usage

```tsx
import { Resizable } from 'moraine'

export function Example() {
  return (
    <div class="h-64 w-full border border-border/60 rounded-xl overflow-hidden bg-card/30">
      <Resizable defaultValue={['30%', '70%']}>
        <Resizable.Panel min="20%" class="p-4 bg-muted/20">
          Sidebar
        </Resizable.Panel>
        <Resizable.Handle />
        <Resizable.Panel min="30%" class="p-4 bg-background/50">
          Content
        </Resizable.Panel>
      </Resizable>
    </div>
  )
}
```

## Anatomy

```text
Resizable [component; slot=root]
├── Resizable.Panel [part; slot=panel]
├── Resizable.Handle [part; slot=handle]
│   ├── intersection [slot]
│   ├── handleControl [slot]
│   └── intersection [slot]
└── Resizable.Panel [part; slot=panel]
```

## Usage

### Panel constraints

Give the root `defaultValue` for initial sizes and set `min` or `max` on each panel. Values can be pixel numbers (e.g. `240`) or percentage strings relative to the root (e.g. `'25%'`). Handles push and pull adjacent panels; boundaries prevent resizing past minimum or maximum thresholds. Panels with `resizable={false}` remain locked at their current size.

```tsx
import { Icon, Resizable } from 'moraine'

export function Constraints() {
  return (
    <div class="border border-border/60 rounded-xl bg-card/30 h-64 w-full shadow-xs overflow-hidden">
      <Resizable defaultValue={['32%', '68%']}>
        <Resizable.Panel
          min="25%"
          max="45%"
          class="p-3 bg-muted/20 flex flex-col min-w-0 justify-between"
        >
          <div class="space-y-3">
            <div class="flex items-center justify-between">
              <span class="text-xs text-foreground font-semibold flex gap-1.5 items-center">
                <Icon name="i-lucide:sliders" class="text-muted-foreground size-3.5" />
                Inspector
              </span>
              <span class="text-[10px] text-muted-foreground font-mono px-1.5 py-0.5 rounded bg-muted">
                25% – 45%
              </span>
            </div>

            <div class="text-xs space-y-2">
              <div class="p-2 border border-border/40 rounded-lg bg-background/60 space-y-1">
                <div class="text-[11px] text-muted-foreground">Canvas Grid</div>
                <div class="text-xs text-foreground font-medium">8px Baseline (Fluid)</div>
              </div>
              <div class="p-2 border border-border/40 rounded-lg bg-background/60 space-y-1">
                <div class="text-[11px] text-muted-foreground">Color Profile</div>
                <div class="text-xs text-foreground font-medium">Display P3 (Wide Gamut)</div>
              </div>
            </div>
          </div>

          <div class="text-[11px] text-muted-foreground/80 flex gap-1 items-center">
            <Icon name="i-lucide:info" class="shrink-0 size-3" />
            <span class="truncate">Boundary stops dragging at limits.</span>
          </div>
        </Resizable.Panel>

        <Resizable.Handle />

        <Resizable.Panel min="35%" class="p-4 bg-background/50 flex flex-col justify-between">
          <div class="space-y-2">
            <div class="flex items-center justify-between">
              <span class="text-xs text-muted-foreground font-medium flex gap-1.5 items-center">
                <Icon name="i-lucide:file-text" class="size-3.5" />
                design-tokens.json
              </span>
              <span class="text-[10px] text-muted-foreground">Viewport: min 35%</span>
            </div>
            <div class="text-xs text-muted-foreground font-mono p-3 border border-border/50 rounded-lg bg-muted/10 space-y-1">
              <div>{'{'}</div>
              <div class="text-emerald-600 pl-4 dark:text-emerald-400">
                "spacing-unit": "0.25rem",
              </div>
              <div class="text-blue-600 pl-4 dark:text-blue-400">"radius-panel": "0.75rem",</div>
              <div class="text-purple-600 pl-4 dark:text-purple-400">"handle-hit-area": "6px"</div>
              <div>{'}'}</div>
            </div>
          </div>

          <div class="text-xs text-muted-foreground pt-2 border-t border-border/40 flex items-center justify-between">
            <span>Drag the separator to verify min and max limits.</span>
            <span class="text-[11px] font-mono">68% active</span>
          </div>
        </Resizable.Panel>
      </Resizable>
    </div>
  )
}
```

### Collapsible panels

Set `collapsible` on a panel to let it collapse down to `collapsibleMin` (defaulting to `0`). Set `action="collapse"` on the adjacent `Resizable.Handle` to make clicking the grip or pressing <kbd>Enter</kbd> toggle the panel. Handle children can be a render function receiving live state (`collapsed`, `dragging`, `active`) to adapt its icon. `onCollapse` and `onExpand` emit the panel's pixel size when toggled.

```tsx
import { Button, Icon, Resizable } from 'moraine'
import { For, createSignal, Show } from 'solid-js'

const NAV_ITEMS = [
  { icon: 'i-lucide:layout-dashboard', label: 'Dashboard', badge: '3' },
  { icon: 'i-lucide:folder-git-2', label: 'Repositories' },
  { icon: 'i-lucide:activity', label: 'Deployments', badge: 'Live' },
  { icon: 'i-lucide:settings', label: 'Settings' },
]

export function Collapsible() {
  const [collapsed, setCollapsed] = createSignal(false)
  const [pixelSize, setPixelSize] = createSignal(220)

  return (
    <div class="space-y-3">
      <div class="text-xs flex items-center justify-between">
        <div class="flex gap-2 items-center">
          <Button
            variant="outline"
            size="xs"
            onClick={() => setCollapsed((prev) => !prev)}
            leading={collapsed() ? 'i-lucide:panel-left-open' : 'i-lucide:panel-left-close'}
          >
            {collapsed() ? 'Expand sidebar' : 'Collapse sidebar'}
          </Button>
          <span class="text-muted-foreground">Click the handle grip or shortcut button</span>
        </div>
        <span class="text-[11px] text-muted-foreground font-mono">
          {collapsed() ? 'Rail: 56px (collapsed)' : `Panel: ${Math.round(pixelSize())}px`}
        </span>
      </div>

      <div class="border border-border/60 rounded-xl bg-card/30 h-64 w-full shadow-xs overflow-hidden">
        <Resizable>
          <Resizable.Panel
            min="20%"
            max="40%"
            collapsible
            collapsibleMin={56}
            onCollapse={(px) => {
              setCollapsed(true)
              setPixelSize(px)
            }}
            onExpand={(px) => {
              setCollapsed(false)
              setPixelSize(px)
            }}
            class="bg-muted/25 flex flex-col transition-colors justify-between"
          >
            <div class="p-2 space-y-1">
              <For each={NAV_ITEMS}>
                {(item) => (
                  <div
                    class={`text-xs px-2.5 py-1.5 rounded-lg flex gap-2.5 cursor-pointer transition-colors items-center ${
                      item.label === 'Dashboard'
                        ? 'bg-accent text-accent-foreground font-medium'
                        : 'text-muted-foreground hover:bg-muted/60 hover:text-foreground'
                    }`}
                  >
                    <Icon name={item.icon} class="shrink-0 size-4" />
                    <span class="flex-1 truncate">{item.label}</span>
                    <Show when={item.badge}>
                      <span class="text-[10px] text-muted-foreground font-mono px-1 rounded bg-muted">
                        {item.badge}
                      </span>
                    </Show>
                  </div>
                )}
              </For>
            </div>

            <div class="p-2.5 border-t border-border/40 flex gap-2 items-center">
              <div class="text-[10px] text-primary font-semibold rounded-full bg-primary/20 flex size-6 items-center justify-center">
                MO
              </div>
              <div class="text-[11px] text-muted-foreground truncate">moraine-app</div>
            </div>
          </Resizable.Panel>

          <Resizable.Handle action="collapse">
            {(state) => (
              <Icon
                name={state.collapsed ? 'i-lucide:panel-left-open' : 'i-lucide:panel-left-close'}
                class="text-muted-foreground/80 size-3"
              />
            )}
          </Resizable.Handle>

          <Resizable.Panel class="p-4 bg-background/50 flex flex-col justify-between">
            <div class="space-y-3">
              <div class="pb-2 border-b border-border/40 flex items-center justify-between">
                <span class="text-xs text-foreground font-semibold">Project Overview</span>
                <span class="text-[10px] text-muted-foreground">Updated 2m ago</span>
              </div>
              <div class="text-xs gap-2 grid grid-cols-2">
                <div class="p-3 border border-border/50 rounded-lg bg-muted/15 space-y-1">
                  <div class="text-[11px] text-muted-foreground">Build Status</div>
                  <div class="text-emerald-600 font-medium flex gap-1.5 items-center dark:text-emerald-400">
                    <span class="rounded-full bg-emerald-500 size-1.5 animate-pulse" />
                    Operational
                  </div>
                </div>
                <div class="p-3 border border-border/50 rounded-lg bg-muted/15 space-y-1">
                  <div class="text-[11px] text-muted-foreground">Hydration Score</div>
                  <div class="text-foreground font-medium">99.8%</div>
                </div>
              </div>
            </div>

            <p class="text-[11px] text-muted-foreground">
              When collapsed, the panel retains an accessible 56px icon rail without unmounting
              state.
            </p>
          </Resizable.Panel>
        </Resizable>
      </div>
    </div>
  )
}
```

### Controlled state and persistence

Use `value` with `onChange` when the surrounding application coordinates layout presets, resets, or persists splits to storage. `onChange` receives exact pixel sizes. Numbers outside the panel count or exceeding bounds are automatically normalized to fit available space.

```tsx
import { Button, Resizable } from 'moraine'
import { createMemo, createSignal } from 'solid-js'

export function Controlled() {
  const [sizes, setSizes] = createSignal<number[]>([280, 520])

  const percentages = createMemo(() => {
    const total = sizes().reduce((a, b) => a + b, 0) || 1
    return sizes().map((size) => `${Math.round((size / total) * 100)}%`)
  })

  return (
    <div class="space-y-3">
      <div class="flex flex-wrap gap-2 items-center justify-between">
        <div class="flex gap-1.5 items-center">
          <Button variant="outline" size="xs" onClick={() => setSizes([200, 600])}>
            25 / 75
          </Button>
          <Button variant="outline" size="xs" onClick={() => setSizes([400, 400])}>
            50 / 50
          </Button>
          <Button variant="outline" size="xs" onClick={() => setSizes([600, 200])}>
            75 / 25
          </Button>
        </div>
        <div class="text-xs text-muted-foreground font-mono">
          Left: {Math.round(sizes()[0] ?? 0)}px ({percentages()[0]}) | Right:{' '}
          {Math.round(sizes()[1] ?? 0)}px ({percentages()[1]})
        </div>
      </div>

      <div class="border border-border/60 rounded-xl bg-card/30 h-60 w-full shadow-xs overflow-hidden">
        <Resizable value={sizes()} onChange={setSizes}>
          <Resizable.Panel min={150} class="p-4 bg-muted/20 flex flex-col justify-between">
            <div class="space-y-2">
              <span class="text-xs text-foreground font-semibold">Worker Logs</span>
              <p class="text-[11px] text-muted-foreground leading-relaxed">
                Controlled through external Solid signal. Size updates trigger seamless state
                synchronization.
              </p>
            </div>
            <span class="text-[10px] text-muted-foreground font-mono">min: 150px</span>
          </Resizable.Panel>

          <Resizable.Handle />

          <Resizable.Panel min={150} class="p-4 bg-background/50 flex flex-col justify-between">
            <div class="space-y-2">
              <span class="text-xs text-foreground font-semibold">Telemetry Output</span>
              <p class="text-[11px] text-muted-foreground leading-relaxed">
                Use controlled state when persisting split configuration to localStorage or URL
                parameters.
              </p>
            </div>
            <span class="text-[10px] text-muted-foreground font-mono">min: 150px</span>
          </Resizable.Panel>
        </Resizable>
      </div>
    </div>
  )
}
```

### Keyboard interaction

Handles render semantic separator elements with `tabIndex={0}`, `role="separator"`, and standard ARIA boundary values.

| Key                                          | Description                                                        |
| -------------------------------------------- | ------------------------------------------------------------------ |
| <kbd>ArrowLeft</kbd> / <kbd>ArrowUp</kbd>    | Decreases the active separator by `keyboardDelta` (default `10%`). |
| <kbd>ArrowRight</kbd> / <kbd>ArrowDown</kbd> | Increases the active separator by `keyboardDelta` (default `10%`). |
| <kbd>Shift</kbd> + <kbd>Arrow</kbd>          | Jumps the active separator to its minimum or maximum limit.        |
| <kbd>Home</kbd>                              | Snaps the adjacent panel to its minimum permitted size.            |
| <kbd>End</kbd>                               | Snaps the adjacent panel to its maximum permitted size.            |
| <kbd>Enter</kbd>                             | Toggles the nearest collapsible panel when `action="collapse"`.    |
| <kbd>Alt</kbd> + drag                        | Distributes delta symmetrically across adjacent panels.            |

## Examples

### Nested IDE workspace

Combine horizontal and vertical Resizable roots to build developer tooling layouts with an explorer sidebar, code editor, and console output. Setting `intersection` on adjoining handles enables 2D diagonal dragging at the intersection junction.

```tsx
import { Icon, Resizable } from 'moraine'
import { For } from 'solid-js'

const FILES = [
  { name: 'src/app.tsx', active: true, icon: 'i-lucide:file-code', color: 'text-blue-500' },
  { name: 'src/theme.ts', icon: 'i-lucide:file-code', color: 'text-blue-500' },
  { name: 'package.json', icon: 'i-lucide:file-json', color: 'text-amber-500' },
  { name: 'README.md', icon: 'i-lucide:file-text', color: 'text-muted-foreground' },
]

export function NestedPanels() {
  return (
    <div class="border border-border/70 rounded-xl bg-card/30 h-80 w-full shadow-xs overflow-hidden">
      <Resizable defaultValue={['28%', '72%']}>
        {/* Sidebar explorer */}
        <Resizable.Panel min="20%" max="40%" class="bg-muted/25 flex flex-col justify-between">
          <div>
            <div class="text-[11px] text-muted-foreground tracking-wider font-semibold px-3 border-b border-border/50 flex h-8 uppercase items-center justify-between">
              <span>Explorer</span>
              <Icon name="i-lucide:more-horizontal" class="size-3.5" />
            </div>
            <div class="p-1 space-y-0.5">
              <For each={FILES}>
                {(file) => (
                  <div
                    class={`text-xs px-2 py-1 rounded-md flex gap-2 cursor-pointer transition-colors items-center ${
                      file.active
                        ? 'bg-accent text-accent-foreground font-medium'
                        : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
                    }`}
                  >
                    <Icon name={file.icon} class={`shrink-0 size-3.5 ${file.color}`} />
                    <span class="truncate">{file.name}</span>
                  </div>
                )}
              </For>
            </div>
          </div>
          <div class="text-[10px] text-muted-foreground px-3 py-1.5 border-t border-border/40 flex items-center justify-between">
            <span>branch: main</span>
            <span class="text-emerald-500">● clean</span>
          </div>
        </Resizable.Panel>

        {/* Intersection handle between sidebar and nested editor/terminal */}
        <Resizable.Handle intersection />

        {/* Nested vertical workspace */}
        <Resizable.Panel min="40%">
          <Resizable orientation="vertical" defaultValue={['62%', '38%']}>
            {/* Editor buffer */}
            <Resizable.Panel min="30%" class="bg-background flex flex-col">
              <div class="text-xs px-3 border-b border-border/50 bg-muted/30 flex gap-2 h-8 items-center">
                <Icon name="i-lucide:file-code" class="text-blue-500 size-3.5" />
                <span class="text-foreground font-medium">app.tsx</span>
                <span class="rounded-full bg-blue-500 size-1.5" />
              </div>
              <div class="text-xs text-muted-foreground font-mono p-3 bg-background/50 flex-1 overflow-auto space-y-1">
                <div class="text-muted-foreground/60">// Resizable Workspace Composition</div>
                <div>
                  <span class="text-purple-500">export function</span>{' '}
                  <span class="text-blue-500">Workspace</span>() {'{'}
                </div>
                <div class="pl-4">
                  <span class="text-purple-500">return</span> (
                </div>
                <div class="text-emerald-600 pl-8 dark:text-emerald-400">
                  {'<Resizable orientation="horizontal">'}
                </div>
                <div class="text-muted-foreground pl-12">{'<Resizable.Panel min="20%" />'}</div>
                <div class="text-muted-foreground pl-12">{'<Resizable.Handle intersection />'}</div>
                <div class="text-emerald-600 pl-8 dark:text-emerald-400">{'</Resizable>'}</div>
                <div class="pl-4">)</div>
                <div>{'}'}</div>
              </div>
            </Resizable.Panel>

            {/* Intersection handle with grip icon */}
            <Resizable.Handle intersection>
              <Icon name="i-lucide:grip-horizontal" class="text-muted-foreground/80 size-3" />
            </Resizable.Handle>

            {/* Terminal console */}
            <Resizable.Panel min="20%" class="bg-muted/20 flex flex-col justify-between">
              <div class="text-[11px] text-muted-foreground px-3 border-b border-border/40 flex gap-3 h-7 items-center">
                <span class="text-foreground font-medium pt-0.5 border-primary border-b-2">
                  Terminal
                </span>
                <span>Output</span>
                <span>Problems (0)</span>
              </div>
              <div class="text-xs text-foreground font-mono p-2.5 space-y-0.5">
                <div class="text-muted-foreground">$ vite --host</div>
                <div class="text-emerald-600 dark:text-emerald-400">
                  ➜ Local: http://localhost:5173/
                </div>
              </div>
              <div class="text-[10px] text-muted-foreground px-2.5 py-1 border-t border-border/30 bg-muted/40 flex items-center justify-between">
                <span>Drag the junction corner to resize both axes simultaneously.</span>
                <span>node v22.14.0</span>
              </div>
            </Resizable.Panel>
          </Resizable>
        </Resizable.Panel>
      </Resizable>
    </div>
  )
}
```

### Multi-column email client

Compose a three-column productivity layout with a folder directory, searchable thread list, and detail reading pane. Min-width constraints safeguard list readability and action layouts.

```tsx
import { Button, Icon, Resizable } from 'moraine'
import { For, createSignal, Show } from 'solid-js'

const FOLDERS = [
  { icon: 'i-lucide:inbox', label: 'Inbox', count: '12', active: true },
  { icon: 'i-lucide:send', label: 'Sent' },
  { icon: 'i-lucide:star', label: 'Starred', count: '4' },
  { icon: 'i-lucide:archive', label: 'Archive' },
  { icon: 'i-lucide:trash-2', label: 'Trash' },
]

const MESSAGES = [
  {
    id: 1,
    sender: 'Sarah Lin',
    subject: 'Moraine 0.5.0 Release Candidate',
    snippet: 'Hey team, the draft release notes and bundle size audit are ready…',
    time: '10:42 AM',
    unread: true,
  },
  {
    id: 2,
    sender: 'GitHub',
    subject: 'Pull request #142 merged',
    snippet: 'chore(resizable): refresh documentation and interactive previews…',
    time: 'Yesterday',
    unread: false,
  },
  {
    id: 3,
    sender: 'Vercel Bot',
    subject: 'Deployment preview succeeded',
    snippet: 'Your preview deployment for branch docs-resizable is live…',
    time: '2d ago',
    unread: false,
  },
]

export function Composition() {
  const [activeMessageId, setActiveMessageId] = createSignal(1)
  const activeMessage = () => MESSAGES.find((m) => m.id === activeMessageId())!

  return (
    <div class="border border-border/60 rounded-xl bg-card/30 h-80 w-full shadow-xs overflow-hidden">
      <Resizable defaultValue={['22%', '36%', '42%']}>
        {/* Column 1: Mailboxes */}
        <Resizable.Panel min="16%" max="30%" class="p-2 bg-muted/25 flex flex-col justify-between">
          <div class="space-y-1">
            <div class="text-xs text-foreground font-semibold px-2 py-1 flex gap-1.5 items-center">
              <Icon name="i-lucide:mail" class="text-primary size-3.5" />
              Mailboxes
            </div>
            <For each={FOLDERS}>
              {(folder) => (
                <div
                  class={`text-xs px-2 py-1.5 rounded-lg flex cursor-pointer transition-colors items-center justify-between ${
                    folder.active
                      ? 'bg-accent text-accent-foreground font-medium'
                      : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground'
                  }`}
                >
                  <div class="flex gap-2 truncate items-center">
                    <Icon name={folder.icon} class="shrink-0 size-3.5" />
                    <span class="truncate">{folder.label}</span>
                  </div>
                  <Show when={folder.count}>
                    <span class="text-[10px] text-muted-foreground font-mono px-1 rounded bg-muted">
                      {folder.count}
                    </span>
                  </Show>
                </div>
              )}
            </For>
          </div>
          <div class="text-[10px] text-muted-foreground p-2 border-t border-border/40">
            3 panes coordinated
          </div>
        </Resizable.Panel>

        <Resizable.Handle />

        {/* Column 2: Message list */}
        <Resizable.Panel min="26%" max="50%" class="bg-background/60 flex flex-col">
          <div class="p-2 border-b border-border/50 flex gap-2 items-center">
            <Icon name="i-lucide:search" class="text-muted-foreground size-3.5" />
            <input
              type="text"
              placeholder="Search messages…"
              class="text-xs outline-none bg-transparent w-full placeholder:text-muted-foreground/60"
            />
          </div>
          <div class="flex-1 overflow-auto divide-border/40 divide-y">
            <For each={MESSAGES}>
              {(msg) => (
                <div
                  onClick={() => setActiveMessageId(msg.id)}
                  class={`text-xs p-3 cursor-pointer transition-colors space-y-1 ${
                    activeMessageId() === msg.id
                      ? 'bg-muted/50'
                      : 'hover:bg-muted/20 text-muted-foreground'
                  }`}
                >
                  <div class="flex items-center justify-between">
                    <span
                      class={`font-medium truncate ${msg.unread ? 'text-primary' : 'text-foreground'}`}
                    >
                      {msg.sender}
                    </span>
                    <span class="text-[10px] text-muted-foreground shrink-0">{msg.time}</span>
                  </div>
                  <div class="text-foreground font-medium truncate">{msg.subject}</div>
                  <p class="text-[11px] text-muted-foreground line-clamp-1">{msg.snippet}</p>
                </div>
              )}
            </For>
          </div>
        </Resizable.Panel>

        <Resizable.Handle />

        {/* Column 3: Reading pane */}
        <Resizable.Panel min="30%" class="p-4 bg-background flex flex-col justify-between">
          <div class="space-y-4">
            <div class="pb-3 border-b border-border/40 flex items-start justify-between">
              <div>
                <div class="text-sm text-foreground font-semibold">{activeMessage().subject}</div>
                <div class="text-xs text-muted-foreground mt-0.5">
                  From: <span class="text-foreground">{activeMessage().sender}</span>
                </div>
              </div>
              <div class="flex gap-1 items-center">
                <Button variant="ghost" size="icon-xs" aria-label="Reply">
                  <Icon name="i-lucide:reply" class="size-3.5" />
                </Button>
                <Button variant="ghost" size="icon-xs" aria-label="Archive">
                  <Icon name="i-lucide:archive" class="size-3.5" />
                </Button>
              </div>
            </div>

            <p class="text-xs text-muted-foreground leading-relaxed">
              {activeMessage().snippet} All test suites and hydration verification checks passed
              with zero warnings across both UnoCSS and Tailwind preset configurations.
            </p>
          </div>

          <div class="text-[11px] text-muted-foreground pt-2 border-t border-border/40 flex items-center justify-between">
            <span>Multi-pane responsive split view</span>
            <span class="text-[10px] font-mono">moraine.v0.5</span>
          </div>
        </Resizable.Panel>
      </Resizable>
    </div>
  )
}
```

### Vertical query workbench

Use `orientation="vertical"` to split horizontal bands such as database query editors and data result tables. The disabled state locks dividers to protect layout integrity during query execution.

```tsx
import { Button, Icon, Resizable } from 'moraine'
import { For, createSignal } from 'solid-js'

const DATA_ROWS = [
  { id: 'usr_01', name: 'Alice Chen', role: 'Maintainer', queries: '1,420', latency: '12ms' },
  { id: 'usr_02', name: 'Devin Cole', role: 'Contributor', queries: '834', latency: '18ms' },
  { id: 'usr_03', name: 'Kiran Patel', role: 'Viewer', queries: '129', latency: '9ms' },
]

export function VerticalDisable() {
  const [disabled, setDisabled] = createSignal(false)

  return (
    <div class="space-y-3">
      <div class="text-xs flex items-center justify-between">
        <div class="flex gap-2 items-center">
          <Button
            variant="outline"
            size="xs"
            onClick={() => setDisabled((prev) => !prev)}
            leading={disabled() ? 'i-lucide:lock' : 'i-lucide:unlock'}
          >
            {disabled() ? 'Unlock divider' : 'Lock divider'}
          </Button>
          <span class="text-muted-foreground">
            {disabled() ? 'Dividers locked against dragging' : 'Drag divider vertically to resize'}
          </span>
        </div>
        <span class="text-[11px] text-muted-foreground font-mono">orientation="vertical"</span>
      </div>

      <div class="border border-border/60 rounded-xl bg-card/30 h-80 w-full shadow-xs overflow-hidden">
        <Resizable
          orientation="vertical"
          disabled={disabled()}
          defaultValue={['52%', '48%']}
          classes={{ handle: 'bg-border/80' }}
        >
          {/* Top Panel: SQL Query Editor */}
          <Resizable.Panel min="25%" class="p-3 bg-muted/20 flex flex-col justify-between">
            <div class="space-y-2">
              <div class="flex items-center justify-between">
                <span class="text-xs text-foreground font-semibold flex gap-1.5 items-center">
                  <Icon name="i-lucide:database" class="text-primary size-3.5" />
                  SQL Query Editor
                </span>
                <span class="text-[10px] text-muted-foreground font-mono">analytics_db.prod</span>
              </div>
              <div class="text-xs text-foreground font-mono p-2.5 border border-border/50 rounded-lg bg-background/70 space-y-1">
                <div>
                  <span class="text-purple-500">SELECT</span> id, name, role, queries, latency
                </div>
                <div>
                  <span class="text-purple-500">FROM</span> team_members
                </div>
                <div>
                  <span class="text-purple-500">WHERE</span> status ={' '}
                  <span class="text-emerald-600 dark:text-emerald-400">'active'</span>
                </div>
                <div>
                  <span class="text-purple-500">ORDER BY</span> queries{' '}
                  <span class="text-purple-500">DESC</span>;
                </div>
              </div>
            </div>

            <div class="pt-2 flex items-center justify-between">
              <span class="text-[11px] text-muted-foreground">Estimated scan: 3 rows (0.4ms)</span>
              <Button size="xs" variant="default" leading="i-lucide:play">
                Execute
              </Button>
            </div>
          </Resizable.Panel>

          <Resizable.Handle>
            <Icon name="i-lucide:grip-horizontal" class="text-muted-foreground/70 size-3" />
          </Resizable.Handle>

          {/* Bottom Panel: Data Table Results */}
          <Resizable.Panel min="25%" class="p-3 bg-background/50 flex flex-col justify-between">
            <div class="overflow-auto space-y-2">
              <div class="text-xs flex items-center justify-between">
                <span class="text-foreground font-medium">Results</span>
                <span class="text-[10px] text-muted-foreground font-mono">3 rows returned</span>
              </div>
              <table class="text-xs text-left w-full">
                <thead>
                  <tr class="text-[11px] text-muted-foreground font-mono border-b border-border/50">
                    <th class="py-1">ID</th>
                    <th class="py-1">NAME</th>
                    <th class="py-1">ROLE</th>
                    <th class="py-1 text-right">LATENCY</th>
                  </tr>
                </thead>
                <tbody class="divide-border/30 divide-y">
                  <For each={DATA_ROWS}>
                    {(row) => (
                      <tr class="hover:bg-muted/30">
                        <td class="text-muted-foreground font-mono py-1">{row.id}</td>
                        <td class="text-foreground font-medium py-1">{row.name}</td>
                        <td class="text-muted-foreground py-1">{row.role}</td>
                        <td class="text-emerald-600 font-mono py-1 text-right dark:text-emerald-400">
                          {row.latency}
                        </td>
                      </tr>
                    )}
                  </For>
                </tbody>
              </table>
            </div>

            <div class="text-[11px] text-muted-foreground pt-1 border-t border-border/40">
              Vertical stack demonstrates flex-column layout with independent scrolling.
            </div>
          </Resizable.Panel>
        </Resizable>
      </div>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-resizable-root` | `resizable` | Identifies the root container of a resizable layout. |
| `data-collapsed` | `resizable-panel` | Present when the panel or section is collapsed. |
| `data-expanded` | `resizable-panel` | Present when the panel, accordion, or menu is expanded. |
| `data-resizing` | `resizable-panel` | Present while resizable handles are actively dragging. |
| `data-transitioning` | `resizable-panel` | Present while a resizable panel is transitioning between sizes. |
| `data-active` | `resizable-handle` | Present when the item or trigger is active. |
| `data-cross` | `resizable-handle` | Present while a resizable handle cross-target is active. |
| `data-dragging` | `resizable-handle` | Present while the related thumb or handle is being dragged. |
| `data-collapse` | `resizable-handle-control` | Present when the handle performs a collapse action. |
| `data-resizable-handle-end-target` | `resizable-intersection` | Identifies the end intersection target for a resize handle. |
| `data-resizable-handle-start-target` | `resizable-intersection` | Identifies the start intersection target for a resize handle. |

## Props

### Resizable

Props for the Resizable component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| defaultValue | readonly ResizableSize[] \| undefined | — | Initial panel sizes for uncontrolled usage. |
| disabled | boolean \| undefined | false | Whether the resizable component is disabled. |
| id | string \| undefined | — | Unique identifier for the resizable root. |
| keyboardDelta | ResizableSize \| undefined | '10%' | The amount to resize when using keyboard shortcuts. |
| onChange | ((sizes: number[]) => void) \| undefined | — | Callback when panel sizes change. Values are in pixels. |
| onHandleKeyDown | ((context: { event: KeyboardEvent handleIndex: number sizes: number[] }) => void) \| undefined | — | Callback when a key is pressed on a handle. |
| onResizeEnd | ((sizes: number[]) => void) \| undefined | — | Callback when a resize operation ends. Values are in pixels. |
| onResizeStart | ((sizes: number[]) => void) \| undefined | — | Callback when a resize operation starts. Values are in pixels. |
| orientation | ResizableOrientation \| undefined | 'horizontal' | Axis along which panels resize. |
| value | readonly ResizableSize[] \| undefined | — | Controlled panel sizes. Numbers are pixels; percentage strings are relative to the root. |
| children | JSX.Element \| undefined | — | Ordered `Resizable.Panel` and `Resizable.Handle` children. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |

### Resizable.Panel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| collapsible | boolean \| undefined | false | Whether the panel is collapsible. |
| collapsibleMin | ResizableSize \| undefined | 0 | Size of the panel when collapsed. Only applies when `collapsible` is true.<br>- Use string as percent, like `20%`<br>- Use number as px, like `328` |
| max | ResizableSize \| undefined | 1 | Maximum size of the panel.<br>- Use string as percent, like `20%`<br>- Use number as px, like `328` |
| min | ResizableSize \| undefined | 0 | Minimum size of the panel.<br>- Use string as percent, like `20%`<br>- Use number as px, like `328` |
| onCollapse | ((size: number) => void) \| undefined | — | Callback when the panel is collapsed. Size is in pixels. |
| onExpand | ((size: number) => void) \| undefined | — | Callback when the panel is expanded. Size is in pixels. |
| resizable | boolean \| undefined | true | Whether the panel is resizable. |
| children | JSX.Element \| undefined | — | Content rendered inside the panel. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |

### Resizable.Handle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| action | 'resize' \| 'collapse' \| undefined | 'resize' | Handle interaction behavior. |
| intersection | boolean \| undefined | false | Whether this handle participates in intersection resizing. |
| children | JSX.Element \| ((props: HandleRenderProps) => JSX.Element) \| undefined | — | Content or render function receiving the live handle state. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
