# Hooks — Complete Reference

All imports use subpath exports: `@loke/ui/<hook-name>`.

---

## useControllableState

**Import:** `@loke/ui/use-controllable-state`

**File:** `src/hooks/use-controllable-state/controllable-state.tsx`

### Signature

```tsx
function useControllableState<T>(params: {
  prop?: T | undefined;       // controlled value; undefined = uncontrolled
  defaultProp: T;             // initial value for uncontrolled mode
  onChange?: (state: T) => void; // fires on every value change in either mode
  caller?: string;            // component name for dev warning messages
}): [T, Dispatch<SetStateAction<T>>]
```

### Behavior

- When `prop` is `undefined`: internal `useState` manages state. `setX` updates it and calls `onChange`.
- When `prop` is defined: no internal state. `setX` calls `onChange(nextValue)` only if value differs. Parent must update `prop` for re-render.
- In development, warns (via `console.warn`) if `prop` transitions between `undefined` and a value after mount.
- The returned setter accepts both `T` and `(prev: T) => T` (same as React's `setState`).

### Notes

`onChange` is stored in a ref via `useInsertionEffect` so it is always the latest version without being a dependency of the setter's `useCallback`.

---

## useCallbackRef

**Import:** `@loke/ui/use-callback-ref`

**File:** `src/hooks/use-callback-ref/callback-ref.ts`

### Signature

```tsx
function useCallbackRef<T extends (...args: any[]) => any>(
  callback: T | undefined,
): T
```

### Behavior

Returns a stable function that always delegates to the latest `callback`. The stable function is created once via `useMemo([], [])`. The ref is updated after every render via `useEffect` (no deps array), so it always holds the latest value.

### When to use

- Passing a callback prop to `useEffect` without adding it to the deps array.
- Storing event handlers in long-lived data structures (maps, closures) that should not re-create on each render.
- Any scenario where you need a stable identity but always-fresh behavior.

### When NOT to use

Do not use `useCallbackRef` to memoize expensive computations — it calls the latest callback every invocation. For computation memoization, use `useMemo`.

---

## useDirection / DirectionProvider

**Import:** `@loke/ui/use-direction`

**File:** `src/hooks/use-direction/direction.tsx`

### Signature

```tsx
function useDirection(localDir?: "ltr" | "rtl"): "ltr" | "rtl"

interface DirectionProviderProps {
  dir: "ltr" | "rtl";
  children?: React.ReactNode;
}
const DirectionProvider: FC<DirectionProviderProps>
```

### Behavior

`useDirection` resolves direction in priority order:
1. `localDir` argument (component-level override)
2. Nearest `DirectionProvider` in the tree
3. `"ltr"` (hardcoded fallback)

`DirectionProvider` uses a standard `React.createContext` (not the library's `createContext`) — it does not throw on missing provider.

### Usage notes

- Set at the application root based on `document.documentElement.dir` or a user preference.
- Popper reads direction from `dir` prop on `PopperContent` for Floating UI's logical alignment calculations — always pass `dir` through to portaled content.
- RTL affects Popper's `start`/`end` alignment interpretation.

---

## useEscapeKeydown

**Import:** `@loke/ui/use-escape-keydown`

**File:** `src/hooks/use-escape-keydown/escape-keydown.tsx`

### Signature

```tsx
function useEscapeKeydown(
  onEscapeKeyDown?: (event: KeyboardEvent) => void,
  ownerDocument?: Document,  // default: globalThis.document
): void
```

### Behavior

Registers a `keydown` listener on `ownerDocument` in **capture phase** (`{ capture: true }`). Fires the callback when `event.key === "Escape"`. Uses `useCallbackRef` internally so the callback is always fresh without re-registering the listener.

### Notes

- Capture phase ensures Escape is caught before bubbling. DismissableLayer uses this directly and checks whether the layer is the topmost before calling `onDismiss`.
- Pass `ownerDocument` when the component renders in an iframe or non-standard document.
- `event.preventDefault()` inside the callback can prevent other Escape handlers from firing (since capture happens first).

---

## useId

**Import:** `@loke/ui/use-id`

**File:** `src/hooks/use-id/id.tsx`

### Signature

```tsx
function useId(deterministicId?: string): string
```

### Behavior

| React version | ID format | Mechanism |
|---|---|---|
| 18+ | `"loke-:r0:"` | Delegates to `React.useId` |
| 16/17 | `"loke-0"` | Module-level counter, set in `useLayoutEffect` |
| SSR (any) | `""` (empty) until hydration | Counter not incremented server-side |

When `deterministicId` is provided, it is returned directly with no internal ID generated.

### Notes

- IDs are stable across re-renders in all React versions.
- On React 16/17, IDs are `""` during SSR and the first render, then populated after `useLayoutEffect`. Guard `aria-*` attributes that require a non-empty ID.
- Prefix `"loke-"` is always prepended to generated IDs to namespace them from user-defined IDs.

---

## useSize

**Import:** `@loke/ui/use-size`

**File:** `src/hooks/use-size/size.tsx`

### Signature

```tsx
function useSize(
  element: HTMLElement | null,
): { width: number; height: number } | undefined
```

### Behavior

- Returns `undefined` before the element mounts (no element reference yet).
- Returns `{ width, height }` immediately on mount using `offsetWidth`/`offsetHeight` (synchronous baseline).
- Creates a `ResizeObserver` with `box: "border-box"`. Subsequent updates use `borderBoxSize` when available, falling back to `offsetWidth`/`offsetHeight`.
- When `element` becomes `null`, returns `undefined`.
- Observer is cleaned up (`unobserve`) when `element` changes or the component unmounts.

### Notes

- Uses SSR-safe `useLayoutEffect` internally, so no server-side errors.
- Popper uses this for arrow size measurement to account for arrow height in `sideOffset`.
- Suitable for measuring content that needs to adapt to container dimensions.

---

## usePrevious

**Import:** `@loke/ui/use-previous`

**File:** `src/hooks/use-previous/previous.tsx`

### Signature

```tsx
function usePrevious<T>(value: T): T
```

### Behavior

Returns the value from the previous render. On the first render, returns the initial value (there is no prior render value). Updates only when `value` changes (compared with `Object.is`).

Implemented via `useRef` + `useMemo` to avoid an extra render cycle that `useState`-based implementations would require.

### Notes

- The previous value is the value from the render before the current one, not from a previous state update within the same render cycle.
- Use for detecting direction of change (e.g., previous tab index to determine slide direction).

---

## useLayoutEffect (SSR-safe)

**Import:** `@loke/ui/use-layout-effect`

**File:** `src/hooks/use-layout-effect/layout-effect.tsx`

### Signature

```tsx
const useLayoutEffect: typeof React.useLayoutEffect
```

### Behavior

- On the client (`globalThis.document` is defined): identical to `React.useLayoutEffect`. Runs synchronously after DOM mutations, before paint.
- On the server: no-op function. No warning emitted.

### When to use vs useIsHydrated

| Need | Use |
|---|---|
| DOM measurement/mutation after render | `useLayoutEffect` |
| Preventing render of browser-only JSX (portals, `document.*`) | `useIsHydrated` |
| Delaying a side effect until client | `useLayoutEffect` |
| Showing different markup server vs client | `useIsHydrated` |

---

## useIsHydrated

**Import:** `@loke/ui/use-is-hydrated`

**File:** `src/hooks/use-is-hydrated/is-hydrated.ts`

### Signature

```tsx
function useIsHydrated(): boolean
```

### Behavior

Uses `useSyncExternalStore` with:
- Server snapshot: `() => false`
- Client snapshot: `() => true`
- Subscribe: no-op (state never changes after hydration)

Returns `false` during SSR and on the initial client render before hydration completes. Returns `true` on all subsequent renders.

### Notes

- Safe to use in components rendered on the server — no hydration mismatch because `useSyncExternalStore` coordinates the server/client transition.
- The hook's state never changes after the first `true` — no re-subscription or re-render from the external store.
- Do NOT use to gate `useEffect` or `useLayoutEffect` — those hooks are already client-only. Use `useLayoutEffect` from `@loke/ui/use-layout-effect` instead.

---

## useIsDocumentHidden

**Import:** `@loke/ui/use-is-document-hidden`

**File:** `src/hooks/use-is-document-hidden/is-document-hidden.ts`

### Signature

```tsx
function useIsDocumentHidden(): boolean
```

### Behavior

Returns `document.hidden` and subscribes to `visibilitychange` events. Updates state whenever the user switches tabs, minimizes the browser, or the OS hides the window.

### Notes

- Initializes with `document.hidden` directly — will throw on SSR if called without guarding. Wrap with `useIsHydrated` check or render only on the client.
- Useful for pausing timers, animations, or polling when the tab is not visible.
- Tooltip uses this (via `useIsDocumentHidden`) to close open tooltips when the document is hidden, preventing tooltips from remaining visible when the user returns to the tab.
