# Resizable

Panels that share a space, with handles between them to resize them.

Source: [resizable.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/resizable.tsx)

```tsx
import { FileCodeIcon, FileJsonIcon, FolderIcon } from "lucide-react";

import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tree, TreeItem } from "@/components/ui/tree";

export default function ResizableEditor() {
  return (
    <div className="h-80 w-full max-w-2xl overflow-hidden rounded-lg border-[0.5px] border-border-muted">
      <ResizablePanelGroup>
        <ResizablePanel defaultSize={35} minSize={20} collapsible className="bg-layer-1">
          <ScrollArea className="h-full">
            <Tree
              aria-label="Files"
              defaultExpanded={["src"]}
              defaultValue="src/app.tsx"
              className="p-1.5"
            >
              <TreeItem value="src" label="src" icon={<FolderIcon />}>
                <TreeItem value="src/app.tsx" label="app.tsx" icon={<FileCodeIcon />} />
                <TreeItem value="src/main.tsx" label="main.tsx" icon={<FileCodeIcon />} />
              </TreeItem>
              <TreeItem value="package.json" label="package.json" icon={<FileJsonIcon />} />
            </Tree>
          </ScrollArea>
        </ResizablePanel>
        <ResizableHandle aria-label="Resize files" />
        <ResizablePanel>
          <ResizablePanelGroup orientation="vertical">
            <ResizablePanel defaultSize={65} minSize={25}>
              <ScrollArea className="h-full">
                <pre className="p-4 font-mono text-sm whitespace-pre-wrap text-foreground">
                  {`export function App() {
  return <h1>Hello, world</h1>;
}`}
                </pre>
              </ScrollArea>
            </ResizablePanel>
            <ResizableHandle aria-label="Resize editor" />
            <ResizablePanel minSize={15} collapsible className="bg-layer-1">
              <ScrollArea className="h-full">
                <pre className="p-4 font-mono text-2xs whitespace-pre-wrap text-muted-foreground">
                  {`$ pnpm dev
  VITE ready in 312 ms
  ➜  Local:   http://localhost:5173/`}
                </pre>
              </ScrollArea>
            </ResizablePanel>
          </ResizablePanelGroup>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/resizable
```

## Usage

```tsx
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";

export function Workspace() {
  return (
    <ResizablePanelGroup className="h-96">
      <ResizablePanel defaultSize={25} minSize={15}>
        Sidebar
      </ResizablePanel>
      <ResizableHandle aria-label="Resize sidebar" />
      <ResizablePanel>Content</ResizablePanel>
    </ResizablePanelGroup>
  );
}
```

Put `ResizablePanel`s directly in `ResizablePanelGroup`, or in a Fragment there, with a
`ResizableHandle` between each pair. The group finds its panels among its children, so a panel
inside a component of your own throws an error; a handle can sit in one. The group fills its parent, so give the parent a height, or
give the group one with `className`. Sizes are percentages of the group: `defaultSize` sets where a
panel starts, panels without one share what's left, and `minSize` and `maxSize` bound it. A panel
clips what overflows it, so put long content in a [Scroll Area](https://ui.jarv.is/components/scroll-area.md), and put padding
on the panel's content rather than the panel, so the panel's size is all the space it takes.

A handle resizes the panels on either side of it. Drag it, or focus it and use the arrow keys to
move it 5% at a time, and Home and End to move it as far as it goes. Give each handle an
`aria-label` that names the panel before it, which is the one its value describes.

## Composition

- `ResizablePanelGroup`
  - `ResizablePanel`
  - `ResizableHandle`

## Examples

### Vertical

Set `orientation="vertical"` to stack the panels. Groups nest, so a panel can hold a group of the
other orientation, as the terminal in the example above does.

```tsx
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { ScrollArea } from "@/components/ui/scroll-area";

