# Hooks

> Input, focus, app control, terminal size, animation, and measurement hooks for voxal UIs

All hooks are imported from `@voxalsh/sdk/ui` and match Ink's. They cover keyboard input, focus, app lifecycle, terminal size, animation, and layout measurement. Call them at the top level of a component, following the usual React rules of hooks.

```jsx

```

## useApp

```ts
useApp(): { exit(error?): void }
```

Returns the app controls. Call `exit()` to end the app, which closes the connection when you are running under [`serve`](/sdk/ui/rendering#serve). Pass an error to surface it to `waitUntilExit`.

```jsx
function App() {
  const { exit } = useApp();
  useInput((input) => {
    if (input === 'q') exit();
  });
  return <Text>press q to quit</Text>;
}
```

## useInput

```ts
useInput(handler: (input: string, key: Key) => void, options?: { isActive?: boolean }): void
```

Subscribes to keyboard input. `input` is the typed character (`''` for named keys), and `key` is the [key object](#the-key-object) describing which special key fired. The handler re-subscribes on every render, so it always sees your latest closure (current state and props).

```jsx
function Menu() {
  const [i, setI] = useState(0);
  useInput((input, key) => {
    if (key.upArrow) setI((n) => Math.max(0, n - 1));
    if (key.downArrow) setI((n) => n + 1);
    if (key.return) console.log('selected', i);
  });
  return <Text>row {i}</Text>;
}
```

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.isActive` | `boolean` | `true` | When `false`, the handler is not subscribed. Use it to disable input for unfocused components. |

<Callout>
While [`exitOnCtrlC`](/sdk/ui/rendering#renderoptions) is on (the default), Ctrl-C is not delivered to your handler. The app exits instead. Set `exitOnCtrlC: false` if you need to handle it yourself.
</Callout>

## The key object

The second argument to your `useInput` handler.

| Field | Description |
| --- | --- |
| `upArrow` `downArrow` `leftArrow` `rightArrow` | Arrow keys. |
| `pageUp` `pageDown` `home` `end` | Navigation keys. |
| `return` `escape` `tab` `backspace` `delete` | Editing and control keys. |
| `ctrl` `shift` `meta` | Modifier flags. |
| `super` `hyper` `capsLock` `numLock` | Accepted, but always `false` over SSH. |

A few rules worth knowing:

- For a named key, `input` is `''` and the matching `key` field is `true`.
- Ctrl plus a letter gives `input` as that letter with `key.ctrl` set to `true`.
- Typing an uppercase letter gives the letter in `input` with `key.shift` set to `true`.

## useFocus

```ts
useFocus(options?: { autoFocus?: boolean; isActive?: boolean; id?: string }): { isFocused: boolean; focus(id): void }
```

Registers the component in the focus system and reports whether it currently has focus. By default, Tab moves focus to the next focusable, Shift+Tab to the previous, and Esc clears focus.

```jsx
function Field({ label }) {
  const { isFocused } = useFocus();
  return (
    <Box borderStyle="round" borderColor={isFocused ? 'cyan' : 'gray'}>
      <Text>{label}</Text>
    </Box>
  );
}
```

| Name | Type | Description |
| --- | --- | --- |
| `options.autoFocus` | `boolean` | Take focus on mount if nothing else is focused. |
| `options.isActive` | `boolean` | When `false`, the component is skipped in focus order. |
| `options.id` | `string` | A stable id so you can target this component with `focus(id)`. |

## useFocusManager

```ts
useFocusManager(): { enableFocus(); disableFocus(); focus(id); focusNext(); focusPrevious(); activeId }
```

Drives focus imperatively: enable or disable the whole focus system, move to the next or previous focusable, jump to a specific `id`, or read the `activeId`.

## useStdout

```ts
useStdout(): { write(data: string): void }
```

Returns a handle that writes raw output to the connection, bypassing the React tree. Use it for one-off escape sequences or raw bytes you do not want to model as components.

## useStderr

```ts
useStderr(): { write(data: string): void }
```

Same shape as `useStdout` and writes to the same connection. There is no separate error stream over SSH.

## useStdin

```ts
useStdin(): StdinHandle
```

Returns a stdin handle for Ink compatibility. `setRawMode` and `setBracketedPasteMode` are no-ops, because the connection is always in raw mode with bracketed paste on, and `isRawModeSupported` is always `true`. For input, prefer [`useInput`](#useinput).

## useWindowSize

```ts
useWindowSize(): { columns: number; rows: number }
```

Returns the live terminal size and re-renders the component when the terminal is resized. Use it to lay out against the current viewport.

```jsx
function Bar() {
  const { columns } = useWindowSize();
  return <Text>{'='.repeat(columns)}</Text>;
}
```

## usePaste

```ts
usePaste(handler: (text: string) => void, options?: { isActive?: boolean }): void
```

Receives pasted text as a single string. While any paste listener is mounted, pasted text goes to it instead of to [`useInput`](#useinput), so you can handle a multi-line paste in one shot rather than character by character.

## useCursor

```ts
useCursor(): { setCursorPosition(pos?: { x: number; y: number }): void }
```

Controls the visible terminal cursor. Call `setCursorPosition({ x, y })` (relative to the UI's top-left) to show the cursor at that cell, or pass `undefined` to hide it. Useful for text inputs where you want a real blinking cursor.

## useBoxMetrics

```ts
useBoxMetrics(ref): { width; height; left; top; hasMeasured }
```

Reads the measured geometry of a box. Pass a ref attached to a `<Box>`. `hasMeasured` is `false` until the first layout completes.

```jsx
function Sized() {
  const ref = useRef(null);
  const { width, height, hasMeasured } = useBoxMetrics(ref);
  return (
    <Box ref={ref} borderStyle="round" padding={1}>
      <Text>{hasMeasured ? `${width}x${height}` : 'measuring...'}</Text>
    </Box>
  );
}
```

## measureElement

```ts
measureElement(node): { width: number; height: number }
```

Measures a node imperatively. Layout runs after commit, so read measurements from an effect or an event handler, not during render.

```jsx
useEffect(() => {
  const { width, height } = measureElement(ref.current);
}, []);
```

## useAnimation

```ts
useAnimation(options?: { interval?: number; isActive?: boolean }): { frame: number; time: number; delta: number; reset(): void }
```

Drives frame-based animation. `frame` increments each tick, `time` is elapsed milliseconds, `delta` is the gap since the last frame, and `reset()` restarts the clock.

```jsx
function Spinner() {
  const frames = ['|', '/', '-', '\\'];
  const { frame } = useAnimation({ interval: 80 });
  return <Text>{frames[frame % frames.length]}</Text>;
}
```

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `options.interval` | `number` | `100` | Milliseconds between frames. |
| `options.isActive` | `boolean` | `true` | When `false`, the animation pauses. |

<Callout type="warn">
All animations in one app share a single timer, but timers are a bounded resource. Keep an eye on the [timer limits](/platform/limits) if you run many independent animations.
</Callout>

## useIsScreenReaderEnabled

```ts
useIsScreenReaderEnabled(): boolean
```

Always returns `false`. There is no screen reader over SSH. It exists for Ink compatibility.

---

Source: https://docs.voxal.sh/sdk/ui/hooks
