<!-- source-hash: 8a7542421be9aaeac5211827f8967375 -->
Expanded drawer component for a single ticket's detail view, rendering the conversation timeline, metadata, and status-dependent actions (reply/close/reopen).

## Key Components

### `TicketDetailDrawer` (main export)
The root drawer component that composes the full expanded ticket view:
- Renders optional linked ClickUp delivery card
- Delegates conversation rendering to `TicketTimelinePanel`
- Conditionally renders `OpenActions` or `ReopenAction` based on ticket status

### `TicketTimelinePanel`
Internal component that builds a chronological conversation thread:
- Splits legacy `ticket.body` on ` --- ` separators for historical compatibility
- Fetches Note engagements via `useTicketEngagements`
- Resolves customer identity with a 4-level precedence chain (live identity → mirror name → email → "You")
- Shows skeleton loader during initial fetch; `EmptyState` when no content exists

### `TicketRef`
Minimal identity bundle (`id` + `external_id`) threaded through mutation callbacks — `external_id` targets HubSpot, `id` drives React/TanStack cache keys.

## Usage Example

```typescript
<TicketDetailDrawer
  ticket={ticket}
  busy={isMutating}
  supportSystemDown={false}
  onSendMessage={async (ref, text, attachments) => {
    await sendNoteEngagement(ref.external_id, text, attachments)
    return true
  }}
  onClose={async (ref, resolution) => {
    await closeTicket(ref.external_id, resolution)
    return true
  }}
  onReopen={async (ref) => {
    await reopenTicket(ref.external_id)
    return true
  }}
  onActionCollapsed={() => setDrawerOpen(false)}
/>
```

## Notes
- All local state (composer text, attachments, close-confirm dialog) is owned here; parent owns ticket data and mutation callbacks
- Optimistic tickets (pre-server-confirmation) skip the engagement fetch until a real `external_id` is available
- The timeline intentionally has no inner scroll — content flows with the page to avoid competing scroll surfaces