# Hooks

`<AvatarWidget>` is built on top of a set of hooks you can use directly. Skip the prebuilt UI when you need a custom look — your own avatar dock, your own toolbar, your own transcript view.

## `useLiveKitSession`

The session lifecycle hook. Connects to the agent's LiveKit room, manages the transcript, exposes audio/video tracks.

```ts title="signature.ts"
import { useLiveKitSession, type SessionOptions } from "@livelayer/react";

function useLiveKitSession(options: SessionOptions): UseLiveKitSessionResult;
```

### Options

```ts
interface SessionOptions {
  agentId: string;
  baseUrl?: string;
  apiKey?: string;
  sessionEndpoint?: string;
  sessionBody?: Record<string, unknown>;
  onDataMessage?: (msg: Record<string, unknown>) => void;
}
```

### Return value

```ts
interface UseLiveKitSessionResult {
  // State
  connectionState: ConnectionState;       // "idle" | "connecting" | "connected" | "disconnected" | "error"
  agentState: AgentState;                 // "idle" | "listening" | "thinking" | "speaking"
  transcript: TranscriptEntry[];          // [{ id, role, text, final }]
  agentConfig: AgentConfig | null;        // { name, avatarImageUrl, idleLoopUrl }
  videoElement: HTMLVideoElement | null;  // live agent video — attach to your own DOM
  audioElement: HTMLAudioElement | null;  // agent voice — attach somewhere mounted
  canResume: boolean;                     // session can resume within 5 minutes after disconnect
  error: string | null;

  // Actions
  connect(): Promise<void>;
  disconnect(): void;

  // Underlying access (for advanced cases)
  getRoom(): Room | null;                 // raw livekit-client Room
  session: LiveKitSession | null;         // raw SDK session
}
```

### Example: minimal "talk to agent" UI

```tsx title="custom-ui.tsx"
import { useLiveKitSession } from "@livelayer/react";

export function MyAgent() {
  const session = useLiveKitSession({ agentId: "agt_abc123" });

  if (session.connectionState === "idle") {
    return <button onClick={session.connect}>Talk to agent</button>;
  }
  if (session.connectionState === "connecting") {
    return <p>Connecting…</p>;
  }
  if (session.connectionState === "error") {
    return <p>Error: {session.error}</p>;
  }

  return (
    <div>
      <p>Agent state: {session.agentState}</p>
      <ul>
        {session.transcript.map((entry) => (
          <li key={entry.id}>
            <strong>{entry.role}:</strong> {entry.text}
          </li>
        ))}
      </ul>
      <button onClick={session.disconnect}>End</button>
    </div>
  );
}
```

> [!WARNING]
> `useLiveKitSession` does **not** publish the mic for you. If you're driving the session manually, also use `useMicrophoneState` and pass it the room from `session.getRoom()`.

## `useMicrophoneState`

Manages mic track lifecycle: publish, mute, unmute, switch device.

```ts
import { useMicrophoneState } from "@livelayer/react";

function MicButton({ room }: { room: Room | null }) {
  const mic = useMicrophoneState(room);

  return (
    <button
      onClick={() => (mic.isMuted ? mic.unmute() : mic.mute())}
      disabled={!mic.isReady}>
      {mic.isMuted ? "🎤 Unmute" : "🔇 Mute"}
    </button>
  );
}
```

Returns `{ isReady, isMuted, mute, unmute, switchDevice, error }`.

## `useAudioLevel`

Web-Audio analyser for visualizing mic input level. Runs at 60fps in a `requestAnimationFrame` loop without re-rendering React.

```tsx
import { useAudioLevel } from "@livelayer/react";

function MicLevelMeter({ room }: { room: Room | null }) {
  const ref = useRef<HTMLDivElement>(null);

  useAudioLevel(room, (level) => {
    // level: 0 to 1
    if (ref.current) ref.current.style.transform = `scale(${1 + level * 0.4})`;
  });

  return <div ref={ref} className="mic-pulse" />;
}
```

Pass a callback that receives a `0..1` level on every animation frame.

## `useCameraState` / `useScreenShareState`

Same shape as `useMicrophoneState`, but for camera and screen-share tracks. Most agents are voice-only — these are useful when building a video-conferencing-style flow.

