{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "message-composer",
  "title": "MessageComposer",
  "description": "Two-line auto-growing conversation textarea with submit, stop, an attached submission-error support surface, and extensible tool, action, and error-recovery regions.",
  "type": "registry:ui",
  "meta": {
    "library": "@ds-mo/ui",
    "distribution": "npm",
    "storybook": "https://zainadeel.github.io/compomo/?path=/story/message-composer",
    "source": "src/wc/components/MessageComposer/MessageComposer.tsx",
    "intentStatus": "complete",
    "intent": {
      "audience": "general",
      "status": "experimental",
      "summary": "Two-line auto-growing conversation textarea with submit, stop, an attached submission-error support surface, and extensible tool, action, and error-recovery regions.",
      "useWhen": [
        "A conversation accepts multi-line text and needs Enter-to-submit with a streaming stop action."
      ],
      "avoidWhen": [
        "The text is a conventional form field submitted with unrelated fields."
      ],
      "commonlyComposedWith": [
        "component:ds-message-scroller",
        "component:ds-button-filled",
        "component:ds-button-unfilled"
      ],
      "patterns": [
        "pattern:conversation"
      ],
      "accessibility": [
        "Always provide a localized textarea label and submit or stop action names; the built-in icon action exposes the same name in its tooltip.",
        "Shift plus Enter inserts a newline while Enter submits non-empty content.",
        "Submission errors remain visible and announced above the editable box as part of one attached composer unit, and the editable control references their text while the error state is active.",
        "Streaming and error states remain perceivable without relying on icon shape or color alone."
      ],
      "states": [
        "The owner controls draft value and transport state; the composer emits intent without performing transport.",
        "Error status never clears or disables the draft; the application supplies recovery actions through error-actions and owns retry semantics. The negative support surface appears above and attached to the composer box, within the component's measured ownership.",
        "Use the tools slot for leading actions such as add and the actions slot for trailing actions such as dictation; the built-in submit or stop action remains last with an 8px gap from the preceding action. These icon buttons use the standard non-rounded control radius, and the application supplies tooltips for slotted icon actions.",
        "The composer surface uses the 10px --dimension-radius-125 token.",
        "Mouse or keyboard focus on the textarea gives the complete composer surface the same bold-brand inset stroke width and color as an active Input; the native textarea never draws a separate outline.",
        "The empty or otherwise inactive submit action is a disabled bordered ButtonUnfilled. Ready submit actions use submitIntent, while the streaming stop action remains a bold-brand ButtonFilled.",
        "The attached error unit keeps a real boundary in forced colors and lets message text and application-owned recovery actions wrap at narrow widths."
      ],
      "responsiveBehavior": [
        "The textarea begins at two body-medium lines, grows one line at a time through six lines, and then scrolls internally."
      ],
      "references": [
        {
          "label": "Storybook examples",
          "path": "src/wc/components/MessageComposer/MessageComposer.stories.ts"
        },
        {
          "label": "Scroller overlay composition",
          "path": "src/wc/components/MessageScroller/MessageScroller.stories.ts"
        }
      ]
    },
    "api": {
      "props": {
        "errorMessage": {
          "type": "string",
          "resolvedType": "string | undefined",
          "attribute": "error-message",
          "required": false,
          "mutable": false,
          "description": "Submission failure shown while status is error."
        },
        "isInactive": {
          "type": "boolean",
          "resolvedType": "boolean",
          "attribute": "is-inactive",
          "default": "false",
          "required": false,
          "mutable": false
        },
        "label": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "label",
          "default": "'Message'",
          "required": false,
          "mutable": false
        },
        "placeholder": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "placeholder",
          "default": "'Write a message'",
          "required": false,
          "mutable": false
        },
        "status": {
          "type": "MessageComposerStatus",
          "resolvedType": "\"error\" | \"ready\" | \"streaming\" | \"submitted\"",
          "attribute": "status",
          "default": "'ready'",
          "required": false,
          "mutable": false
        },
        "submitIntent": {
          "type": "ButtonFilledIntent",
          "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"guide\" | \"negative\" | \"neutral\" | \"positive\" | \"walkthrough\" | \"warning\"",
          "attribute": "submit-intent",
          "default": "'brand'",
          "required": false,
          "mutable": false
        },
        "value": {
          "type": "string",
          "resolvedType": "string",
          "attribute": "value",
          "default": "''",
          "required": false,
          "mutable": true
        }
      },
      "events": [
        {
          "name": "dsInput",
          "detail": "string",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        },
        {
          "name": "dsStop",
          "detail": "void",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        },
        {
          "name": "dsSubmit",
          "detail": "{ text: string }",
          "bubbles": true,
          "cancelable": true,
          "composed": true
        }
      ],
      "methods": [
        {
          "name": "setFocus",
          "signature": "setFocus() => Promise<void>"
        }
      ],
      "slots": []
    },
    "props": {
      "errorMessage": {
        "type": "string",
        "resolvedType": "string | undefined",
        "attribute": "error-message",
        "required": false,
        "mutable": false,
        "description": "Submission failure shown while status is error."
      },
      "isInactive": {
        "type": "boolean",
        "resolvedType": "boolean",
        "attribute": "is-inactive",
        "default": "false",
        "required": false,
        "mutable": false
      },
      "label": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "label",
        "default": "'Message'",
        "required": false,
        "mutable": false
      },
      "placeholder": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "placeholder",
        "default": "'Write a message'",
        "required": false,
        "mutable": false
      },
      "status": {
        "type": "MessageComposerStatus",
        "resolvedType": "\"error\" | \"ready\" | \"streaming\" | \"submitted\"",
        "attribute": "status",
        "default": "'ready'",
        "required": false,
        "mutable": false
      },
      "submitIntent": {
        "type": "ButtonFilledIntent",
        "resolvedType": "\"ai\" | \"brand\" | \"caution\" | \"guide\" | \"negative\" | \"neutral\" | \"positive\" | \"walkthrough\" | \"warning\"",
        "attribute": "submit-intent",
        "default": "'brand'",
        "required": false,
        "mutable": false
      },
      "value": {
        "type": "string",
        "resolvedType": "string",
        "attribute": "value",
        "default": "''",
        "required": false,
        "mutable": true
      }
    },
    "events": [
      {
        "name": "dsInput",
        "detail": "string",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      },
      {
        "name": "dsStop",
        "detail": "void",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      },
      {
        "name": "dsSubmit",
        "detail": "{ text: string }",
        "bubbles": true,
        "cancelable": true,
        "composed": true
      }
    ],
    "methods": [
      {
        "name": "setFocus",
        "signature": "setFocus() => Promise<void>"
      }
    ],
    "slots": [],
    "exports": {
      "customElement": "ds-message-composer",
      "react": "DsMessageComposer",
      "vue": "DsMessageComposer",
      "angular": "DsMessageComposer"
    },
    "consumption": {
      "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
      "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
      "customElements": {
        "import": "import '@ds-mo/ui/dist/components/ds-message-composer.js';",
        "example": "<ds-message-composer></ds-message-composer>"
      },
      "react": {
        "import": "import { DsMessageComposer } from '@ds-mo/ui/react';",
        "example": "<DsMessageComposer />"
      },
      "vue": {
        "import": "import { DsMessageComposer } from '@ds-mo/ui/vue';",
        "example": "<DsMessageComposer />"
      },
      "angular": {
        "import": "import { DsMessageComposer } from '@ds-mo/ui/angular/ds-message-composer';",
        "example": "<ds-message-composer></ds-message-composer>"
      },
      "peerDependencies": {
        "required": [
          "@ds-mo/tokens ^6.5.0",
          "@ds-mo/icons ^7.0.0"
        ],
        "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
      }
    }
  },
  "dependencies": [
    "@ds-mo/ui",
    "@ds-mo/tokens",
    "@ds-mo/icons"
  ],
  "registryDependencies": [
    "button-filled",
    "button-unfilled",
    "icon",
    "text",
    "tooltip"
  ],
  "files": [
    {
      "path": "src/wc/components/MessageComposer/MessageComposer.css",
      "content": "@import '../../utils/forced-colors.css';\n\n:host {\n  display: block;\n  width: 100%;\n  box-sizing: border-box;\n  -webkit-user-select: none;\n  user-select: none;\n}\n\n.message-composer {\n  display: flex;\n  flex-direction: column;\n  gap: var(--dimension-space-050);\n  width: 100%;\n  margin: 0;\n}\n\n.message-composer__attachments:empty,\n.message-composer__tools:empty,\n.message-composer__error-actions:empty {\n  display: none;\n}\n\n.message-composer__error-support,\n.message-composer__error-message,\n.message-composer__error-actions {\n  display: flex;\n  align-items: center;\n}\n\n.message-composer__error-support {\n  justify-content: space-between;\n  flex-wrap: wrap;\n  gap: var(--dimension-space-075) var(--dimension-space-100);\n  min-width: 0;\n  padding: var(--dimension-space-075) var(--dimension-space-100);\n  background: var(--color-background-faint-negative);\n}\n\n.message-composer__error-message {\n  flex: 1 1 var(--dimension-panel-width-xs);\n  gap: var(--dimension-space-050);\n  min-width: 0;\n  color: var(--color-foreground-bold-negative);\n}\n\n.message-composer__error-actions {\n  flex: 0 1 auto;\n  flex-wrap: wrap;\n  gap: var(--dimension-space-050);\n}\n\n.message-composer__stack {\n  min-width: 0;\n}\n\n.message-composer--error .message-composer__stack {\n  overflow: hidden;\n  border: var(--dimension-stroke-width-012) solid var(--color-border-bold-negative);\n  border-radius: var(--dimension-radius-125);\n  background: var(--color-background-primary);\n  box-shadow: var(--effect-elevation-elevated-md);\n}\n\n.message-composer__field {\n  display: flex;\n  flex-direction: column;\n  gap: var(--dimension-space-050);\n  padding: var(--dimension-space-100);\n  border-radius: var(--dimension-radius-125);\n  background: var(--color-background-primary);\n  box-shadow: var(--effect-elevation-elevated-md);\n  overflow: hidden;\n}\n\n.message-composer:not(.message-composer--error) .message-composer__field:focus-within {\n  box-shadow: var(--effect-elevation-elevated-md),\n    inset 0 0 0 var(--dimension-stroke-width-018) var(--color-border-bold-brand);\n}\n\n.message-composer--error .message-composer__field {\n  border-radius: 0;\n  box-shadow: none;\n}\n\ntextarea {\n  width: 100%;\n  min-height: calc(var(--typography-lineheight-md) * 2 + var(--dimension-space-150));\n  max-height: calc(var(--typography-lineheight-md) * 6 + var(--dimension-space-150));\n  padding: var(--dimension-space-075);\n  border: none;\n  outline: none;\n  resize: none;\n  overflow-y: auto;\n  box-sizing: border-box;\n  background: transparent;\n  color: var(--color-foreground-primary);\n  font-family: var(--typography-font-family-ui);\n  font-size: var(--typography-fontsize-md);\n  line-height: var(--typography-lineheight-md);\n  font-weight: var(--typography-weight-regular);\n  letter-spacing: var(--typography-letterspacing-negative-half);\n  -webkit-user-select: text;\n  user-select: text;\n}\n\ntextarea::placeholder {\n  color: var(--color-foreground-secondary);\n}\n\ntextarea:focus::placeholder {\n  color: var(--color-foreground-quaternary);\n}\n\ntextarea:focus,\ntextarea:focus-visible {\n  outline: none;\n}\n\n.message-composer__tools {\n  display: flex;\n  align-items: center;\n  gap: var(--dimension-space-050);\n  min-height: var(--dimension-size-400);\n}\n\n.message-composer__controls {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  gap: var(--dimension-space-050);\n  width: 100%;\n  min-width: 0;\n}\n\n.message-composer__actions {\n  display: flex;\n  align-items: center;\n  gap: var(--dimension-space-100);\n  flex: 0 0 auto;\n  margin-inline-start: auto;\n}\n\n@media (forced-colors: active) {\n  .message-composer__field {\n    border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-control-boundary);\n    box-shadow: none;\n  }\n\n  .message-composer--error .message-composer__field {\n    border: 0;\n  }\n\n  .message-composer--error .message-composer__stack {\n    border: var(--dimension-stroke-width-025) dashed var(--ds-forced-color-content);\n    box-shadow: none;\n  }\n\n  .message-composer__error-support {\n    border-block-end: var(--dimension-stroke-width-012) solid\n      var(--ds-forced-color-content);\n    background: var(--ds-forced-color-surface);\n  }\n}\n",
      "type": "registry:ui"
    },
    {
      "path": "src/wc/components/MessageComposer/MessageComposer.tsx",
      "content": "import {\n  Component,\n  Element,\n  Event,\n  EventEmitter,\n  h,\n  Host,\n  Method,\n  Prop,\n  Watch,\n} from '@stencil/core';\nimport type { ButtonFilledIntent } from '../ButtonFilled/ButtonFilled';\nimport type { MessageComposerStatus } from '../conversation-types';\n\nlet composerId = 0;\n\n@Component({\n  tag: 'ds-message-composer',\n  styleUrl: 'MessageComposer.css',\n  scoped: true,\n})\nexport class MessageComposer {\n  @Element() el!: HTMLElement;\n\n  @Prop({ mutable: true }) value: string = '';\n  @Prop() placeholder: string = 'Write a message';\n  @Prop() label: string = 'Message';\n  @Prop() status: MessageComposerStatus = 'ready';\n  /** Submission failure shown while status is error. */\n  @Prop() errorMessage?: string;\n  @Prop() isInactive: boolean = false;\n  @Prop() submitIntent: ButtonFilledIntent = 'brand';\n\n  @Event() dsInput!: EventEmitter<string>;\n  @Event() dsSubmit!: EventEmitter<{ text: string }>;\n  @Event() dsStop!: EventEmitter<void>;\n\n  private textarea?: HTMLTextAreaElement;\n  private readonly errorId = `ds-message-composer-error-${++composerId}`;\n\n  componentDidLoad() {\n    this.resize();\n  }\n\n  @Watch('value')\n  onValueChange() {\n    requestAnimationFrame(() => this.resize());\n  }\n\n  @Method()\n  async setFocus() {\n    this.textarea?.focus();\n  }\n\n  private get streaming(): boolean {\n    return this.status === 'submitted' || this.status === 'streaming';\n  }\n\n  private resize() {\n    if (!this.textarea) return;\n    this.textarea.style.height = 'auto';\n    this.textarea.style.height = `${this.textarea.scrollHeight}px`;\n  }\n\n  private handleInput = (event: Event) => {\n    this.value = (event.target as HTMLTextAreaElement).value;\n    this.resize();\n    this.dsInput.emit(this.value);\n  };\n\n  private submit() {\n    const text = this.value.trim();\n    if (!text || this.isInactive || this.streaming) return;\n    this.dsSubmit.emit({ text });\n  }\n\n  private handleKeyDown = (event: KeyboardEvent) => {\n    if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;\n    event.preventDefault();\n    this.submit();\n  };\n\n  private handleAction = () => {\n    if (this.streaming) this.dsStop.emit();\n    else this.submit();\n  };\n\n  render() {\n    const error = this.status === 'error';\n    const visibleErrorMessage = this.errorMessage || 'Message could not be sent.';\n    const actionInactive = !this.streaming && (this.isInactive || !this.value.trim());\n    return (\n      <Host>\n        <form\n          class={{ 'message-composer': true, 'message-composer--error': error }}\n          aria-busy={this.streaming ? 'true' : undefined}\n          onSubmit={(event: Event) => {\n            event.preventDefault();\n            this.submit();\n          }}\n        >\n          <div class=\"message-composer__attachments\">\n            <slot name=\"attachments\" />\n          </div>\n          <div class=\"message-composer__stack\">\n            {error ? (\n              <div class=\"message-composer__error-support\">\n                <div id={this.errorId} class=\"message-composer__error-message\" role=\"alert\">\n                  <ds-icon name=\"ErrorTriangle\" size=\"xs\" color=\"inherit\" />\n                  <ds-text variant=\"text-body-small\" color=\"negative\">\n                    {visibleErrorMessage}\n                  </ds-text>\n                </div>\n                <div class=\"message-composer__error-actions\">\n                  <slot name=\"error-actions\" />\n                </div>\n              </div>\n            ) : null}\n            <div class=\"message-composer__field\">\n              <textarea\n                ref={element => {\n                  this.textarea = element;\n                }}\n                value={this.value}\n                placeholder={this.placeholder}\n                aria-label={this.label}\n                aria-invalid={error ? 'true' : undefined}\n                aria-describedby={error ? this.errorId : undefined}\n                disabled={this.isInactive}\n                rows={2}\n                onInput={this.handleInput}\n                onKeyDown={this.handleKeyDown}\n              />\n              <div class=\"message-composer__controls\">\n                <div class=\"message-composer__tools\">\n                  <slot name=\"tools\" />\n                </div>\n                <div class=\"message-composer__actions\">\n                  <slot name=\"actions\" />\n                  {actionInactive ? (\n                    <ds-tooltip label=\"Send message\" side=\"top\" size=\"sm\">\n                      <ds-button-unfilled\n                        class=\"message-composer__action\"\n                        variant=\"icon\"\n                        icon=\"ArrowUp\"\n                        size=\"md\"\n                        aria-label=\"Send message\"\n                        hasBorder\n                        isInactive\n                        onDsClick={this.handleAction}\n                      />\n                    </ds-tooltip>\n                  ) : (\n                    <ds-tooltip\n                      label={this.streaming ? 'Stop response' : 'Send message'}\n                      side=\"top\"\n                      size=\"sm\"\n                    >\n                      <ds-button-filled\n                        class=\"message-composer__action\"\n                        variant=\"icon\"\n                        icon={this.streaming ? 'SquareFilled' : 'ArrowUp'}\n                        intent={this.streaming ? 'brand' : this.submitIntent}\n                        contrast=\"bold\"\n                        size=\"md\"\n                        aria-label={this.streaming ? 'Stop response' : 'Send message'}\n                        onDsClick={this.handleAction}\n                      />\n                    </ds-tooltip>\n                  )}\n                </div>\n              </div>\n            </div>\n          </div>\n        </form>\n      </Host>\n    );\n  }\n}\n",
      "type": "registry:ui"
    }
  ]
}
