# MessageComposer

> 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.

## Facts

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

## Guidelines

**Use it for** replying in a conversation on any channel. Emoji are plain UTF-8 in the text.

**What you provide**
- Text: `value` and `onChange`.
- Images: `attachments` (`ComposerAttachment[]`, each `{ id, file, previewUrl }`), `onAddFiles(files)` and `onRemoveAttachment(id)`.
- Sending: `onSubmit` (the parent sends and clears) and `isSending`.
- Optional: `takeoverNotice` (default false), which warns that sending stops Max's sequence for this contact; and `inputRef`.

**Anatomy**
- Staged images are `size-20 rounded-xl` thumbnails with a remove button on hover.
- The field is `rounded-2xl border-input bg-background/70 px-2 py-1 backdrop-blur-sm`. On focus-within it gains `border-primary/40 ring-2 ring-primary/20 shadow-lg shadow-primary/10` over 200ms.
- Round `size-9` attach and emoji buttons, then a one-row textarea capped at `max-h-32`.
- Send is a `size-9` circle: `bg-primary shadow-md shadow-primary/30` when there is something to send, muted otherwise. Below `sm` it becomes a labelled "Send" button.

**Behaviour**
- Enter sends and Shift+Enter adds a line. A pasted file is staged, and an image alone is sendable.
- The file picker accepts PNG, JPEG, GIF and WebP. The app's staging hook limits a message to 5 images and 4 MB in total.
- The send button rests at scale 0.94 while disabled and dips to 0.86 on tap. Reduced motion removes both.

**Don't** submit for the user, and don't hide the takeover notice while a Max sequence may still run.

## API

```ts
interface MessageComposerProps {
    value: string;
    onChange: (value: string) => void;
    /** Images staged for this message, newest last. */
    attachments: ComposerAttachment[];
    onAddFiles: (files: Iterable<File> | null | undefined) => void;
    onRemoveAttachment: (id: string) => void;
    /** Send: the parent owns the request and clears the composer. */
    onSubmit: () => void;
    isSending: boolean;
    /** Explain the campaign consequence before this irreversible human send. */
    takeoverNotice?: boolean;
    /**
     * The text area itself, handed back to the parent. A reply suggestion is
     * dropped into the composer by the parent, which then puts the caret here
     * so the user can edit it without reaching for the mouse.
     */
    inputRef?: RefObject<HTMLTextAreaElement | null>;
}
export declare function MessageComposer({ value, onChange, attachments, onAddFiles, onRemoveAttachment, onSubmit, isSending, takeoverNotice, inputRef, }: MessageComposerProps): JSX.Element;
```

## Example

```html
<div id="root" class="p-6"></div>
<script>
(function () {
  var h = React.createElement, DC = window.DigitalCrew, I = DC.Icons;
  var SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="160" height="160" viewBox="0 0 160 160">' +
    '<defs><linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#fff4ec"/><stop offset="1" stop-color="#ffd9c2"/></linearGradient></defs>' +
    '<rect width="160" height="160" fill="url(#g)"/>' +
    '<rect x="18" y="20" width="84" height="9" rx="4" fill="#3b2a22" opacity=".8"/>' +
    '<rect x="18" y="36" width="58" height="6" rx="3" fill="#3b2a22" opacity=".35"/>' +
    '<rect x="22" y="104" width="18" height="36" rx="3" fill="#f26b3a" opacity=".55"/>' +
    '<rect x="50" y="88" width="18" height="52" rx="3" fill="#f26b3a" opacity=".7"/>' +
    '<rect x="78" y="70" width="18" height="70" rx="3" fill="#f26b3a" opacity=".85"/>' +
    '<rect x="106" y="54" width="18" height="86" rx="3" fill="#f26b3a"/>' +
    '</svg>';
  var PREVIEW = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(SVG);
  function staged(name, url, size) {
    return { id: name, file: new File([new Uint8Array(size)], name, { type: "image/png" }), previewUrl: url };
  }
  var START_TEXT = "Thursday at 10 works on our side too. Sharing the Q1 rollout plan so Anouk can forward it.";
  function App() {
    var v = React.useState(START_TEXT), value = v[0], setValue = v[1];
    var a = React.useState(function () { return [staged("northwind-q1-rollout.png", PREVIEW, 184320)]; }), atts = a[0], setAtts = a[1];
    var s = React.useState(false), sending = s[0], setSending = s[1];
    return h("div", { className: "mx-auto max-w-[640px]" },
      h(DC.MessageComposer, {
        value: value, onChange: setValue, attachments: atts, isSending: sending, takeoverNotice: true,
        onAddFiles: function (files) {
          var list = Array.prototype.slice.call(files || []);
          setAtts(function (prev) {
            return prev.concat(list.map(function (f, i) { return { id: f.name + Date.now() + i, file: f, previewUrl: URL.createObjectURL(f) }; }));
          });
        },
        onRemoveAttachment: function (id) { setAtts(function (prev) { return prev.filter(function (x) { return x.id !== id; }); }); },
        onSubmit: function () {
          setSending(true);
          setTimeout(function () { setSending(false); setValue(""); setAtts([]); }, 900);
          setTimeout(function () { setValue(START_TEXT); setAtts([staged("northwind-q1-rollout.png", PREVIEW, 184320)]); }, 3200);
        }
      }));
  }
  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.
- [ChatListItem](/design/components/chat-list-item.md): 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.
- [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.
- [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.
