# Inbox Messenger Layout

> Two-pane chat messenger with a selectable conversation list beside the active thread, each conversation keeping its own messages, plus a composer.

A shadcn/ui block from the [@7ovr](https://7ovr.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/7ovr/chat-2).

- Page, with a live preview: https://shadcn.exchange/r/7ovr/chat-2
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/7ovr/chat-2-light-1de722315a8857ba.png
- Registry item: https://7ovr.com/r/chat-2.json
- Categories: chat
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://7ovr.com/r/chat-2.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@base-ui/react`
- Registry items: `avatar`, `bubble`, `button`, `input`, `message`, `message-scroller`

## Files

### `registry/blocks/chat/2/chat-block.tsx`

```tsx
"use client"

import * as React from "react"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Message, MessageContent } from "@/components/ui/message"
import {
  MessageScroller,
  MessageScrollerButton,
  MessageScrollerContent,
  MessageScrollerItem,
  MessageScrollerProvider,
  MessageScrollerViewport,
} from "@/components/ui/message-scroller"
import { cn } from "@/lib/utils"
import { IconPlaceholder } from "@/components/icons/icon-placeholder"

type ChatMessage = { id: number; from: "me" | "them"; text: string }

type Conversation = {
  id: string
  name: string
  avatar: string
  preview: string
  time: string
}

const conversations: Conversation[] = [
  {
    id: "mara",
    name: "Mara Lin",
    avatar: "https://i.pravatar.cc/80?img=32",
    preview: "Anything you would change before we ship?",
    time: "9:43 AM",
  },
  {
    id: "leo",
    name: "Leo Tanaka",
    avatar: "https://i.pravatar.cc/80?img=12",
    preview: "The API docs are live now.",
    time: "Tue",
  },
  {
    id: "owen",
    name: "Owen Reyes",
    avatar: "https://i.pravatar.cc/80?img=53",
    preview: "Thanks for the quick review!",
    time: "Mon",
  },
]

const seed: Record<string, ChatMessage[]> = {
  mara: [
    { id: 1, from: "them", text: "Did you look at the new onboarding flow?" },
    { id: 2, from: "me", text: "Just did. The empty states are a nice touch." },
    { id: 3, from: "them", text: "Anything you would change before we ship?" },
  ],
  leo: [
    { id: 1, from: "them", text: "Pushed the v2 API reference." },
    { id: 2, from: "them", text: "The API docs are live now." },
  ],
  owen: [
    { id: 1, from: "me", text: "Left a few notes on your PR." },
    { id: 2, from: "them", text: "Thanks for the quick review!" },
  ],
}

export default function ChatBlock() {
  const [threads, setThreads] = React.useState(seed)
  const [activeId, setActiveId] = React.useState(conversations[0].id)
  const [draft, setDraft] = React.useState("")
  const nextId = React.useRef(1000)

  const active =
    conversations.find((c) => c.id === activeId) ?? conversations[0]
  const messages = threads[activeId]

  const send = (e: React.FormEvent) => {
    e.preventDefault()
    const text = draft.trim()
    if (!text) return
    const id = nextId.current++
    setThreads((prev) => ({
      ...prev,
      [activeId]: [...prev[activeId], { id, from: "me", text }],
    }))
    setDraft("")
  }

  return (
    <section className="flex w-full items-center justify-center bg-muted/30 px-6 py-16 text-foreground">
      <div className="grid h-[560px] w-full max-w-3xl grid-cols-1 overflow-hidden rounded-lg border border-border bg-background sm:grid-cols-[240px_1fr]">
        <div className="hidden flex-col border-r border-border sm:flex">
          <div className="border-b border-border p-4">
            <h2 className="font-heading text-sm font-semibold">Messages</h2>
          </div>
          <ul className="flex flex-1 flex-col overflow-y-auto">
            {conversations.map((conversation) => (
              <li key={conversation.id}>
                <button
                  type="button"
                  onClick={() => {
                    setActiveId(conversation.id)
                    setDraft("")
                  }}
                  aria-pressed={conversation.id === activeId}
                  className={cn(
                    "flex w-full items-center gap-3 border-b border-border p-3 text-left transition-colors",
                    conversation.id === activeId
                      ? "bg-muted"
                      : "hover:bg-muted/50"
                  )}
                >
                  <Avatar className="size-9 shrink-0">
                    <AvatarImage
                      src={conversation.avatar}
                      alt={conversation.name}
                      className="grayscale"
                    />
                    <AvatarFallback>{conversation.name[0]}</AvatarFallback>
                  </Avatar>
                  <div className="flex min-w-0 flex-1 flex-col">
                    <div className="flex items-center justify-between gap-2">
                      <span className="truncate text-sm font-medium">
                        {conversation.name}
                      </span>
                      <span className="shrink-0 text-[11px] text-muted-foreground">
                        {conversation.time}
                      </span>
                    </div>
                    <span className="truncate text-xs text-muted-foreground">
                      {conversation.preview}
                    </span>
                  </div>
                </button>
              </li>
            ))}
          </ul>
        </div>

        <div className="flex min-w-0 flex-col">
          <MessageScrollerProvider
            key={activeId}
            autoScroll
            defaultScrollPosition="end"
          >
            <div className="flex items-center gap-3 border-b border-border p-4">
              <Avatar className="size-8">
                <AvatarImage
                  src={active.avatar}
                  alt={active.name}
                  className="grayscale"
                />
                <AvatarFallback>{active.name[0]}</AvatarFallback>
              </Avatar>
              <span className="text-sm font-semibold">{active.name}</span>
            </div>

            <MessageScroller className="flex-1">
              <MessageScrollerViewport>
                <MessageScrollerContent className="gap-4 p-4">
                  {messages.map((message) => (
                    <MessageScrollerItem
                      key={message.id}
                      messageId={String(message.id)}
                      scrollAnchor={message.from === "me"}
                    >
                      <Message align={message.from === "me" ? "end" : "start"}>
                        <MessageContent>
                          <Bubble
                            variant={
                              message.from === "me" ? "default" : "muted"
                            }
                          >
                            <BubbleContent>{message.text}</BubbleContent>
                          </Bubble>
                        </MessageContent>
                      </Message>
                    </MessageScrollerItem>
                  ))}
                </MessageScrollerContent>
              </MessageScrollerViewport>
              <MessageScrollerButton />
            </MessageScroller>

            <form
              onSubmit={send}
              className="flex items-center gap-2 border-t border-border p-3"
            >
              <Input
                value={draft}
                onChange={(e) => setDraft(e.target.value)}
                placeholder="Type a message..."
                aria-label="Message"
              />
              <Button
                type="submit"
                size="icon"
                disabled={!draft.trim()}
                aria-label="Send message"
              >
                <IconPlaceholder
                  lucide="Send"
                  tabler="IconSend"
                  hugeicons="SentIcon"
                  phosphor="PaperPlane"
                  remixicon="RiSendPlane2Fill"
                  className="size-4"
                  aria-hidden="true"
                />
              </Button>
            </form>
          </MessageScrollerProvider>
        </div>
      </div>
    </section>
  )
}
```
