# ChatListItem

> One conversation in the Unibox list: a motion row that staggers in, glides when a new reply reorders the inbox, and pops its unread badge.

## Facts

- **Product**: Max
- **Family**: Unibox
- **Global**: `window.DigitalCrew.ChatListItem`
- **Source**: `max-agent: src/features/unibox/ui/components/chat-list-item.tsx`
- **Import in the app**: `import { ChatListItem } from "@/features/unibox/ui/components/chat-list-item";`
- **Live preview**: /design/components/chat-list-item/preview.html
- **Page**: /design/components/chat-list-item

## Guidelines

**Use it for** conversation lists (email, LinkedIn, WhatsApp) where recency and "your turn" matter.

**What you provide**
- Required: `chat` (`ChatWithProspect`: the `chats` row plus `prospect`, `account` and optional `campaign`), `index` (entrance order), `isActive`, `selectionMode`, `isChecked`, `unreadCount` and `onSelect`.
- Optional: `onViewProfile(prospectId)`, which adds a button to the avatar's hover card; `layoutDependency` (pass the list); and `ref`.

**Anatomy**
- The row is a button with `rounded-xl px-2.5 py-2.5` and `hover:bg-accent/70`. Active adds `bg-primary/[0.07] ring-1 ring-primary/15`.
- A 36px [`ProspectAvatar`](/design/components/prospect-avatar.md) with an unread halo; its hover card opens after 350ms.
- The name is `font-medium` (`font-semibold` when unread), followed by [`ChannelBadge`](/design/components/channel-badge.md), a campaign megaphone and a shared or private icon.
- Time is `text-[11px] tabular-nums`: "h:mm a" today, "Yesterday", then "MM/dd/yyyy". Unread times are orange only in dark mode, because orange fails AA at 11px on light.
- `reply_state: "awaiting_us"` adds an `emerald-600` reply icon: they replied, so it's your turn.
- The unread count is a `bg-primary text-[10px] font-semibold` pill.

**Motion**
- Entrance starts from `y: 14`, scale 0.98 and 4px blur, on `SPRING_ENTER` plus `staggerDelay(index)`. Reordering uses `SPRING_GLIDE`.
- The badge pops on `SPRING_POP`, and a 1.3s primary wash crosses the row when a new message lands.
- Reduced motion removes all of it.

**Don't** pass a new `layoutDependency` on every render, and don't nest buttons in the row.

## API

```ts
interface ChatListItemProps {
    chat: ChatWithProspect;
    index: number;
    isActive: boolean;
    selectionMode: boolean;
    isChecked: boolean;
    unreadCount: number;
    onSelect: () => void;
    onViewProfile?: (prospectId: string) => void;
    /**
     * Layout re-measurement gate: pass the list identity so motion only
     * re-measures rows when the list actually changes, not on every parent
     * re-render (e.g. each search-box keystroke).
     */
    layoutDependency?: unknown;
    /** AnimatePresence popLayout measures the row through this ref. */
    ref?: React.Ref<HTMLButtonElement>;
}
export declare function ChatListItem({ chat, index, isActive, selectionMode, isChecked, unreadCount, onSelect, onViewProfile, layoutDependency, ref, }: ChatListItemProps): JSX.Element;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  var MIN = 60 * 1000, DAY = 24 * 60 * MIN;
  function ago(ms) { return new Date(Date.now() - ms).toISOString(); }
  function chat(o) {
    return {
      id: o.id, account_id: "5a1c2e10-0000-4000-8000-000000000001", prospect_id: o.id + "-p",
      workflow_execution_state_id: null, channel: o.channel, chat_id: "prov-" + o.id, title: null,
      is_group: false, is_archived: false, unread_count: o.unread || 0,
      last_message_at: o.at, last_message_preview: o.preview, metadata: {},
      share_override: o.share || "inherit", reply_state: o.reply || null,
      workspace_id: "9d7e0000-0000-4000-8000-000000000001", created_at: ago(30 * DAY), updated_at: o.at,
      prospect: { id: o.id + "-p", full_name: o.name, first_name: null, last_name: null, email: null, linkedin_url: null, photo_url: null },
      account: { id: "5a1c2e10-0000-4000-8000-000000000001", name: "Mathieu Laurent", email: "mathieu@digitalcrew.ai" },
      campaign: o.campaign ? { id: "c0ffee00-0000-4000-8000-000000000001", name: o.campaign } : null
    };
  }
  var CHATS = [
    chat({ id: "c1", name: "Claire Dubois", channel: "linkedin", unread: 2, reply: "awaiting_us", campaign: "Q4 CFO outreach", at: ago(6 * MIN), preview: "Thursday at 10 works. Can you send the invite to my assistant?" }),
    chat({ id: "c2", name: "Léa Martin", channel: "whatsapp", share: "shared", reply: "awaiting_them", at: ago(48 * MIN), preview: "You: Perfect, I'll share the deck tonight." }),
    chat({ id: "c3", name: "Marcus Reinholt", channel: "email", unread: 1, reply: "awaiting_us", at: ago(2 * 60 * MIN), preview: "Re: Pipeline review — attached the numbers we discussed" }),
    chat({ id: "c4", name: "Tomás Alvarez", channel: "email", reply: "awaiting_them", at: ago(DAY + 3 * 60 * MIN), preview: "You: Following up on the Brightline pilot proposal" }),
    chat({ id: "c5", name: "Priya Raman", channel: "linkedin", share: "private", reply: "awaiting_them", at: ago(6 * DAY), preview: "You: Great meeting you at SaaStr — worth a quick chat?" })
  ];
  function App() {
    var st = React.useState("c2"), active = st[0], setActive = st[1];
    var un = React.useState({ c1: 2, c3: 1 }), unread = un[0], setUnread = un[1];
    return h("div", { className: "mx-auto flex w-[360px] flex-col gap-0.5 rounded-2xl border border-border/60 bg-card p-2 shadow-sm" },
      CHATS.map(function (c, i) {
        return h(DC.ChatListItem, {
          key: c.id, chat: c, index: i, isActive: active === c.id, selectionMode: false, isChecked: false,
          unreadCount: unread[c.id] || 0, layoutDependency: CHATS,
          onSelect: function () {
            setActive(c.id);
            setUnread(function (u) { var n = Object.assign({}, u); delete n[c.id]; return n; });
          }
        });
      }));
  }
  ReactDOM.createRoot(document.getElementById("root")).render(h(DC.TooltipProvider, { delayDuration: 80 }, h(App)));
})();
</script>
```

## More in Unibox

- [ChannelSegments](/design/components/channel-segments.md): The Unibox channel filter: a rail of icons where one shared pill glides to the selected channel, and only that segment spells out its name.
- [MessageBubble](/design/components/message-bubble.md): One message in a Unibox thread: your side in the brand gradient, theirs in muted glass, each rising in from its own side of the conversation.
- [MessageComposer](/design/components/message-composer.md): The Unibox reply box: text, emoji and staged images in one rounded field, with a send button that springs awake when there is something to send.
- [ReplySuggestions](/design/components/reply-suggestions.md): The band between a Unibox thread and its composer: ask for a few replies, watch them arrive, pick one.
- [SyncProgressPanel](/design/components/sync-progress-panel.md): The dialog behind the Unibox sync button.
- [UniboxIdlePanel](/design/components/unibox-idle-panel.md): The Unibox's empty conversation pane: a breathing glass orb with the three channels in slow orbit and a quiet "Listening for new replies" line.