export default function ResizableVertical() {
  return (
    <div className="h-64 w-full max-w-md overflow-hidden rounded-lg border-[0.5px] border-border-muted">
      <ResizablePanelGroup orientation="vertical">
        <ResizablePanel defaultSize={40} minSize={20}>
          <ScrollArea className="h-full">
            <pre className="p-4 font-mono text-sm whitespace-pre-wrap text-foreground">
              {`select name, email
from customers
where plan = 'team';`}
            </pre>
          </ScrollArea>
        </ResizablePanel>
        <ResizableHandle aria-label="Resize query" />
        <ResizablePanel minSize={20} className="bg-layer-1">
          <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
            Run the query to see results.
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}
```

### With handle

`withHandle` puts a grip on the line, which is easier to find and to grab on a touch screen.

```tsx
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";

export default function ResizableWithHandle() {
  return (
    <div className="h-48 w-full max-w-md overflow-hidden rounded-lg border-[0.5px] border-border-muted">
      <ResizablePanelGroup>
        <ResizablePanel defaultSize={35} minSize={20} className="bg-layer-1">
          <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
            Sidebar
          </div>
        </ResizablePanel>
        <ResizableHandle withHandle aria-label="Resize sidebar" />
        <ResizablePanel minSize={30}>
          <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
            Content
          </div>
        </ResizablePanel>
      </ResizablePanelGroup>
    </div>
  );
}
```

### Collapsible

A `collapsible` panel collapses to `collapsedSize`, `0` by default, once it's dragged past half its
`minSize`, or when an arrow key pushes it past `minSize`. Enter on the handle collapses the panel
before it, or the one after it when only that one is collapsible, or restores it to its last size,
and a panel at `0` is hidden. A handle never moves either neighbor past its `minSize` or `maxSize`,
so a panel only collapses when the panel beside it can take the space.

Pass `layout` and `onLayoutChange` to set the sizes yourself, such as from a button, with one size
per panel. An uncontrolled group goes back to its `defaultSize`s when panels are added or removed,
and refits its sizes when a panel's `minSize` or `maxSize` changes, calling `onLayoutChange` either
way.

```tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";

export default function ResizableCollapsible() {
  const [layout, setLayout] = React.useState([30, 70]);
  const [lastWidth, setLastWidth] = React.useState(30);
  const collapsed = layout[0] === 0;

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Button
        variant="secondary"
        size="sm"
        className="self-start"
        onClick={() => {
          if (!collapsed) setLastWidth(layout[0]);
          setLayout(collapsed ? [lastWidth, 100 - lastWidth] : [0, 100]);
        }}
      >
        {collapsed ? "Show sidebar" : "Hide sidebar"}
      </Button>
      <div className="h-48 overflow-hidden rounded-lg border-[0.5px] border-border-muted">
        <ResizablePanelGroup layout={layout} onLayoutChange={setLayout}>
          <ResizablePanel minSize={20} collapsible className="bg-layer-1">
            <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
              Sidebar
            </div>
          </ResizablePanel>
          <ResizableHandle aria-label="Resize sidebar" />
          <ResizablePanel>
            <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
              Content
            </div>
          </ResizablePanel>
        </ResizablePanelGroup>
      </div>
    </div>
  );
}
```

### Disabled

A `disabled` handle stays where it is: it can't be dragged, the keyboard skips it, and it shows the
not-allowed cursor. Panels keep their sizes, and the other handles still move them.

```tsx
import * as React from "react";

import { Field, FieldLabel } from "@/components/ui/field";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { Switch } from "@/components/ui/switch";

export default function ResizableDisabled() {
  const [locked, setLocked] = React.useState(true);

  return (
    <div className="flex w-full max-w-md flex-col gap-3">
      <Field orientation="horizontal" className="self-start">
        <Switch checked={locked} onCheckedChange={setLocked} />
        <FieldLabel>Lock layout</FieldLabel>
      </Field>
      <div className="h-48 overflow-hidden rounded-lg border-[0.5px] border-border-muted">
        <ResizablePanelGroup>
          <ResizablePanel defaultSize={35} minSize={20} className="bg-layer-1">
            <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
              Sidebar
            </div>
          </ResizablePanel>
          <ResizableHandle withHandle disabled={locked} aria-label="Resize sidebar" />
          <ResizablePanel minSize={30}>
            <div className="flex h-full items-center justify-center p-4 text-sm text-muted-foreground">
              Content
            </div>
          </ResizablePanel>
        </ResizablePanelGroup>
      </div>
    </div>
  );
}
```

## Props

| Part | Extends |
| --- | --- |
| `ResizablePanelGroup` | `<div>` |
| `ResizablePanel` | `<div>` |
| `ResizableHandle` | `<div>` |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### ResizablePanelGroup

| Prop              | Type                           | Description                                                                                                                                             |
| ----------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orientation?`    | `"horizontal" \| "vertical"`   | Lays the panels out in a row, with handles between them that move sideways, or in a column. Default: `"horizontal"`                                     |
| `layout?`         | `number[]`                     | Each panel's size, as a percentage of the group, one per panel. Without it, or with the wrong number of sizes, the panels start at their `defaultSize`. |
| `onLayoutChange?` | `((layout: number[]) => void)` | Called with the new sizes when a handle moves them, and when adding, removing or rebounding panels refits them.                                         |

### ResizablePanel

| Prop             | Type      | Description                                                                                 |
| ---------------- | --------- | ------------------------------------------------------------------------------------------- |
| `defaultSize?`   | `number`  | The panel's starting size, as a percentage of the group. Panels without one share the rest. |
| `minSize?`       | `number`  | The smallest size, as a percentage of the group.                                            |
| `maxSize?`       | `number`  | The largest size, as a percentage of the group.                                             |
| `collapsible?`   | `boolean` | Lets the panel collapse when it's dragged past half its `minSize`.                          |
| `collapsedSize?` | `number`  | The size it collapses to, as a percentage of the group.                                     |

### ResizableHandle

| Prop          | Type      | Description                                                                                                         |
| ------------- | --------- | ------------------------------------------------------------------------------------------------------------------- |
| `withHandle?` | `boolean` | Shows a grip on the line, which is easier to find and to grab on a touch screen. Default: `false`                   |
| `disabled?`   | `boolean` | Keeps the handle from being dragged or moved with the keyboard, and takes it out of the tab order. Default: `false` |
