

## Overview [#overview]

Mode transitions are consumer-controlled. There is no internal routing or mode state — you swap the variant component using your own state and Cubitt renders accordingly.

## Basic Pattern [#basic-pattern]

```tsx
const [mode, setMode] = useState<"floating" | "sidebar">("floating");

// Shared props across variants
const chatProps = {
  conversationId,
  isStreaming,
  onCancelStreaming: handleCancel,
  onSend: handleSend,
  onNewConversation: handleNew,
};

// Mode switching handler
const handleModeChange = (newMode: "floating" | "sidebar" | "fullscreen") => {
  if (newMode === "floating") setMode("floating");
  else if (newMode === "sidebar") setMode("sidebar");
  else if (newMode === "fullscreen") {
    // Navigate to fullscreen route
    router.push("/chat/fullscreen");
  }
};

switch (mode) {
  case "sidebar":
    return (
      <Chat.Sidebar {...chatProps}>
        <Chat.Sidebar.Shell>
          <Chat.Sidebar.Header
            conversations={history}
            onConversationSelect={handleSelect}
            onNewConversation={handleNew}
            onModeChange={handleModeChange}
          />
          <Chat.Sidebar.Content>
            <Chat.MessageList>{messageRows}</Chat.MessageList>
            <MessageFeedScrollAnchor />
          </Chat.Sidebar.Content>
          <Chat.Sidebar.Footer
            files={fileConfig}
            instructions={instructionConfig}
          />
        </Chat.Sidebar.Shell>
      </Chat.Sidebar>
    );
  default: // "floating"
    return (
      <Chat.Floating {...chatProps} hasMessages={hasMessages}>
        <Chat.Floating.Shell>
          <Chat.Floating.Header
            conversations={history}
            onConversationSelect={handleSelect}
            onNewConversation={handleNew}
            onModeChange={handleModeChange}
          />
          <Chat.Floating.Content>
            <Chat.MessageList>{messageRows}</Chat.MessageList>
            <MessageFeedScrollAnchor />
          </Chat.Floating.Content>
          <Chat.Floating.Footer
            files={fileConfig}
            instructions={instructionConfig}
          />
        </Chat.Floating.Shell>
      </Chat.Floating>
    );
}
```

## State Preservation [#state-preservation]

Conversation state is preserved across transitions because:

1. **Same `conversationId`** — both variants receive the same ID, so your data queries return the same rows
2. **Same callbacks** — `onSend`, `onCancelStreaming`, `onNewConversation` are shared
3. **Reactive data** — your data store (e.g. TanStack DB live queries) picks up exactly where the previous variant left off

The `ChatProvider` is remounted on variant switch, but since it's stateless (all state lives in your data layer), there's no loss.

## Transition Triggers [#transition-triggers]

Each variant provides mode switching through header menus and callbacks:

| From       | Available Transitions | Trigger                       |
| ---------- | --------------------- | ----------------------------- |
| Floating   | Sidebar, Fullscreen   | `onModeChange` prop on Header |
| Sidebar    | Floating, Fullscreen  | `onModeChange` prop on Header |
| Fullscreen | Floating, Sidebar     | Consumer routes or navigation |

### Mode Switching Menu [#mode-switching-menu]

Headers now include a view menu (⋯) that shows available mode options:

**Floating**:

* Close button (X) - closes chat entirely
* View menu - shows "Sidebar" and "Fullscreen" options

**Sidebar**:

* Close button (X) - closes sidebar only, stays in sidebar mode
* View menu - shows "Floating" and "Fullscreen" options

### Floating → Sidebar/Fullscreen [#floating--sidebarfullscreen]

The floating header renders both a close button and a view menu. Wire mode switching via `onModeChange`:

```tsx
<Chat.Floating.Header
  conversations={history}
  onConversationSelect={handleSelect}
  onModeChange={(mode) => {
    if (mode === "sidebar") setMode("sidebar");
    else if (mode === "fullscreen") router.push("/chat/fullscreen");
  }}
/>
```

