# ResizablePanels

A fluid center column with optional, collapsible, user-resizable side panels.

Use ResizablePanels for desktop application shells, editors, inboxes, and workspaces where side content should resize without squashing the primary column. The primitive controls layout and resizing only. Consumers retain ownership of each panel's open state and collapse controls.

```tsx
"use client";

import { useState } from "react";
import {
  Button,
  ResizablePanels,
  ResizeHandle,
  SidePanel,
  usePanelWidth,
} from "@patchui/react";
import {
  SidebarSimple,
  SlidersHorizontal,
} from "@phosphor-icons/react/dist/ssr";

export function ResizablePanelsDemo() {
  const [leftOpen, setLeftOpen] = useState(true);
  const [rightOpen, setRightOpen] = useState(true);
  const [leftWidth, setLeftWidth] = usePanelWidth(
    "patch-ui-demo-left-panel",
    220,
    160,
    360,
  );
  const [rightWidth, setRightWidth] = usePanelWidth(
    "patch-ui-demo-right-panel",
    180,
    160,
    320,
  );

  return (
    <div className="h-96 overflow-x-auto rounded-[var(--radius-12)] bg-layer-1">
      <div className="flex h-full w-full min-w-160 flex-col">
        <header className="flex h-12 shrink-0 items-center gap-2 border-b border-hairline px-3">
          <Button
            variant="tertiary"
            size="sm"
            aria-label="Toggle projects panel"
            aria-pressed={leftOpen}
            onClick={() => setLeftOpen((open) => !open)}
          >
            <SidebarSimple aria-hidden />
          </Button>
          <span className="min-w-0 flex-1 truncate text-small font-medium">
            Release planning
          </span>
          <Button
            variant="tertiary"
            size="sm"
            aria-label="Toggle details panel"
            aria-pressed={rightOpen}
            onClick={() => setRightOpen((open) => !open)}
          >
            <SlidersHorizontal aria-hidden />
          </Button>
        </header>

        <ResizablePanels className="min-h-0 flex-1" centerFloor={200}>
          <SidePanel
            side="left"
            open={leftOpen}
            width={leftWidth}
            min={160}
            max={360}
            onResize={setLeftWidth}
            className="border-r border-hairline bg-layer-1 p-3"
          >
            <p className="mb-2 text-mini font-medium text-ink-muted">
              Projects
            </p>
            <div className="rounded-[var(--radius-8)] bg-layer-hover px-2 py-1.5 text-small">
              Web application
            </div>
            <div className="px-2 py-1.5 text-small text-ink-muted">
              Design system
            </div>
            <div className="px-2 py-1.5 text-small text-ink-muted">
              Documentation
            </div>
          </SidePanel>
          <ResizeHandle side="left" />

          <main className="flex h-full min-w-0 flex-col bg-base p-4">
            <h3 className="text-regular font-medium">July release</h3>
            <p className="mt-1 text-small text-ink-muted">
              The center column remains fluid while either side panel changes.
            </p>
            <div className="mt-4 rounded-[var(--radius-12)] bg-layer-1 p-4 text-small">
              Drag a boundary or focus it and use the arrow keys.
            </div>
          </main>

          <SidePanel
            side="right"
            open={rightOpen}
            width={rightWidth}
            min={160}
            max={320}
            onResize={setRightWidth}
            className="border-l border-hairline bg-layer-1 p-3"
          >
            <p className="mb-3 text-mini font-medium text-ink-muted">Details</p>
            <dl className="grid gap-3 text-small">
              <div>
                <dt className="text-ink-muted">Status</dt>
                <dd>In progress</dd>
              </div>
              <div>
                <dt className="text-ink-muted">Owner</dt>
                <dd>Grace Hopper</dd>
              </div>
            </dl>
          </SidePanel>
          <ResizeHandle side="right" />
        </ResizablePanels>
      </div>
    </div>
  );
}

```

## Installation

```bash
npx shadcn add @patchui/resizable-panels
```

The copied source is available in the [registry JSON](https://ui.hotfix.jobs/r/resizable-panels.json). The canonical implementation lives in [packages/react/src/components/resizable-panels.tsx](https://github.com/hotfix-jobs/patch-ui/blob/main/packages/react/src/components/resizable-panels.tsx).

## Usage

```tsx
const [leftOpen, setLeftOpen] = useState(true);
const [leftWidth, setLeftWidth] = usePanelWidth(
  "workspace-left-panel",
  275,
  240,
  520,
);

<ResizablePanels centerFloor={320}>
  <SidePanel
    side="left"
    open={leftOpen}
    width={leftWidth}
    min={240}
    max={520}
    onResize={setLeftWidth}
  >
    <ProjectNavigation />
  </SidePanel>
  <ResizeHandle side="left" />
  <main>{children}</main>
</ResizablePanels>
```

Place SidePanel and ResizeHandle elements directly inside ResizablePanels. Other direct children are grouped into the fluid center track. A handle is rendered only while its matching side panel is open.

## API reference

| Prop                        | Type                                           | Default | Description                                                                        |
| --------------------------- | ---------------------------------------------- | ------- | ---------------------------------------------------------------------------------- |
| ResizablePanels.centerFloor | number                                         | -       | Minimum center-track width in pixels.                                              |
| SidePanel.side              | "left" \| "right"                              | -       | Pins the fixed-width panel content to an outer edge.                               |
| SidePanel.open              | boolean                                        | -       | Expands or collapses the panel track. The consumer owns this state.                |
| SidePanel.width             | number                                         | -       | Controlled panel width in pixels.                                                  |
| SidePanel.min / max         | number / number                                | -       | Bounds resizing. The group tightens the effective maximum to preserve centerFloor. |
| SidePanel.onResize          | (width: number) => void                        | -       | Receives clamped pointer and keyboard resize values.                               |
| ResizeHandle.side           | "left" \| "right"                              | -       | Connects the separator to the matching SidePanel.                                  |
| usePanelWidth               | (key, default, min, max) => \[width, setWidth] | -       | Clamps and persists a controlled width in localStorage.                            |

## Behavior

* Opening and closing animates the grid tracks with the panel motion tokens. The fixed-width content stays pinned to the outer edge and is revealed instead of compressed.
* Pointer dragging is clamped to the panel's effective range. Dragging does not collapse a panel.
* While dragging, `body.resizing` disables track transitions, selection, and non-resize cursors so the boundary follows the pointer directly.
* Applications should close one or both panels when the viewport cannot accommodate their minimum widths plus centerFloor.
* `prefers-reduced-motion` sets the panel duration to 0ms and removes its easing.

## Accessibility

ResizeHandle is a focusable vertical separator with `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, and a pixel-based `aria-valuetext`. Arrow Right expands a left panel and contracts a right panel. Arrow Left performs the inverse. Toggle buttons that control `open` should expose their state with `aria-pressed` or `aria-expanded`.