## `useMediaDevices`

Enumerate available mics and cameras for a settings UI:

```tsx
import { useMediaDevices } from "@livelayer/react";

function DevicePicker() {
  const { microphones, cameras, refresh } = useMediaDevices();

  return (
    <select>
      {microphones.map((d) => (
        <option key={d.deviceId} value={d.deviceId}>{d.label}</option>
      ))}
    </select>
  );
}
```

## `useAgentInfo`

Fetch agent metadata (name, avatar, idle loop) from the server. Useful before connecting — show the agent's photo on a "Talk to me" button.

```ts
import { useAgentInfo } from "@livelayer/react";

function PreConnectCard({ agentId }: { agentId: string }) {
  const { agent, loading, error } = useAgentInfo(agentId);

  if (loading) return <p>Loading…</p>;
  if (error || !agent) return null;

  return (
    <button onClick={onConnect}>
      <img src={agent.avatarImageUrl} alt={agent.name} />
      Talk to {agent.name}
    </button>
  );
}
```

## `useTranscript`

Subscribes to transcript updates from a session. Returns the current array of entries.

```ts
import { useTranscript } from "@livelayer/react";

const transcript = useTranscript(session);
```

(Most consumers just read `session.transcript` from `useLiveKitSession` directly. `useTranscript` is exposed for cases where you have a session instance from elsewhere.)

## `useDisplayMode` / `useDisplayModePersistence`

Display state machine. `useDisplayMode` exposes `[mode, setMode]`; `useDisplayModePersistence` syncs to `localStorage`.

```ts
import { useDisplayMode, useDisplayModePersistence } from "@livelayer/react";

const [mode, setMode] = useDisplayMode("expanded");
useDisplayModePersistence(mode, setMode, "ll-widget-key");
```

## `useIsMobile`

Viewport width check (default breakpoint 640px).

```ts
import { useIsMobile } from "@livelayer/react";

const isMobile = useIsMobile(); // boolean
const isVerySmall = useIsMobile(480); // custom breakpoint
```

## `usePathname` / `useRouteMatch`

Pathname and route-matching helpers — fall back to `window.location` if no React Router or Next.js context is detected.

```ts
import { usePathname, useRouteMatch } from "@livelayer/react";

const pathname = usePathname();
const shouldRender = useRouteMatch(pathname, ["/app/**"], ["/app/admin/**"]);
```

## Putting it all together — a custom dock

```tsx title="custom-dock.tsx"
import {
  useLiveKitSession,
  useMicrophoneState,
  useAudioLevel,
  useAgentInfo,
} from "@livelayer/react";
import { useRef, useEffect } from "react";

export function CustomAgentDock({ agentId }: { agentId: string }) {
  const session = useLiveKitSession({ agentId });
  const room = session.getRoom();
  const mic = useMicrophoneState(room);
  const { agent } = useAgentInfo(agentId);
  const videoRef = useRef<HTMLDivElement>(null);
  const levelRef = useRef<HTMLDivElement>(null);

  useAudioLevel(room, (level) => {
    if (levelRef.current) {
      levelRef.current.style.opacity = String(0.3 + level * 0.7);
    }
  });

  useEffect(() => {
    if (session.videoElement && videoRef.current) {
      videoRef.current.appendChild(session.videoElement);
    }
  }, [session.videoElement]);

  if (!agent) return null;

  return (
    <div className="my-agent-dock">
      <div ref={videoRef} className="agent-video" />
      <div ref={levelRef} className="mic-pulse" />
      <button onClick={mic.isMuted ? mic.unmute : mic.mute}>
        {mic.isMuted ? "🎤" : "🔇"}
      </button>
      <button onClick={session.disconnect}>End</button>
    </div>
  );
}
```

You're now driving the whole stack — session, mic, audio level — from your own component. The widget package becomes a library, not a layout.

## Read next

- [Controlled session](/docs/develop/npm/react/controlled-session) — wrap these hooks into a context provider
- [TypeScript types](/docs/develop/npm/types) — reference for `AgentState`, `ConnectionState`, etc.
- [`<livelayer-widget>` web component](/docs/develop/npm/sdk/livelayer-widget) — the same SDK, framework-agnostic
