# Module: `src/hooks/useResizablePanel.ts`

> **Source**: `src/hooks/useResizablePanel.ts` (33 lines)
> **Topo batch**: B1 (leaf)

## Purpose

React hook that backs a draggable splitter between two horizontally-arranged panels. Owns the panel width as state and exposes a mouse handler the host attaches to the splitter element.

## Public interface

```ts
export function useResizablePanel(
  initialWidth: number,
  min?: number,   // default 100
  max?: number,   // default 600
): {
  width: number
  onMouseDown: (e: React.MouseEvent) => void
  setWidth: React.Dispatch<React.SetStateAction<number>>
}
```

`width` is the current panel width (px). `onMouseDown` should be wired to the splitter element. `setWidth` is exposed for programmatic resets — currently unused by callers but kept for parity with the persistence story (see Notes).

## Internal logic

1. `useState(initialWidth)` for `width`.
2. Drag bookkeeping is held in three `useRef` cells (`dragging`, `startX`, `startWidth`) — kept out of state so the move/up handlers never re-render the host.
3. `onMouseDown` (memoized via `useCallback([width])`) snapshots `clientX` and `width`, marks `dragging = true`, and calls `e.preventDefault()` to avoid triggering text selection.
4. A `useEffect` registers global `mousemove` / `mouseup` listeners on `window`. While `dragging.current === true`, `mousemove` updates `width` to `clamp(startWidth + (e.clientX - startX), min, max)`. `mouseup` flips `dragging` back to `false`.
5. The effect cleans up on unmount.

## Dependencies

- **Internal**: none.
- **External**: `react` (`useState`, `useCallback`, `useRef`, `useEffect`).

## Consumers (intra-repo)

- `src/features/annotations/AnnotationsPage.tsx` — left + right panel widths.
- `src/features/dataset/DatasetPage.tsx` — left + right panel widths.

Both pages persist their layout server-side via `UserSettings.{annotationsLeftPanelWidth, annotationsRightPanelWidth, datasetLeftPanelWidth, datasetRightPanelWidth}` (see `src/types/index.ts`). The persistence read/write happens in the page, not in this hook.

## Data models

None.

## Configuration

None.

## External integrations

DOM only — `window.addEventListener('mousemove' / 'mouseup')`.

## Security

None.

## Tests

None.

## Notes / open questions

- The hook is keyboard- and touch-blind: only mouse drag is supported, so accessibility for non-pointer input is missing. Track for the broader a11y pass; out of scope for `/document`.
- `setWidth` is exposed but no current caller hydrates `initialWidth` from `UserSettings` via it — they pass the persisted width directly to `useResizablePanel(persistedWidth)` on mount. If the persisted width arrives **after** mount (asynchronous load), the panel jumps. Flag for the consumers' module docs (B8).
- Default bounds (100 / 600) are arbitrary; consumer pages may want different ceilings (e.g., the annotations sidebar). Currently both consumers accept the defaults.
