<!-- source-hash: 14e8fee946105e4d0d83a53313cfd255 -->
Fetches the conversation timeline (notes, messages, and attachments) for a single HubSpot ticket, powering the ticket drawer's timeline view. Intentionally separate from the ticket list hook since engagements require expensive multi-stage API calls and are only loaded on demand.

## Key Components

### Interfaces

| Interface | Description |
|---|---|
| `TicketEngagementFile` | Attachment metadata: `id`, `name`, `url`, `mime`, `size` |
| `TicketEngagement` | A single timeline entry with body, author info (`authorRole`, `authorName`, `authorEmail`, `authorAvatarUrl`), timestamp, and attachments |
| `UseTicketEngagementsReturn` | Return shape: `engagements`, `isLoading`, `isFetching`, `error`, `refetch` |

### Hook

**`useTicketEngagements(externalTicketId, enabled?)`** — Queries `/api/chat/agent/list-engagements` via `embedAuthedFetch`. Skips execution for anonymous users, missing ticket IDs, and optimistic `temp-*` placeholders. Caching is intentionally disabled (`staleTime: 0`, `gcTime: 0`) so every drawer-open reflects live HubSpot state.

## Usage Example

```typescript
const { engagements, isLoading, error, refetch } = useTicketEngagements(ticket.externalId)

if (isLoading) return <Spinner />
if (error) return <ErrorBanner message={error.message} />

return (
  <Timeline>
    {engagements.map(e => (
      <TimelineEntry
        key={e.id}
        body={e.body}
        role={e.authorRole}   // 'customer' | 'support'
        author={e.authorName}
        avatarUrl={e.authorAvatarUrl}
        attachments={e.attachments}
      />
    ))}
  </Timeline>
)
```

> **Security note:** The server-side route enforces ticket ownership via `ticketBelongsToCustomer` before returning notes — customers cannot enumerate another customer's timeline by guessing ticket IDs.