### Sidebar → Floating/Fullscreen [#sidebar--floatingfullscreen]

The sidebar header renders only a view menu (no close button). Handle mode switching via `onModeChange`:

```tsx
<Chat.Sidebar.Header
  conversations={history}
  onConversationSelect={handleSelect}
  onModeChange={(mode) => {
    if (mode === "floating") setMode("floating");
    else if (mode === "fullscreen") router.push("/chat/fullscreen");
  }}
/>
```

### To/from Fullscreen [#tofrom-fullscreen]

Fullscreen typically lives on its own route, so transitions are handled by your router:

```tsx
// Navigate to fullscreen
router.push("/chat/fullscreen");

// Navigate back
router.push("/chat");
```

## Behavior Changes [#behavior-changes]

### Close vs. Mode Switching [#close-vs-mode-switching]

The new system separates "closing the chat" from "switching between layout modes":

**Floating Chat**:

* Close button (X) - closes chat entirely, returns to collapsed state
* View menu (⋯) - switches to Sidebar or Fullscreen mode while preserving conversation

**Sidebar Chat**:

* Close button (X) - closes sidebar only, consumer can reopen with external toggle
* View menu (⋯) - switches to Floating or Fullscreen mode

### Backward Compatibility [#backward-compatibility]

The new `onModeChange` prop provides a unified interface for all mode transitions, replacing the previous approach where different variants had different callback props.

## Three-way Switching [#three-way-switching]

For apps that support all three modes:

```tsx
type ChatMode = "floating" | "sidebar" | "fullscreen";
const [mode, setMode] = useState<ChatMode>("floating");

const chatProps = {
  conversationId,
  isStreaming,
  onSend,
  onNewConversation,
  onCancelStreaming,
};

// Unified mode switching handler
const handleModeChange = (newMode: "floating" | "sidebar" | "fullscreen") => {
  if (newMode === "floating") setMode("floating");
  else if (newMode === "sidebar") setMode("sidebar");
  else if (newMode === "fullscreen") setMode("fullscreen");
};

switch (mode) {
  case "fullscreen":
    return (
      <Chat.Fullscreen {...chatProps}>
        <Chat.Fullscreen.Shell>
          <Chat.Fullscreen.Header title="Chat" />
          <Chat.Fullscreen.Content>
            <Chat.MessageList>{messageRows}</Chat.MessageList>
            <MessageFeedScrollAnchor />
          </Chat.Fullscreen.Content>
          <Chat.Fullscreen.Footer files={fileConfig} />
        </Chat.Fullscreen.Shell>
      </Chat.Fullscreen>
    );
  case "sidebar":
    return (
      <Chat.Sidebar {...chatProps}>
        <Chat.Sidebar.Shell>
          <Chat.Sidebar.Header
            conversations={history}
            onConversationSelect={handleSelect}
            onModeChange={handleModeChange}
          />
          <Chat.Sidebar.Content>
            <Chat.MessageList>{messageRows}</Chat.MessageList>
            <MessageFeedScrollAnchor />
          </Chat.Sidebar.Content>
          <Chat.Sidebar.Footer files={fileConfig} />
        </Chat.Sidebar.Shell>
      </Chat.Sidebar>
    );
  default:
    return (
      <Chat.Floating {...chatProps} hasMessages={hasMessages}>
        <Chat.Floating.Shell>
          <Chat.Floating.Header
            conversations={history}
            onConversationSelect={handleSelect}
            onModeChange={handleModeChange}
          />
          <Chat.Floating.Content>
            <Chat.MessageList>{messageRows}</Chat.MessageList>
            <MessageFeedScrollAnchor />
          </Chat.Floating.Content>
          <Chat.Floating.Footer files={fileConfig} />
        </Chat.Floating.Shell>
      </Chat.Floating>
    );
}
```
