{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"composer","type":"registry:ui","title":"Composer","description":"The input at the bottom of a chat, taken apart: a root that owns the draft and eight parts that read it — a growing textarea, a send button that becomes a stop button, a toolbar, a model picker, an attachment row with its chips, and a completion menu you can mount twice on the same draft for / commands and @ mentions","author":"fancy-ui <https://fancy-ui.rama.app>","categories":["ai-chat"],"registryDependencies":["https://fancy-ui.rama.app/r/internals.json","https://fancy-ui.rama.app/r/internals-dom.json","https://fancy-ui.rama.app/r/sound.json","https://fancy-ui.rama.app/r/utils.json"],"files":[{"path":"react/src/components/composer/Composer.tsx","content":"\"use client\";\n\nimport { forwardRef, useRef, useState } from \"react\";\nimport type { FormEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useConstant, useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport type { AttachmentData } from \"../../internals/ai-types\";\nimport { findTokenStart } from \"./caret\";\nimport { ComposerInput } from \"./ComposerInput\";\nimport { ComposerSubmit } from \"./ComposerSubmit\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport type { ComposerContext } from \"./types\";\nimport \"./composer.css\";\n\n/**\n * Props for Composer\n */\nexport interface ComposerProps {\n\t/**\n\t * The draft text. Controlled when passed — report every write through\n\t * `onValueChange`; leave it out and the composer keeps its own copy.\n\t * Cleared by a successful submit.\n\t */\n\tvalue?: string;\n\t/** Called with the draft on every write the composer makes: typing, an insertion, a clear. */\n\tonValueChange?: (value: string) => void;\n\t/**\n\t * Files riding along with the draft. Controlled when passed — the consumer\n\t * owns uploading them and reports removals back through\n\t * `onAttachmentsChange`.\n\t */\n\tattachments?: AttachmentData[];\n\t/** Called with the remaining attachments whenever the composer drops one. */\n\tonAttachmentsChange?: (attachments: AttachmentData[]) => void;\n\t/** Blocks typing, sending, and attaching. */\n\tdisabled?: boolean;\n\t/** A response is arriving: the send button becomes a stop button. */\n\tstreaming?: boolean;\n\t/** Placeholder for the default input. Ignored once `children` replaces the composition. */\n\tplaceholder?: string;\n\t/** Called with the trimmed draft and a snapshot of the attachments. */\n\tonSubmit?: (payload: { text: string; attachments: AttachmentData[] }) => void;\n\t/** Called when the stop button is pressed while streaming. */\n\tonStop?: () => void;\n\t/** Called with the files handed to `addFiles`. Upload them, then push onto `attachments`. */\n\tonAttach?: (files: File[]) => void;\n\t/** Replaces the default input-and-send-row composition entirely. */\n\tchildren?: ReactNode;\n\t/** An overlay covering the composer — a voice panel, a drop target, a confirmation. */\n\taccessory?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n\t/**\n\t * Plays the matching interface cue through the sound controller. Off by\n\t * default; only audible once the user has enabled sound.\n\t */\n\tsound?: boolean;\n}\n\n/** Shared empty list, so the uncontrolled seed is one allocation for the module. */\nconst NO_ATTACHMENTS: AttachmentData[] = [];\n\n/**\n * The root of the composer compound: it owns the draft and publishes it.\n *\n * The form element arrives through the ref channel rather than a `ref` prop,\n * per PORTING.md — the source declares `ref = $bindable(null)`.\n */\nexport const Composer = forwardRef<HTMLFormElement, ComposerProps>(function Composer(\n\t{\n\t\tvalue: valueProp,\n\t\tonValueChange,\n\t\tattachments: attachmentsProp,\n\t\tonAttachmentsChange,\n\t\tdisabled = false,\n\t\tstreaming = false,\n\t\tplaceholder,\n\t\tonSubmit,\n\t\tonStop,\n\t\tonAttach,\n\t\tchildren,\n\t\taccessory,\n\t\tclassName,\n\t\tsound = false,\n\t},\n\tforwardedRef\n) {\n\t// The source's `value` is `$bindable(\"\")` and `attachments` is\n\t// `$bindable([])`: a consumer can bind either, or leave it alone and let the\n\t// component keep writing its own copy. React has no such channel, so each\n\t// prop is controlled when it is passed and the local copy takes over when it\n\t// is not. Either way the `on*Change` callback fires with the same payload.\n\tconst playCue = useSoundCue(sound);\n\n\tconst [uncontrolledValue, setUncontrolledValue] = useState(\"\");\n\tconst value = valueProp !== undefined ? valueProp : uncontrolledValue;\n\n\tconst [uncontrolledAttachments, setUncontrolledAttachments] =\n\t\tuseState<AttachmentData[]>(NO_ATTACHMENTS);\n\tconst attachments = attachmentsProp !== undefined ? attachmentsProp : uncontrolledAttachments;\n\n\t// Written by ComposerInput through the context, read by insertText's caret\n\t// arithmetic. A composer with no input part keeps it null and falls back to\n\t// appending at the end of the draft.\n\t//\n\t// The ref holds the element so a write lands the instant it is made — the\n\t// context object every part reads is rebuilt per render, but this accessor\n\t// pair is not, so a part that stored it (or a test that captured it) keeps\n\t// reaching the live element. The state beside it exists only to re-render\n\t// the parts that render off the element's arrival: the completion menus,\n\t// which are nothing at all until an input has registered one.\n\tconst textareaElRef = useRef<HTMLTextAreaElement | null>(null);\n\tconst [, setRegisteredTextarea] = useState<HTMLTextAreaElement | null>(null);\n\tconst textareaRef = useConstant(() => ({\n\t\tget current(): HTMLTextAreaElement | null {\n\t\t\treturn textareaElRef.current;\n\t\t},\n\t\tset current(next: HTMLTextAreaElement | null) {\n\t\t\ttextareaElRef.current = next;\n\t\t\tsetRegisteredTextarea(next);\n\t\t},\n\t}));\n\n\t// Where insertText wants the caret once its write has reached the DOM, and\n\t// the render that guarantees the effect below gets a chance to put it there\n\t// even when the consumer ignores `onValueChange`. The counterpart of the\n\t// source's `void tick().then(...)`.\n\tconst pendingCaretRef = useRef<{ el: HTMLTextAreaElement; caret: number } | null>(null);\n\tconst [, setCaretNonce] = useState(0);\n\n\tfunction applyValue(next: string) {\n\t\tif (valueProp === undefined) setUncontrolledValue(next);\n\t\tonValueChange?.(next);\n\t}\n\n\tfunction applyAttachments(next: AttachmentData[]) {\n\t\tif (attachmentsProp === undefined) setUncontrolledAttachments(next);\n\t\tonAttachmentsChange?.(next);\n\t}\n\n\t// Every command is identity-stable and always runs the latest render's\n\t// logic: a part holds one of these across its whole life (a menu's document\n\t// listener, an input's mount registration) and must never call a closure\n\t// over a draft that has moved on.\n\tconst submit = useEventCallback(() => {\n\t\t// A composer that is off or already busy sends nothing, whatever the draft\n\t\t// says — the send button is disabled in both states, but Enter and a\n\t\t// programmatic `submit()` reach here too.\n\t\tif (disabled || streaming) return;\n\t\tconst text = value.trim();\n\t\tif (text === \"\" && attachments.length === 0) return;\n\t\t// With nobody listening there is nowhere for the draft to go, and clearing\n\t\t// it would throw away text the reader has no way of getting back.\n\t\tif (!onSubmit) return;\n\t\tplayCue(\"press\");\n\t\t// A copy, so a consumer stashing the payload does not end up holding the\n\t\t// live list it is about to mutate.\n\t\tonSubmit({ text, attachments: [...attachments] });\n\t\t// The text is ours to clear; the attachments belong to the consumer, who\n\t\t// alone knows whether an upload is still in flight.\n\t\tapplyValue(\"\");\n\t});\n\n\tconst stop = useEventCallback(() => {\n\t\tif (!streaming) return;\n\t\t// No handler, no interruption — the cue must not announce a stop that\n\t\t// cannot happen (ComposerSubmit disables itself in that state too).\n\t\tif (onStop) playCue(\"press\");\n\t\tonStop?.();\n\t});\n\n\tconst setValue = useEventCallback((next: string) => {\n\t\tapplyValue(next);\n\t});\n\n\tconst insertText = useEventCallback((text: string, replaceTriggerToken = false) => {\n\t\t// A composer that is off or already busy takes no dictation either — the\n\t\t// textarea is readonly in both states, and a menu writing through the\n\t\t// context would be the one way around that.\n\t\tif (disabled || streaming) return;\n\t\tconst current = value;\n\t\tconst el = textareaElRef.current;\n\t\tconst rawEnd = el?.selectionEnd;\n\t\tconst end = typeof rawEnd === \"number\" ? rawEnd : current.length;\n\t\tconst rawStart = el?.selectionStart;\n\t\tconst selectionStart = typeof rawStart === \"number\" ? Math.min(rawStart, end) : end;\n\n\t\t// The menus' own definition of a token, not a second one that could drift\n\t\t// from it: what a menu matched is exactly what a completion overwrites.\n\t\tconst trigger = replaceTriggerToken ? findTokenStart(current, end) : -1;\n\t\tconst start = trigger >= 0 ? trigger : selectionStart;\n\t\tconst trailing = current.slice(end);\n\n\t\t// Completing a token leaves the caret ready for the next word; a plain\n\t\t// insert splices exactly what it was handed, and nothing more.\n\t\tconst needsSpace = trigger >= 0 && !/\\s$/.test(text) && !/^\\s/.test(trailing);\n\t\tconst insertion = needsSpace ? `${text} ` : text;\n\t\tconst caret = start + insertion.length;\n\n\t\tapplyValue(`${current.slice(0, start)}${insertion}${trailing}`);\n\n\t\tif (!el) return;\n\t\t// The caret can only be placed once the value has reached the DOM, so it\n\t\t// waits for the commit. Focus comes back with it: the insertion was almost\n\t\t// certainly triggered from a menu that stole it.\n\t\tpendingCaretRef.current = { el, caret };\n\t\tsetCaretNonce((nonce) => nonce + 1);\n\t});\n\n\tconst addFiles = useEventCallback((files: File[]) => {\n\t\tif (disabled || files.length === 0) return;\n\t\tonAttach?.(files);\n\t});\n\n\tconst removeAttachment = useEventCallback((id: string) => {\n\t\tapplyAttachments(attachments.filter((attachment) => attachment.id !== id));\n\t});\n\n\t// No dependency list: the caret restore has to run on the commit that carried\n\t// the insertion, whichever render that turns out to be. It disarms itself.\n\t// A layout effect, so the first painted frame after an insertion already has\n\t// the caret where the source's `tick()` puts it (contract §4).\n\tuseIsomorphicLayoutEffect(() => {\n\t\tconst pending = pendingCaretRef.current;\n\t\tif (!pending) return;\n\t\tpendingCaretRef.current = null;\n\t\tif (!pending.el.isConnected) return;\n\t\tpending.el.focus();\n\t\tpending.el.setSelectionRange(pending.caret, pending.caret);\n\t});\n\n\t// Rebuilt every render on purpose: the rebuild is what re-renders the parts\n\t// reading the draft off it. Never memoise it.\n\tconst context: ComposerContext = {\n\t\tvalue: { current: value },\n\t\tattachments: { current: attachments },\n\t\tdisabled,\n\t\tstreaming,\n\t\tstoppable: typeof onStop === \"function\",\n\t\tsound,\n\t\t// Declared read-only on ComposerContext so no other part writes it; the\n\t\t// setter exists for ComposerInput alone, which registers its element here\n\t\t// on mount. See the note at the top of types.ts.\n\t\ttextareaRef,\n\t\tsubmit,\n\t\tstop,\n\t\tsetValue,\n\t\tinsertText,\n\t\taddFiles,\n\t\tremoveAttachment,\n\t};\n\n\tfunction handleSubmit(event: FormEvent<HTMLFormElement>) {\n\t\t// Nothing here navigates: the draft leaves through onSubmit.\n\t\tevent.preventDefault();\n\t\tsubmit();\n\t}\n\n\treturn (\n\t\t<COMPOSER_CONTEXT_KEY.Provider value={context}>\n\t\t\t<form\n\t\t\t\tref={forwardedRef}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-composer relative flex w-full flex-col border p-2\",\n\t\t\t\t\tclassName,\n\t\t\t\t\tdisabled && \"ft-composer-disabled\"\n\t\t\t\t)}\n\t\t\t\tdata-streaming={streaming ? \"\" : undefined}\n\t\t\t\tonSubmit={handleSubmit}\n\t\t\t>\n\t\t\t\t{children ?? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t<ComposerInput placeholder={placeholder} />\n\t\t\t\t\t\t<div className=\"mt-2 flex items-center gap-2\">\n\t\t\t\t\t\t\t<div className=\"flex-1\" />\n\t\t\t\t\t\t\t<ComposerSubmit />\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</>\n\t\t\t\t)}\n\n\t\t\t\t{accessory ? (\n\t\t\t\t\t<div className=\"ft-composer-accessory absolute inset-0 z-10\">{accessory}</div>\n\t\t\t\t) : null}\n\t\t\t</form>\n\t\t</COMPOSER_CONTEXT_KEY.Provider>\n\t);\n});\n","type":"registry:ui","target":"@ui/fancy/components/composer/Composer.tsx"},{"path":"react/src/components/composer/ComposerAttachment.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport type { AttachmentData } from \"../../internals/ai-types\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport \"./composer-attachment.css\";\n\n/**\n * Props for ComposerAttachment\n */\nexport interface ComposerAttachmentProps {\n\t/** The file to show: its name, and whatever the upload knows so far. */\n\tattachment: AttachmentData;\n\t/** Called with the attachment id instead of the composer's own removal. */\n\tonRemove?: (id: string) => void;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Powers of 1024, stopping at megabytes: a chip is not where gigabytes belong,\n * and a four-digit megabyte count still reads faster than a unit nobody\n * expects next to a file name.\n */\nconst UNITS = [\"B\", \"KB\", \"MB\"] as const;\n\n/** Bytes as a chip-sized caption. Anything unmeasurable prints nothing at all. */\nfunction formatSize(bytes: number | undefined): string {\n\tif (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) return \"\";\n\tlet scaled = bytes;\n\tlet unit = 0;\n\twhile (scaled >= 1024 && unit < UNITS.length - 1) {\n\t\tscaled /= 1024;\n\t\tunit += 1;\n\t}\n\t// Bytes are whole things; the scaled units keep one decimal, and a trailing\n\t// `.0` is noise at this size.\n\tconst rounded = unit === 0 ? Math.round(scaled) : Math.round(scaled * 10) / 10;\n\treturn `${rounded} ${UNITS[unit]}`;\n}\n\n/** One file riding along with the draft: its name, its size, its upload, its cross. */\nexport function ComposerAttachment({ attachment, onRemove, className }: ComposerAttachmentProps) {\n\t// Undefined when the chip is used outside a Composer: it then relies on\n\t// `onRemove` alone, and goes inert without one rather than throwing.\n\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\tconst playCue = useSoundCue(composer?.sound);\n\n\tconst status = attachment.status;\n\tconst uploading = status === \"uploading\";\n\tconst failed = status === \"error\";\n\t// Out-of-range progress is a consumer bug, not a reason to paint a bar past\n\t// the end of the chip.\n\tconst percent = Math.round(Math.min(1, Math.max(0, attachment.progress ?? 0)) * 100);\n\tconst size = formatSize(attachment.size);\n\tconst removeLabel = `Remove ${attachment.name}`;\n\n\t// A remove button with nothing to call is a lie, and so is a live one inside a\n\t// composer that has been switched off.\n\tconst removable = onRemove !== undefined || composer !== undefined;\n\tconst removeDisabled = !removable || (composer?.disabled ?? false);\n\n\tfunction remove() {\n\t\t// The native `disabled` attribute already blocks a real click, but a\n\t\t// synthetic dispatch — in a test, or from any other caller — walks\n\t\t// straight past it, so the handler guards again rather than trusting the\n\t\t// attribute alone (see Button's `handleClick`).\n\t\tif (removeDisabled) return;\n\t\tplayCue(\"press\");\n\t\t// The prop wins outright: a consumer that passes one is running its own\n\t\t// upload bookkeeping and will drop the entry itself.\n\t\tif (onRemove) {\n\t\t\tonRemove(attachment.id);\n\t\t\treturn;\n\t\t}\n\t\tcomposer?.removeAttachment(attachment.id);\n\t}\n\n\treturn (\n\t\t<div\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-composer-attachment relative flex max-w-full min-w-0 items-center gap-1.5 py-1 pr-1 pl-1.5 text-xs\",\n\t\t\t\tclassName,\n\t\t\t\tfailed && \"ft-failed\"\n\t\t\t)}\n\t\t\tdata-status={status}\n\t\t\taria-busy={uploading ? \"true\" : undefined}\n\t\t>\n\t\t\t{attachment.previewUrl ? (\n\t\t\t\t// Decorative: the file name sits right beside it and says the same thing.\n\t\t\t\t<img\n\t\t\t\t\tclassName=\"ft-composer-attachment-thumb size-5 shrink-0 rounded object-cover\"\n\t\t\t\t\tsrc={attachment.previewUrl}\n\t\t\t\t\talt=\"\"\n\t\t\t\t/>\n\t\t\t) : (\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"size-3.5 shrink-0\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"1.75\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5z\" />\n\t\t\t\t\t<path d=\"M14 2v6h6\" />\n\t\t\t\t</svg>\n\t\t\t)}\n\n\t\t\t<span className=\"ft-composer-attachment-name max-w-32 truncate\" title={attachment.name}>\n\t\t\t\t{attachment.name}\n\t\t\t</span>\n\n\t\t\t{size ? (\n\t\t\t\t<span className=\"ft-composer-attachment-size text-foreground/70 shrink-0 tabular-nums\">\n\t\t\t\t\t{size}\n\t\t\t\t</span>\n\t\t\t) : null}\n\n\t\t\t{/* The tint is the only other thing that says so, and it says it to no one. */}\n\t\t\t{failed ? <span className=\"sr-only\">Upload failed</span> : null}\n\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"ft-composer-attachment-remove hover:bg-muted focus-visible:ring-ring inline-flex size-4 shrink-0 cursor-pointer items-center justify-center rounded transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\"\n\t\t\t\tdisabled={removeDisabled}\n\t\t\t\taria-label={removeLabel}\n\t\t\t\ttitle={removeLabel}\n\t\t\t\tonClick={remove}\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"size-3\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"2.5\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"M18 6 6 18\" />\n\t\t\t\t\t<path d=\"m6 6 12 12\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\n\t\t\t{uploading ? (\n\t\t\t\t<span\n\t\t\t\t\tclassName=\"ft-composer-attachment-track\"\n\t\t\t\t\trole=\"progressbar\"\n\t\t\t\t\taria-label={`Uploading ${attachment.name}`}\n\t\t\t\t\taria-valuemin={0}\n\t\t\t\t\taria-valuemax={100}\n\t\t\t\t\taria-valuenow={percent}\n\t\t\t\t>\n\t\t\t\t\t<span className=\"ft-composer-attachment-bar\" style={{ width: `${percent}%` }} />\n\t\t\t\t</span>\n\t\t\t) : null}\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerAttachment.tsx"},{"path":"react/src/components/composer/ComposerAttachments.tsx","content":"\"use client\";\n\nimport { useContext, useRef } from \"react\";\nimport type { ChangeEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { ComposerAttachment } from \"./ComposerAttachment\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport \"./composer-attachments.css\";\n\n/**\n * Props for ComposerAttachments\n */\nexport interface ComposerAttachmentsProps {\n\t/** Replaces the default chips. The add button and its file input stay. */\n\tchildren?: ReactNode;\n\t/** Accessible name and tooltip for the add button. */\n\taddLabel?: string;\n\t/** `accept` for the file picker, e.g. `\"image/*,.pdf\"`. Omitted by default. */\n\taccept?: string;\n\t/** Whether one pick may carry several files. */\n\tmultiple?: boolean;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/** The chip row and the paperclip that fills it. */\nexport function ComposerAttachments({\n\tchildren,\n\taddLabel = \"Attach files\",\n\taccept,\n\tmultiple = true,\n\tclassName,\n}: ComposerAttachmentsProps) {\n\t// Undefined when the row is used outside a Composer: there is then nothing to\n\t// list and nowhere to send a pick, so it renders nothing rather than throwing.\n\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\n\t// Only ever reached imperatively, from the add button's click, so a plain ref\n\t// is enough — no hook is keyed on the node's existence (convention C-1).\n\tconst inputRef = useRef<HTMLInputElement | null>(null);\n\n\tconst attachments = composer?.attachments.current ?? [];\n\tconst disabled = composer?.disabled ?? false;\n\t// An empty row inside a composer still earns its line — that is where the\n\t// paperclip lives. An empty row outside one is a button that could not work.\n\tconst empty = composer === undefined && attachments.length === 0;\n\n\tfunction pick() {\n\t\t// The real picker is the hidden input; the button exists to be named,\n\t\t// focusable, and styled like the rest of the composer chrome.\n\t\tinputRef.current?.click();\n\t}\n\n\tfunction handleChange(event: ChangeEvent<HTMLInputElement>) {\n\t\tconst el = event.currentTarget;\n\t\tconst files = Array.from(el.files ?? []);\n\t\t// Cleared before anything else: re-picking the same file fires no second\n\t\t// change event while the input still holds the first pick, and a consumer\n\t\t// that rejects an upload would be stuck with it.\n\t\tel.value = \"\";\n\t\tif (files.length === 0) return;\n\t\tcomposer?.addFiles(files);\n\t}\n\n\t// Two uploads of the same file can arrive carrying the same id, and a duplicate\n\t// key is a crash — so the second and later sightings of an id take an\n\t// occurrence suffix. The first sighting keeps the bare id, which is what holds\n\t// a chip's identity steady when a chip ahead of it is removed: keying on the\n\t// position would change every key after the gap and replay the enter keyframe\n\t// down the whole tail of the row.\n\tconst seen = new Map<string, number>();\n\tconst keyed = attachments.map((attachment) => {\n\t\tconst occurrence = seen.get(attachment.id) ?? 0;\n\t\tseen.set(attachment.id, occurrence + 1);\n\t\treturn {\n\t\t\tattachment,\n\t\t\tkey: occurrence === 0 ? attachment.id : `${attachment.id}#${occurrence}`,\n\t\t};\n\t});\n\n\tif (empty) return null;\n\n\treturn (\n\t\t<div className={cn(\"ft-composer-attachments flex flex-wrap items-center gap-1.5\", className)}>\n\t\t\t{children ??\n\t\t\t\tkeyed.map(({ attachment, key }) => (\n\t\t\t\t\t<div key={key} className=\"ft-composer-attachment-slot flex min-w-0\">\n\t\t\t\t\t\t<ComposerAttachment attachment={attachment} />\n\t\t\t\t\t</div>\n\t\t\t\t))}\n\n\t\t\t<button\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName=\"ft-composer-attach text-foreground/70 hover:bg-muted hover:text-foreground focus-visible:ring-ring inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\"\n\t\t\t\tdisabled={disabled}\n\t\t\t\taria-label={addLabel}\n\t\t\t\ttitle={addLabel}\n\t\t\t\tonClick={pick}\n\t\t\t>\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"size-4\"\n\t\t\t\t\tviewBox=\"0 0 24 24\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"1.75\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\n\t\t\t{/* Out of the tab order and out of the accessibility tree: the button above\n\t\t\t    is the control, and two names for one picker is one too many. */}\n\t\t\t<input\n\t\t\t\tref={inputRef}\n\t\t\t\tclassName=\"ft-composer-file-input\"\n\t\t\t\ttype=\"file\"\n\t\t\t\taccept={accept}\n\t\t\t\tmultiple={multiple}\n\t\t\t\tdisabled={disabled}\n\t\t\t\ttabIndex={-1}\n\t\t\t\taria-hidden=\"true\"\n\t\t\t\tonChange={handleChange}\n\t\t\t/>\n\t\t</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerAttachments.tsx"},{"path":"react/src/components/composer/ComposerCommandMenu.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useEffect, useMemo, useState } from \"react\";\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { useFloat } from \"../../internals/use-float\";\nimport type { FloatRect } from \"../../internals/float\";\nimport type { CommandItemData } from \"../../internals/ai-types\";\nimport { findTriggerToken, measureCaretRect } from \"./caret\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport \"./composer-command-menu.css\";\n\n/**\n * Props for ComposerCommandMenu\n */\nexport interface ComposerCommandMenuProps {\n\t/** The character that opens the menu — `/` for commands, `@` for mentions. */\n\ttrigger: string;\n\t/** Everything the menu can offer, before filtering. */\n\titems: CommandItemData[];\n\t/**\n\t * Handles a picked item. Defaults to completing the trigger token with the\n\t * item's label. `query` is what had been typed after the trigger.\n\t */\n\tonSelect?: (\n\t\titem: CommandItemData,\n\t\tctx: { insertText: (text: string, replaceTriggerToken?: boolean) => void; query: string }\n\t) => void;\n\t/** Decides which items survive the query. Defaults to a case-insensitive label/description match. */\n\tfilter?: (item: CommandItemData, query: string) => boolean;\n\t/** Shown in place of the rows when nothing matches. */\n\tempty?: ReactNode;\n\t/** How many matches the menu shows at once. */\n\tmaxItems?: number;\n\t/** What this menu offers: its accessible name, and the noun it is counted in. */\n\tlabel?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/** Anchors the float before the caret has ever been measured. */\nconst ORIGIN: FloatRect = { x: 0, y: 0, width: 0, height: 0 };\n\nfunction defaultFilter(item: CommandItemData, text: string): boolean {\n\tif (text === \"\") return true;\n\tconst needle = text.toLowerCase();\n\treturn (\n\t\titem.label.toLowerCase().includes(needle) ||\n\t\t(item.description?.toLowerCase().includes(needle) ?? false)\n\t);\n}\n\n/**\n * Take the key away from everything else.\n *\n * `preventDefault` alone is not enough: the input's Enter-to-send handler never\n * asks whether the event was already handled, and it is registered further up\n * the tree, so the event has to stop travelling.\n */\nfunction consume(event: KeyboardEvent) {\n\tevent.preventDefault();\n\tevent.stopPropagation();\n}\n\n/**\n * A completion list that opens on a trigger token in the composer's draft.\n *\n * The menu element arrives through the ref channel rather than a `ref` prop,\n * per PORTING.md — the source declares `ref = $bindable(null)`. It is null\n * whenever the menu is closed, because it is not rendered then.\n */\nexport const ComposerCommandMenu = forwardRef<HTMLDivElement, ComposerCommandMenuProps>(\n\tfunction ComposerCommandMenu(\n\t\t{ trigger, items, onSelect, filter, empty, maxItems = 8, label = \"Commands\", className },\n\t\tforwardedRef\n\t) {\n\t\t// Undefined outside a Composer: the menu then has no textarea to watch and\n\t\t// renders nothing at all, rather than throwing on a missing provider.\n\t\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\t\tconst playCue = useSoundCue(composer?.sound);\n\n\t\tconst uid = useFancyId();\n\t\tconst listId = `${uid}-list`;\n\n\t\tconst textarea = composer?.textareaRef.current ?? null;\n\t\t// A composer that is off or streaming takes no dictation: completing into it\n\t\t// would write through a textarea the reader cannot type in.\n\t\tconst inert = (composer?.disabled ?? false) || (composer?.streaming ?? false);\n\n\t\tconst [open, setOpen] = useState(false);\n\t\tconst [query, setQuery] = useState(\"\");\n\t\t/** Where the open token starts, or -1. This is what \"the same token\" means. */\n\t\tconst [tokenStart, setTokenStart] = useState(-1);\n\t\t/** The token Escape dismissed, so it does not spring back on the next keystroke. */\n\t\tconst [dismissedStart, setDismissedStart] = useState(-1);\n\t\tconst [activeIndex, setActiveIndex] = useState(0);\n\n\t\t// The node, not a ref: the float below is keyed on it, and the menu only\n\t\t// exists while it is open — exactly the conditional node a `[]`-deps effect\n\t\t// reading a ref would miss (convention C-1). Composed above the early\n\t\t// return, never inside the JSX after it (convention C-2).\n\t\tconst [menuEl, menuRef] = useElementRef<HTMLDivElement>();\n\t\tconst ref = useComposedRefs(forwardedRef, menuRef);\n\n\t\tconst matches = items.filter((item) => (filter ?? defaultFilter)(item, query));\n\t\tconst visible = matches.slice(0, Math.max(0, maxItems));\n\t\t// The stored index is a wish; this is what it can actually be once the query\n\t\t// has shortened the list under it.\n\t\tconst active = visible.length === 0 ? -1 : Math.min(activeIndex, visible.length - 1);\n\n\t\t// A getter, not a rect measured once when the token opened: the float\n\t\t// re-reads its anchor on every scroll and resize, and a frozen rect would\n\t\t// send it repositioning against where the caret used to be.\n\t\tconst anchor = useMemo(() => {\n\t\t\tconst el = textarea;\n\t\t\tconst start = tokenStart;\n\t\t\treturn (): FloatRect => (el && start >= 0 ? measureCaretRect(el, start) : ORIGIN);\n\t\t}, [textarea, tokenStart]);\n\t\tuseFloat(menuEl, { anchor, placement: \"top-start\", offset: 6 });\n\n\t\t/**\n\t\t * The menu never takes focus — the reader is typing, and a completion list\n\t\t * that steals the caret would break the very sentence it is completing. That\n\t\t * rules out the usual combobox wiring, which needs `role`, `aria-expanded`,\n\t\t * `aria-controls` and `aria-activedescendant` on the input itself, and this\n\t\t * component is in no position to put them there: the textarea belongs to\n\t\t * `ComposerInput`, and reaching across to rewrite another component's\n\t\t * attributes from a sibling is exactly the kind of spooky action a compound\n\t\t * component should not do (it would also fight that component's own renders).\n\t\t *\n\t\t * So the announcement is made out loud instead, through a live region that is\n\t\t * always in the DOM — a region inserted at the same moment as its text usually\n\t\t * goes unread. The tradeoff is real: a screen-reader user hears how many\n\t\t * matches there are and that the arrows do something, but cannot hear each row\n\t\t * as it becomes active. Pointer and sighted-keyboard users lose nothing.\n\t\t *\n\t\t * The rows still carry stable ids, so a consumer who owns their own input part\n\t\t * — and may therefore write to it — can read the menu through `ref`, point\n\t\t * `aria-activedescendant` at the selected row, and have the full pattern.\n\t\t */\n\t\tconst announcement = (() => {\n\t\t\tif (!open) return \"\";\n\t\t\tconst noun = label.toLowerCase();\n\t\t\tconst word = visible.length === 1 && noun.endsWith(\"s\") ? noun.slice(0, -1) : noun;\n\t\t\treturn `${visible.length} ${word} available, use the arrow keys`;\n\t\t})();\n\n\t\tconst close = useEventCallback(() => {\n\t\t\tsetOpen(false);\n\t\t\tsetTokenStart(-1);\n\t\t});\n\n\t\t/**\n\t\t * Re-read the draft and decide whether the menu belongs on screen.\n\t\t *\n\t\t * Called from the textarea's own events rather than from an effect: the caret\n\t\t * is not reactive state, so nothing would wake an effect when it moves.\n\t\t */\n\t\tconst sync = useEventCallback(() => {\n\t\t\tconst el = textarea;\n\t\t\tif (!el || inert) {\n\t\t\t\tclose();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst end = el.selectionEnd ?? el.value.length;\n\t\t\t// Mid-selection there is no caret to complete at, only a range.\n\t\t\tif ((el.selectionStart ?? end) !== end) {\n\t\t\t\tclose();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst token = findTriggerToken(el.value, end, trigger);\n\t\t\tif (!token) {\n\t\t\t\t// Outside any token, a dismissal has nothing left to apply to: coming\n\t\t\t\t// back to the same spot should open the menu again.\n\t\t\t\tsetDismissedStart(-1);\n\t\t\t\tclose();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (token.start === dismissedStart) {\n\t\t\t\tsetOpen(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t// A different token, or a different query within it, starts the list again\n\t\t\t// from the top — the row that was active may not even be in it any more.\n\t\t\tif (token.start !== tokenStart || token.query !== query) setActiveIndex(0);\n\t\t\t// Anchored to the trigger character, not to the caret: the menu then holds\n\t\t\t// still while the query is typed instead of crawling along with it. The\n\t\t\t// anchor getter above reads this position live.\n\t\t\tsetTokenStart(token.start);\n\t\t\tsetQuery(token.query);\n\t\t\tsetOpen(true);\n\t\t});\n\n\t\tconst move = useEventCallback((delta: number) => {\n\t\t\tconst count = visible.length;\n\t\t\tif (count === 0) return;\n\t\t\tconst from = active < 0 ? 0 : active;\n\t\t\tsetActiveIndex((from + delta + count) % count);\n\t\t});\n\n\t\tconst select = useEventCallback((item: CommandItemData | undefined) => {\n\t\t\tif (!item) return;\n\t\t\t// The menu's own open/close stay silent — it opens from keystrokes and\n\t\t\t// closes on blur/Escape, not a dismissal the reader triggered — so a pick\n\t\t\t// is the only cue this component ever plays.\n\t\t\tplayCue(\"select\");\n\t\t\tconst insertText = (text: string, replaceTriggerToken?: boolean) =>\n\t\t\t\tcomposer?.insertText(text, replaceTriggerToken);\n\t\t\tif (onSelect) onSelect(item, { insertText, query });\n\t\t\t// The trailing space is part of the completion: it closes the token, which\n\t\t\t// is also what stops the menu from immediately reopening on it.\n\t\t\telse insertText(`${item.label} `, true);\n\t\t\tsetDismissedStart(-1);\n\t\t\tclose();\n\t\t});\n\n\t\tconst handleKeydown = useEventCallback((event: KeyboardEvent) => {\n\t\t\tif (!open) return;\n\t\t\t// Mid-composition these keys belong to the IME, which is picking a candidate\n\t\t\t// of its own.\n\t\t\tif (event.isComposing) return;\n\n\t\t\tif (event.key === \"Escape\") {\n\t\t\t\t// Dismissed for this token only. Typing on in it keeps the menu away;\n\t\t\t\t// starting another one brings it back.\n\t\t\t\tsetDismissedStart(tokenStart);\n\t\t\t\tclose();\n\t\t\t\tconsume(event);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"ArrowDown\") {\n\t\t\t\tmove(1);\n\t\t\t\tconsume(event);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"ArrowUp\") {\n\t\t\t\tmove(-1);\n\t\t\t\tconsume(event);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (event.key === \"Enter\" || event.key === \"Tab\") {\n\t\t\t\t// Nothing to complete: Enter goes back to meaning send.\n\t\t\t\tif (active < 0) return;\n\t\t\t\tselect(visible[active]);\n\t\t\t\tconsume(event);\n\t\t\t}\n\t\t});\n\n\t\tconst handleBlur = useEventCallback(() => {\n\t\t\tclose();\n\t\t});\n\n\t\t// Wired here rather than with `on*` props because the element belongs to\n\t\t// another component: this part only ever gets handed the node through the\n\t\t// context, and it must let go of it just as cleanly. Every handler is\n\t\t// identity-stable, so the list of deps is the node and nothing else — one\n\t\t// registration per element, whatever the menu's own state does.\n\t\tuseEffect(() => {\n\t\t\tconst el = textarea;\n\t\t\tif (!el) {\n\t\t\t\tsetOpen(false);\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst doc = el.ownerDocument;\n\t\t\tconst handleSelectionChange = () => {\n\t\t\t\t// `selectionchange` only fires on the document, for every selection on the\n\t\t\t\t// page: the guard is what makes it mean \"the caret moved in *our* input\",\n\t\t\t\t// and it is the only way to notice arrow keys and clicks that move the\n\t\t\t\t// caret without changing a character.\n\t\t\t\tif (doc.activeElement === el) sync();\n\t\t\t};\n\n\t\t\tel.addEventListener(\"input\", sync);\n\t\t\tel.addEventListener(\"keydown\", handleKeydown);\n\t\t\tel.addEventListener(\"blur\", handleBlur);\n\t\t\tdoc.addEventListener(\"selectionchange\", handleSelectionChange);\n\t\t\treturn () => {\n\t\t\t\tel.removeEventListener(\"input\", sync);\n\t\t\t\tel.removeEventListener(\"keydown\", handleKeydown);\n\t\t\t\tel.removeEventListener(\"blur\", handleBlur);\n\t\t\t\tdoc.removeEventListener(\"selectionchange\", handleSelectionChange);\n\t\t\t};\n\t\t}, [textarea, sync, handleKeydown, handleBlur]);\n\n\t\t// Reads the switches, writes only the menu: a composer that goes dark or\n\t\t// starts streaming mid-query takes the open menu down with it.\n\t\tuseEffect(() => {\n\t\t\tif (inert) close();\n\t\t}, [inert, close]);\n\n\t\t// The draft can also change under the menu without a keystroke — a submit\n\t\t// clearing it, a consumer restoring one — and neither fires `input`. Reading\n\t\t// the draft here is what wakes the token search on those writes; `open` is\n\t\t// read off a live ref rather than a dependency, so the effect answers to the\n\t\t// draft alone and never to the state `sync` itself sets.\n\t\tconst openLive = useLiveRef(open);\n\t\tconst draft = composer?.value.current;\n\t\tuseEffect(() => {\n\t\t\tvoid draft;\n\t\t\tif (openLive.current) sync();\n\t\t}, [draft, openLive, sync]);\n\n\t\tif (!textarea) return null;\n\n\t\treturn (\n\t\t\t<>\n\t\t\t\t{/*\n\t\t\t\t\tOnly in the DOM while it is open: a closed completion list is not a\n\t\t\t\t\thidden one, it does not exist, and neither its rows nor its live\n\t\t\t\t\tgeometry should cost anything while the reader is just typing.\n\t\t\t\t*/}\n\t\t\t\t{open ? (\n\t\t\t\t\t<div\n\t\t\t\t\t\tref={ref}\n\t\t\t\t\t\tid={listId}\n\t\t\t\t\t\trole=\"listbox\"\n\t\t\t\t\t\taria-label={label}\n\t\t\t\t\t\tclassName={cn(\"ft-composer-command-menu flex flex-col text-sm\", className)}\n\t\t\t\t\t>\n\t\t\t\t\t\t{visible.length > 0 ? (\n\t\t\t\t\t\t\t/* Suffixed with the index: two items may arrive carrying the same id. */\n\t\t\t\t\t\t\tvisible.map((item, index) => (\n\t\t\t\t\t\t\t\t<button\n\t\t\t\t\t\t\t\t\tkey={`${item.id}#${index}`}\n\t\t\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\t\t\tid={`${listId}-${index}`}\n\t\t\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\t\t\taria-selected={index === active}\n\t\t\t\t\t\t\t\t\tclassName={cn(\n\t\t\t\t\t\t\t\t\t\t\"ft-composer-command flex w-full items-baseline gap-2 rounded-md px-2 py-1.5 text-left\",\n\t\t\t\t\t\t\t\t\t\tindex === active && \"ft-active\"\n\t\t\t\t\t\t\t\t\t)}\n\t\t\t\t\t\t\t\t\tonMouseDown={(event) => event.preventDefault()}\n\t\t\t\t\t\t\t\t\tonClick={() => select(item)}\n\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t<span className=\"ft-composer-command-label min-w-0 truncate font-medium\">\n\t\t\t\t\t\t\t\t\t\t{item.label}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t{item.description ? (\n\t\t\t\t\t\t\t\t\t\t/*\n\t\t\t\t\t\t\t\t\t\t\t`text-foreground/70` rather than `text-muted-foreground`: this\n\t\t\t\t\t\t\t\t\t\t\ttext sits on the menu's own opaque surface, and on the tinted\n\t\t\t\t\t\t\t\t\t\t\tactive row above it, where the muted token drops under 4.5:1.\n\t\t\t\t\t\t\t\t\t\t\tSee the note on `.ft-active` for the arithmetic.\n\t\t\t\t\t\t\t\t\t\t*/\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground/70 min-w-0 flex-1 truncate text-xs\">\n\t\t\t\t\t\t\t\t\t\t\t{item.description}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t\t{item.hint ? (\n\t\t\t\t\t\t\t\t\t\t<span className=\"text-foreground/70 ml-auto flex-none font-mono text-xs\">\n\t\t\t\t\t\t\t\t\t\t\t{item.hint}\n\t\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t</button>\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t) : empty ? (\n\t\t\t\t\t\t\tempty\n\t\t\t\t\t\t) : (\n\t\t\t\t\t\t\t<p className=\"text-foreground/70 px-2 py-1.5 text-xs italic\">No matches.</p>\n\t\t\t\t\t\t)}\n\t\t\t\t\t</div>\n\t\t\t\t) : null}\n\n\t\t\t\t{/* Mounted whether or not the menu is: see the note on `announcement`. */}\n\t\t\t\t<div className=\"sr-only\" role=\"status\" aria-live=\"polite\">\n\t\t\t\t\t{announcement}\n\t\t\t\t</div>\n\t\t\t</>\n\t\t);\n\t}\n);\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerCommandMenu.tsx"},{"path":"react/src/components/composer/ComposerInput.tsx","content":"\"use client\";\n\nimport { forwardRef, useContext, useEffect, useRef } from \"react\";\nimport type { ChangeEvent, ComponentPropsWithoutRef, KeyboardEvent } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useComposedRefs } from \"../../internals/dom/use-composed-refs\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport \"./composer-input.css\";\n\n/**\n * Props for ComposerInput\n */\ninterface ComposerInputOwnProps {\n\t/** Shown while the draft is empty. */\n\tplaceholder?: string;\n\t/** Height, in lines, before anything has been typed. Also the SSR height. */\n\trows?: number;\n\t/** Height ceiling, in lines. Past it the textarea scrolls instead of growing. */\n\tmaxRows?: number;\n\t/** Take focus on mount. */\n\tautofocus?: boolean;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Native `<textarea>` attributes and listeners are forwarded to the textarea; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface ComposerInputProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"textarea\">, keyof ComposerInputOwnProps>,\n\t\tComposerInputOwnProps {}\n\n/** Used when the platform reports no usable line-height, e.g. under jsdom. */\nconst FALLBACK_LINE_HEIGHT = 20;\n/** Multiplier applied to the font size when only that is measurable. */\nconst NORMAL_LINE_RATIO = 1.5;\n\n/**\n * A line count a textarea can actually be built from.\n *\n * `rows={0}` collapses the box to nothing and `maxRows={NaN}` poisons every\n * height derived from it — both arrive from a consumer computing the number\n * rather than writing it, so they fall back instead of reaching the geometry.\n */\nfunction lines(count: number, fallback: number): number {\n\tconst whole = Math.floor(count);\n\treturn Number.isFinite(whole) && whole >= 1 ? whole : fallback;\n}\n\nfunction px(input: string): number {\n\tconst parsed = Number.parseFloat(input);\n\treturn Number.isFinite(parsed) ? parsed : 0;\n}\n\n/**\n * The composer's textarea: auto-growing, Enter-to-send, registered into the root.\n *\n * The textarea arrives through the ref channel rather than a `ref` prop, per\n * PORTING.md — the source declares `ref = $bindable(null)`.\n */\nexport const ComposerInput = forwardRef<HTMLTextAreaElement, ComposerInputProps>(\n\tfunction ComposerInput(\n\t\t{\n\t\t\tplaceholder = \"Message…\",\n\t\t\trows = 1,\n\t\t\tmaxRows = 8,\n\t\t\tautofocus = false,\n\t\t\tclassName,\n\t\t\tonChange,\n\t\t\tonKeyDown,\n\t\t\t...rest\n\t\t},\n\t\tforwardedRef\n\t) {\n\t\t// Undefined when the input is used outside a Composer: it then behaves as a\n\t\t// plain uncontrolled textarea rather than throwing, and Enter does nothing.\n\t\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\n\t\t// The node, not a ref: the mount registration below is keyed on it, and a\n\t\t// `[]`-deps effect reading a ref would be the bug convention C-1 exists to\n\t\t// remove.\n\t\tconst [node, setNode] = useElementRef<HTMLTextAreaElement>();\n\t\tconst ref = useComposedRefs(forwardedRef, setNode);\n\n\t\tconst value = composer?.value.current ?? \"\";\n\n\t\tconst minLines = lines(rows, 1);\n\t\t// A ceiling below the floor is not a ceiling: the box would be told to grow\n\t\t// and to stop growing at the same height.\n\t\tconst maxLines = Math.max(lines(maxRows, 8), minLines);\n\t\t// Readonly rather than disabled: a disabled textarea drops out of the tab\n\t\t// order and stops announcing its content, and a reader mid-draft should still\n\t\t// be able to select and copy what they wrote while a response streams in.\n\t\tconst locked = (composer?.disabled ?? false) || (composer?.streaming ?? false);\n\n\t\t// Measured once from the mounted element — line-height does not change under\n\t\t// this component's feet, and re-reading it on every keystroke would force a\n\t\t// layout for an answer we already have.\n\t\tconst metricsRef = useRef<{ line: number; extra: number } | null>(null);\n\n\t\tfunction measure(el: HTMLTextAreaElement): { line: number; extra: number } {\n\t\t\tif (metricsRef.current) return metricsRef.current;\n\t\t\tconst style = getComputedStyle(el);\n\t\t\tconst declared = Number.parseFloat(style.lineHeight);\n\t\t\tconst fontSize = Number.parseFloat(style.fontSize);\n\t\t\tconst line =\n\t\t\t\tNumber.isFinite(declared) && declared > 0\n\t\t\t\t\t? declared\n\t\t\t\t\t: Number.isFinite(fontSize) && fontSize > 0\n\t\t\t\t\t\t? fontSize * NORMAL_LINE_RATIO\n\t\t\t\t\t\t: FALLBACK_LINE_HEIGHT;\n\t\t\tconst padding = px(style.paddingTop) + px(style.paddingBottom);\n\t\t\t// scrollHeight covers content and padding but never the border, which a\n\t\t\t// border-box height does include.\n\t\t\tconst border =\n\t\t\t\tstyle.boxSizing === \"border-box\"\n\t\t\t\t\t? px(style.borderTopWidth) + px(style.borderBottomWidth)\n\t\t\t\t\t: 0;\n\t\t\tmetricsRef.current = { line, extra: padding + border };\n\t\t\treturn metricsRef.current;\n\t\t}\n\n\t\t/**\n\t\t * Fit the box to its content, between the declared rows and the ceiling.\n\t\t *\n\t\t * `height: auto` first, otherwise scrollHeight reports the box we last set\n\t\t * rather than the text inside it, and the textarea can only ever grow.\n\t\t */\n\t\tfunction grow() {\n\t\t\tconst el = node;\n\t\t\tif (!el) return;\n\t\t\tconst { line, extra } = measure(el);\n\t\t\tel.style.height = \"auto\";\n\t\t\tconst content = el.scrollHeight;\n\t\t\tconst min = minLines * line + extra;\n\t\t\tconst max = maxLines * line + extra;\n\t\t\tconst height = Math.min(Math.max(content, min), max);\n\t\t\tel.style.height = `${height}px`;\n\t\t\tel.style.overflowY = content > max ? \"auto\" : \"hidden\";\n\t\t}\n\n\t\tfunction handleInput(event: ChangeEvent<HTMLTextAreaElement>) {\n\t\t\tcomposer?.setValue(event.currentTarget.value);\n\t\t\tgrow();\n\t\t\tonChange?.(event);\n\t\t}\n\n\t\tfunction handleKeydown(event: KeyboardEvent<HTMLTextAreaElement>) {\n\t\t\thandleEnterKey(event);\n\t\t\tonKeyDown?.(event);\n\t\t}\n\n\t\tfunction handleEnterKey(event: KeyboardEvent<HTMLTextAreaElement>) {\n\t\t\tif (event.key !== \"Enter\" || event.shiftKey) return;\n\t\t\t// Mid-composition Enter belongs to the IME, which is picking a candidate,\n\t\t\t// not to the composer.\n\t\t\tif (event.nativeEvent.isComposing) return;\n\t\t\tevent.preventDefault();\n\t\t\tcomposer?.submit();\n\t\t}\n\n\t\tuseEffect(() => {\n\t\t\t// Registering here rather than through a prop keeps the wiring inside the\n\t\t\t// pair that needs it: the root's caret arithmetic reaches the element, and\n\t\t\t// no consumer has to thread a ref between two components it composed.\n\t\t\t// The cast is the documented escape hatch from the read-only view every\n\t\t\t// other part sees — see the note at the top of types.ts. The slot is the\n\t\t\t// one member of the context the root does NOT rebuild per render, so\n\t\t\t// capturing it here for the life of the node is safe.\n\t\t\tif (!node) return;\n\t\t\tconst slot = composer?.textareaRef as { current: HTMLTextAreaElement | null } | undefined;\n\t\t\tif (slot) slot.current = node;\n\t\t\tif (autofocus) node.focus();\n\t\t\tgrow();\n\t\t\treturn () => {\n\t\t\t\t// Only retract our own registration: a second input may have replaced\n\t\t\t\t// us in the slot already.\n\t\t\t\tif (slot && slot.current === node) slot.current = null;\n\t\t\t};\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [node]);\n\n\t\t// Growth has to answer to programmatic writes too — an inserted slash command\n\t\t// or a restored draft never passes through the input handler. Reads the draft,\n\t\t// writes only the element's style, so it can never wake itself.\n\t\t//\n\t\t// The line counts are dependencies, not constants: a consumer that raises or\n\t\t// lowers `maxRows` after mount — an expand toggle, a breakpoint — must see the\n\t\t// box re-fit to the new ceiling straight away rather than at the next\n\t\t// keystroke. `grow` itself is deliberately absent: it is rebuilt every render\n\t\t// and reads nothing else reactive.\n\t\tuseEffect(() => {\n\t\t\tgrow();\n\t\t\t// eslint-disable-next-line react-hooks/exhaustive-deps\n\t\t}, [value, node, minLines, maxLines]);\n\n\t\treturn (\n\t\t\t<textarea\n\t\t\t\t{...rest}\n\t\t\t\tref={ref}\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-composer-input placeholder:text-muted-foreground w-full resize-none bg-transparent px-2 py-1.5 text-sm leading-relaxed outline-none\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\trows={minLines}\n\t\t\t\tplaceholder={placeholder}\n\t\t\t\t// Controlled by the root's draft, and only by it. Outside a composer\n\t\t\t\t// there is no draft to control, and a permanently-empty controlled\n\t\t\t\t// value would erase every keystroke the moment React restored it —\n\t\t\t\t// where the source's one-way `value={value}` simply never fires again.\n\t\t\t\tvalue={composer ? value : undefined}\n\t\t\t\treadOnly={locked}\n\t\t\t\taria-disabled={locked ? \"true\" : undefined}\n\t\t\t\tonChange={handleInput}\n\t\t\t\tonKeyDown={handleKeydown}\n\t\t\t/>\n\t\t);\n\t}\n);\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerInput.tsx"},{"path":"react/src/components/composer/ComposerModelPicker.tsx","content":"\"use client\";\n\nimport { useCallback, useContext, useEffect, useState } from \"react\";\nimport type { ComponentPropsWithoutRef, KeyboardEvent, MouseEvent as ReactMouseEvent } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { useIsomorphicLayoutEffect } from \"../../internals/dom/ssr\";\nimport { useElementRef } from \"../../internals/dom/use-element-ref\";\nimport { useEventCallback } from \"../../internals/dom/use-event-callback\";\nimport { useLiveRef } from \"../../internals/dom/use-live-ref\";\nimport { useFancyId } from \"../../internals/use-id\";\nimport { useSoundCue } from \"../../sound/use-sound\";\nimport { useFloat } from \"../../internals/use-float\";\nimport type { FloatRect } from \"../../internals/float\";\nimport type { ModelOptionData } from \"../../internals/ai-types\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\nimport \"./composer-model-picker.css\";\n\n/**\n * Props for ComposerModelPicker\n */\ninterface ComposerModelPickerOwnProps {\n\t/** The models on offer. An empty list leaves the picker inert. */\n\tmodels: ModelOptionData[];\n\t/**\n\t * The selected model's id. Controlled when passed — report the pick back\n\t * through `onValueChange`; leave it out and the picker keeps its own copy.\n\t * Falls back to the first model.\n\t */\n\tvalue?: string;\n\t/** Called with every pick the picker makes, changed or not — the write-back channel. */\n\tonValueChange?: (id: string) => void;\n\t/** Called with the id of a newly picked model. Silent when the pick changes nothing. */\n\tonChange?: (id: string) => void;\n\t/** Accessible name for the control and its menu. */\n\tlabel?: string;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Native `<button>` attributes and listeners are forwarded to the trigger button; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface ComposerModelPickerProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof ComposerModelPickerOwnProps>,\n\t\tComposerModelPickerOwnProps {}\n\n/**\n * The model switcher that lives on the composer's bottom rail.\n *\n * Focus lives on the listbox while the menu is open — the element carrying\n * `aria-activedescendant` has to be the one holding focus — and returns to the\n * trigger when the menu closes on Escape or on a pick. The menu is only in the\n * DOM while it is open, and the one document listener it needs is added on open\n * and taken back on close.\n */\nexport function ComposerModelPicker({\n\tmodels,\n\tvalue: valueProp,\n\tonValueChange,\n\tonChange,\n\tlabel = \"Model\",\n\tclassName,\n\tonClick,\n\tonKeyDown,\n\t...rest\n}: ComposerModelPickerProps) {\n\t// Undefined when the picker is used outside a Composer: it then behaves as a\n\t// standalone select rather than throwing, and nothing switches it off.\n\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\tconst playCue = useSoundCue(composer?.sound);\n\n\tconst uid = useFancyId();\n\tconst menuId = `${uid}-menu`;\n\t// Ids are built from the position, not from the model's own id: two entries\n\t// arriving with the same id — a duplicated tier, a badly deduplicated list —\n\t// would otherwise both answer to the same `aria-activedescendant`.\n\tconst optionId = (index: number) => `${uid}-option-${index}`;\n\n\t// The source's `value` is `$bindable()`: a consumer can bind it, or leave it\n\t// alone and let the picker keep writing its own copy. React has no such\n\t// channel, so the prop is controlled when it is passed and the local copy\n\t// takes over when it is not. Either way `onValueChange` fires with the pick.\n\tconst [uncontrolledValue, setUncontrolledValue] = useState<string | undefined>(undefined);\n\tconst value = valueProp !== undefined ? valueProp : uncontrolledValue;\n\n\tconst [open, setOpen] = useState(false);\n\tconst [activeIndex, setActiveIndex] = useState(0);\n\tconst [triggerEl, triggerRef] = useElementRef<HTMLButtonElement>();\n\tconst [menuEl, menuRef] = useElementRef<HTMLDivElement>();\n\n\t// Read by the long-lived closures below — the document listener that must be\n\t// registered exactly once per open, and the anchor getter the float re-reads\n\t// on every scroll tick — so neither is rebuilt when an element arrives.\n\tconst triggerLive = useLiveRef(triggerEl);\n\tconst menuLive = useLiveRef(menuEl);\n\n\t// An unset `value` means \"whichever model comes first\", so the picker can be\n\t// dropped in without the consumer having to seed the binding.\n\tconst selectedId = value ?? models[0]?.id;\n\t// Deliberately not falling back to the first model: a `value` naming something\n\t// that is not on offer shows the bare label instead of quietly claiming a\n\t// model the consumer never selected.\n\tconst selected = models.find((model) => model.id === selectedId);\n\tconst activeId = models[activeIndex] ? optionId(activeIndex) : undefined;\n\t// Nothing to choose from is as inert as a composer that is switched off.\n\tconst isDisabled = (composer?.disabled ?? false) || models.length === 0;\n\n\tfunction applyValue(next: string) {\n\t\tif (valueProp === undefined) setUncontrolledValue(next);\n\t\tonValueChange?.(next);\n\t}\n\n\tconst openMenu = useEventCallback(() => {\n\t\tif (isDisabled || open) return;\n\t\t// The menu opens on the model in force, so Enter without touching an arrow\n\t\t// is a no-op rather than a silent switch to whatever sits at the top.\n\t\tconst current = models.findIndex((model) => model.id === selectedId);\n\t\tsetActiveIndex(current >= 0 ? current : 0);\n\t\tsetOpen(true);\n\t\tplayCue(\"open\");\n\t});\n\n\t// `reason` distinguishes a commit-flavoured close (a model was just picked)\n\t// from a plain dismiss (Escape, Tab, an outside press, or the trigger\n\t// toggling the menu shut). Only a dismiss plays `close` — a commit already\n\t// played `select` inside `select()` below, and the contract is one cue per\n\t// interaction, never both.\n\tconst closeMenu = useEventCallback(\n\t\t(returnFocus: boolean, reason: \"commit\" | \"dismiss\" = \"dismiss\") => {\n\t\t\tif (!open) return;\n\t\t\tsetOpen(false);\n\t\t\tif (reason === \"dismiss\") playCue(\"close\");\n\t\t\t// Focus was moved into the listbox on open; leaving it there would drop the\n\t\t\t// keyboard user at the top of the document when the listbox disappears.\n\t\t\tif (returnFocus) triggerLive.current?.focus();\n\t\t}\n\t);\n\n\tconst move = useEventCallback((delta: number) => {\n\t\tif (models.length === 0) return;\n\t\t// Wraps: the list is short, and a menu that dead-ends at its last entry\n\t\t// makes the reader reverse direction to reach the option one step past it.\n\t\tsetActiveIndex((index) => (index + delta + models.length) % models.length);\n\t});\n\n\tconst select = useEventCallback((index: number) => {\n\t\tconst model = models[index];\n\t\tif (!model || isDisabled) return;\n\t\tconst changed = model.id !== selectedId;\n\t\tapplyValue(model.id);\n\t\t// Re-picking the model already in force plays no `select` — same as\n\t\t// `onChange` below — and the menu closes as a dismissal instead, with\n\t\t// `close`, rather than in silence (RadioGroup/Select parity).\n\t\tif (changed) playCue(\"select\");\n\t\tcloseMenu(true, changed ? \"commit\" : \"dismiss\");\n\t\t// `onChange` reports a change, not an interaction: re-picking the model\n\t\t// already in force has nothing to announce.\n\t\tif (changed) onChange?.(model.id);\n\t});\n\n\tfunction handleTriggerClick(event: ReactMouseEvent<HTMLButtonElement>) {\n\t\tif (isDisabled) return;\n\t\tif (open) closeMenu(true);\n\t\telse openMenu();\n\t\tonClick?.(event);\n\t}\n\n\tfunction handleTriggerKeydown(event: KeyboardEvent<HTMLButtonElement>) {\n\t\tif (isDisabled) return;\n\t\thandleTriggerKey(event);\n\t\tonKeyDown?.(event);\n\t}\n\n\tfunction handleTriggerKey(event: KeyboardEvent<HTMLButtonElement>) {\n\t\tif (event.key !== \"ArrowDown\" && event.key !== \"ArrowUp\") return;\n\t\t// Both arrows open. The menu sits above the composer, which is where \"up\"\n\t\t// expects to find it, and \"down\" is the habit every native select taught.\n\t\tevent.preventDefault();\n\t\topenMenu();\n\t}\n\n\tfunction handleMenuKeydown(event: KeyboardEvent<HTMLDivElement>) {\n\t\tswitch (event.key) {\n\t\t\tcase \"ArrowDown\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tmove(1);\n\t\t\t\tbreak;\n\t\t\tcase \"ArrowUp\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tmove(-1);\n\t\t\t\tbreak;\n\t\t\tcase \"Enter\":\n\t\t\tcase \" \":\n\t\t\t\t// preventDefault also stops the browser turning the key into a click on\n\t\t\t\t// the option button that is about to be removed.\n\t\t\t\tevent.preventDefault();\n\t\t\t\tselect(activeIndex);\n\t\t\t\tbreak;\n\t\t\tcase \"Escape\":\n\t\t\t\tevent.preventDefault();\n\t\t\t\tcloseMenu(true);\n\t\t\t\tbreak;\n\t\t\tcase \"Tab\":\n\t\t\t\t// Focus is leaving of its own accord: the menu steps aside and puts\n\t\t\t\t// focus back on the trigger, so the browser's own Tab — left to run —\n\t\t\t\t// continues from the picker rather than from the top of the document.\n\t\t\t\tcloseMenu(true);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\tuseEffect(() => {\n\t\tif (!open) return;\n\t\t// mousedown rather than click: the menu has to be gone before the press\n\t\t// lands, or a press that starts outside and finishes on the trigger closes\n\t\t// and reopens in one gesture.\n\t\tconst onPointerDown = (event: MouseEvent) => {\n\t\t\tconst target = event.target as Node | null;\n\t\t\tif (target && (menuLive.current?.contains(target) || triggerLive.current?.contains(target)))\n\t\t\t\treturn;\n\t\t\t// No focus return: the press is already moving focus somewhere else.\n\t\t\tcloseMenu(false);\n\t\t};\n\t\t// Capture, so a surface that swallows mousedown on the way up cannot pin the\n\t\t// menu open. Registered only while open, and retracted the moment it closes.\n\t\tdocument.addEventListener(\"mousedown\", onPointerDown, true);\n\t\treturn () => document.removeEventListener(\"mousedown\", onPointerDown, true);\n\t}, [open, closeMenu, menuLive, triggerLive]);\n\n\t// A layout effect, so the first painted frame of the opened menu already has\n\t// focus inside the listbox rather than still on the trigger (contract §4).\n\tuseIsomorphicLayoutEffect(() => {\n\t\tif (!open) return;\n\t\t// Focus moves into the listbox instead of staying on the trigger: the\n\t\t// element pointing at the active option with `aria-activedescendant` must be\n\t\t// the element that actually holds focus.\n\t\tmenuEl?.focus();\n\t}, [open, menuEl]);\n\n\t// Reads the switch, writes only the menu: a composer that goes dark mid-pick\n\t// takes its menu down with it.\n\tuseEffect(() => {\n\t\tif (isDisabled) setOpen(false);\n\t}, [isDisabled]);\n\n\t// The anchor is read through a getter so the float re-measures the trigger on\n\t// each scroll and resize tick instead of holding a rect that went stale the\n\t// moment the page moved. Identity-stable, so the float is never re-synced for\n\t// the getter alone.\n\tconst anchor = useCallback(\n\t\t(): FloatRect | null => triggerLive.current?.getBoundingClientRect() ?? null,\n\t\t[triggerLive]\n\t);\n\t// In the DOM only while it is on screen, so the hook is handed `null` — and\n\t// does nothing at all — for as long as the menu is closed.\n\tuseFloat(menuEl, { anchor, placement: \"top-start\", offset: 6 });\n\n\treturn (\n\t\t<>\n\t\t\t<button\n\t\t\t\t{...rest}\n\t\t\t\tref={triggerRef}\n\t\t\t\ttype=\"button\"\n\t\t\t\tclassName={cn(\n\t\t\t\t\t\"ft-composer-model text-foreground/70 hover:bg-muted hover:text-foreground inline-flex max-w-full cursor-pointer items-center gap-1.5 rounded-md px-2 py-1 font-mono text-xs transition-colors focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\",\n\t\t\t\t\tclassName\n\t\t\t\t)}\n\t\t\t\tdisabled={isDisabled}\n\t\t\t\tdata-open={open ? \"\" : undefined}\n\t\t\t\taria-haspopup=\"listbox\"\n\t\t\t\taria-expanded={open}\n\t\t\t\taria-controls={open ? menuId : undefined}\n\t\t\t\taria-label={selected ? `${label}: ${selected.label}` : label}\n\t\t\t\tonClick={handleTriggerClick}\n\t\t\t\tonKeyDown={handleTriggerKeydown}\n\t\t\t>\n\t\t\t\t<span className=\"truncate\">{selected?.label ?? label}</span>\n\t\t\t\t{selected?.badge ? (\n\t\t\t\t\t<span className=\"ft-composer-model-badge shrink-0 rounded px-1 py-px text-[0.625rem] tracking-wide\">\n\t\t\t\t\t\t{selected.badge}\n\t\t\t\t\t</span>\n\t\t\t\t) : null}\n\t\t\t\t<svg\n\t\t\t\t\tclassName=\"ft-composer-model-chevron size-3 shrink-0\"\n\t\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t>\n\t\t\t\t\t<path d=\"m4 6 4 4 4-4\" />\n\t\t\t\t</svg>\n\t\t\t</button>\n\n\t\t\t{/*\n\t\t\t\tIn the DOM only while it is on screen. A permanently mounted menu is a\n\t\t\t\tlist of every model on offer that every reader of the page has to step\n\t\t\t\tpast to reach the composer, once per composer.\n\t\t\t*/}\n\t\t\t{open ? (\n\t\t\t\t<div\n\t\t\t\t\tref={menuRef}\n\t\t\t\t\tid={menuId}\n\t\t\t\t\trole=\"listbox\"\n\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\taria-label={label}\n\t\t\t\t\taria-activedescendant={activeId}\n\t\t\t\t\tclassName=\"ft-composer-model-menu z-50 max-h-64 min-w-52 overflow-y-auto rounded-lg border p-1 shadow-lg outline-none\"\n\t\t\t\t\tonKeyDown={handleMenuKeydown}\n\t\t\t\t>\n\t\t\t\t\t{models.map((model, index) => (\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tA button carrying `role=\"option\"`: the row has to be clickable and\n\t\t\t\t\t\t\tit has to be an option, and starting from a button is what keeps\n\t\t\t\t\t\t\tthe press, the pointer cursor and the disabled semantics native.\n\t\t\t\t\t\t\t`tabindex=\"-1\"` keeps it out of the tab order, where the listbox\n\t\t\t\t\t\t\tdoes the walking.\n\t\t\t\t\t\t*/\n\t\t\t\t\t\t<button\n\t\t\t\t\t\t\tkey={`${model.id}#${index}`}\n\t\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\t\trole=\"option\"\n\t\t\t\t\t\t\ttabIndex={-1}\n\t\t\t\t\t\t\tid={optionId(index)}\n\t\t\t\t\t\t\taria-selected={model.id === selectedId}\n\t\t\t\t\t\t\tdata-active={index === activeIndex ? \"\" : undefined}\n\t\t\t\t\t\t\tclassName=\"ft-composer-model-option flex w-full cursor-pointer flex-col items-start gap-0.5 rounded-md px-2 py-1.5 text-left\"\n\t\t\t\t\t\t\tonClick={() => select(index)}\n\t\t\t\t\t\t\tonMouseEnter={() => setActiveIndex(index)}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<span className=\"flex w-full items-center gap-1.5\">\n\t\t\t\t\t\t\t\t<span className=\"truncate text-xs font-medium\">{model.label}</span>\n\t\t\t\t\t\t\t\t{model.badge ? (\n\t\t\t\t\t\t\t\t\t<span className=\"ft-composer-model-badge shrink-0 rounded px-1 py-px text-[0.625rem] tracking-wide\">\n\t\t\t\t\t\t\t\t\t\t{model.badge}\n\t\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t\t{model.id === selectedId ? (\n\t\t\t\t\t\t\t\t\t<svg\n\t\t\t\t\t\t\t\t\t\tclassName=\"ml-auto size-3 shrink-0\"\n\t\t\t\t\t\t\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\t\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\t\t\t\t\tstrokeWidth=\"2\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t\t\t<path d=\"m3 8.5 3.5 3.5L13 5\" />\n\t\t\t\t\t\t\t\t\t</svg>\n\t\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t{model.description ? (\n\t\t\t\t\t\t\t\t<span className=\"ft-composer-model-description text-[0.6875rem] leading-snug\">\n\t\t\t\t\t\t\t\t\t{model.description}\n\t\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t) : null}\n\t\t\t\t\t\t</button>\n\t\t\t\t\t))}\n\t\t\t\t</div>\n\t\t\t) : null}\n\t\t</>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerModelPicker.tsx"},{"path":"react/src/components/composer/ComposerSubmit.tsx","content":"\"use client\";\n\nimport { useContext } from \"react\";\nimport type { ComponentPropsWithoutRef, MouseEvent, ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\nimport { COMPOSER_CONTEXT_KEY } from \"./types\";\n\n/**\n * Props for ComposerSubmit\n */\ninterface ComposerSubmitOwnProps {\n\t/** Accessible name while the composer is idle. */\n\tlabel?: string;\n\t/** Accessible name while a response is streaming. */\n\tstopLabel?: string;\n\t/** Replaces the built-in icon. Rendered in both the send and the stop state. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * Native `<button>` attributes and listeners are forwarded to the button; the managed\n * ARIA, state and structural attributes stay the component's.\n */\nexport interface ComposerSubmitProps\n\textends\n\t\tOmit<ComponentPropsWithoutRef<\"button\">, keyof ComposerSubmitOwnProps>,\n\t\tComposerSubmitOwnProps {}\n\n/** The send button, which becomes a stop button for as long as a response is arriving. */\nexport function ComposerSubmit({\n\tlabel = \"Send\",\n\tstopLabel = \"Stop\",\n\tchildren,\n\tclassName,\n\tonClick,\n\t...rest\n}: ComposerSubmitProps) {\n\t// Undefined when the button is used outside a Composer: it then renders as a\n\t// permanently disabled button rather than throwing.\n\tconst composer = useContext(COMPOSER_CONTEXT_KEY);\n\n\tconst streaming = composer?.streaming ?? false;\n\tconst empty =\n\t\t(composer?.value.current ?? \"\").trim() === \"\" &&\n\t\t(composer?.attachments.current.length ?? 0) === 0;\n\t// A stop button with nothing to call is a lie, so it goes grey — which is also\n\t// what happens with no context at all.\n\t// `composer.stop` is always a function — the root publishes one whether or not\n\t// a consumer passed `onStop` — so the context reports separately whether there\n\t// is anything behind it.\n\tconst stoppable = composer?.stoppable ?? false;\n\t// Outside a composer both branches land on disabled: there is nothing to stop,\n\t// and an absent draft is an empty one.\n\tconst isDisabled = (composer?.disabled ?? false) || (streaming ? !stoppable : empty);\n\n\tconst name = streaming ? stopLabel : label;\n\n\treturn (\n\t\t<button\n\t\t\t{...rest}\n\t\t\ttype={streaming ? \"button\" : \"submit\"}\n\t\t\tclassName={cn(\n\t\t\t\t\"ft-composer-submit bg-foreground text-background hover:bg-foreground/90 focus-visible:ring-ring inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-lg transition-colors focus-visible:ring-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40\",\n\t\t\t\tclassName\n\t\t\t)}\n\t\t\tdisabled={isDisabled}\n\t\t\taria-label={name}\n\t\t\ttitle={name}\n\t\t\tonClick={(event: MouseEvent<HTMLButtonElement>) => {\n\t\t\t\tif (isDisabled) return;\n\t\t\t\tif (streaming) composer?.stop();\n\t\t\t\tonClick?.(event);\n\t\t\t}}\n\t\t>\n\t\t\t{children ??\n\t\t\t\t(streaming ? (\n\t\t\t\t\t<svg viewBox=\"0 0 16 16\" className=\"size-3.5\" aria-hidden=\"true\">\n\t\t\t\t\t\t<rect x=\"3\" y=\"3\" width=\"10\" height=\"10\" rx=\"2\" fill=\"currentColor\" />\n\t\t\t\t\t</svg>\n\t\t\t\t) : (\n\t\t\t\t\t<svg\n\t\t\t\t\t\tviewBox=\"0 0 16 16\"\n\t\t\t\t\t\tclassName=\"size-4\"\n\t\t\t\t\t\tfill=\"none\"\n\t\t\t\t\t\tstroke=\"currentColor\"\n\t\t\t\t\t\tstrokeWidth=\"1.5\"\n\t\t\t\t\t\tstrokeLinecap=\"round\"\n\t\t\t\t\t\tstrokeLinejoin=\"round\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<path d=\"M14 2 2 6.8l4.6 2.6L9.2 14z\" />\n\t\t\t\t\t\t<path d=\"M14 2 6.6 9.4\" />\n\t\t\t\t\t</svg>\n\t\t\t\t))}\n\t\t</button>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerSubmit.tsx"},{"path":"react/src/components/composer/ComposerToolbar.tsx","content":"\"use client\";\n\nimport type { ReactNode } from \"react\";\n\nimport { cn } from \"../../utils\";\n\n/**\n * Props for ComposerToolbar\n */\nexport interface ComposerToolbarProps {\n\t/** The controls on the rail, laid out left to right in source order. */\n\tchildren?: ReactNode;\n\t/** Additional CSS classes */\n\tclassName?: string;\n}\n\n/**\n * The bottom rail of a composer: one row, one gap, no opinions about what sits\n * on it. A model picker, an attach button and a send button are all just\n * children here, laid out left to right in source order.\n *\n * The trailing spacer convention\n * ------------------------------\n * The rail lays everything out from the left. Anything that belongs on the\n * right — the send button, a token meter — is pushed there by a spacer the\n * integrator writes into `children`, rather than by a second slot this\n * component would have to invent:\n *\n * ```tsx\n * <ComposerToolbar>\n *   <ComposerModelPicker models={models} />\n *   <div className=\"flex-1\" />\n *   <ComposerSubmit />\n * </ComposerToolbar>\n * ```\n *\n * One flexible box, any number of groups, and the split stays visible in the\n * consumer's markup instead of being buried in ours.\n *\n * Deliberately not `role=\"toolbar\"`: that role promises arrow-key roving focus\n * between its controls, and a row that announces itself as a toolbar without\n * implementing that is worse for a keyboard user than a plain row of tab stops.\n */\nexport function ComposerToolbar({ children, className }: ComposerToolbarProps) {\n\treturn (\n\t\t<div className={cn(\"ft-composer-toolbar flex items-center gap-1\", className)}>{children}</div>\n\t);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/ComposerToolbar.tsx"},{"path":"react/src/components/composer/caret.ts","content":"\"use client\";\n\n/**\n * Caret arithmetic and caret geometry for the composer's trigger menus.\n *\n * Two jobs, one file, because a completion menu needs both at the same moment:\n * *what* the reader is typing after a trigger character, and *where* on screen\n * that trigger sits so the menu can be pinned to it.\n *\n * `findTokenStart` and `findTriggerToken` are pure — no DOM, no globals,\n * exhaustively testable. `measureCaretRect` has to touch the DOM, since a\n * textarea exposes no caret geometry of its own: the only way to find out where\n * character *n* landed is to lay the same text out again in an element that can\n * be measured.\n */\n\nimport type { FloatRect } from \"../../internals/float\";\n\n/** The trigger token the caret currently sits in. */\nexport interface TriggerToken {\n\t/** Index of the trigger character itself, within the value. */\n\tstart: number;\n\t/** Everything typed between the trigger and the caret. Empty right after the trigger. */\n\tquery: string;\n}\n\n/** True when `index` falls between the two halves of a surrogate pair. */\nfunction splitsPair(value: string, index: number): boolean {\n\tconst high = value.charCodeAt(index - 1);\n\tconst low = value.charCodeAt(index);\n\treturn high >= 0xd800 && high <= 0xdbff && low >= 0xdc00 && low <= 0xdfff;\n}\n\n/**\n * The caret, brought inside the value and onto a code point boundary.\n *\n * Rounding a mid-pair caret down is what keeps every index the token search\n * hands back — and therefore every slice taken from it — from cutting a\n * character in half.\n */\nfunction tokenEnd(value: string, caret: number): number {\n\tconst end = Math.max(0, Math.min(caret, value.length));\n\treturn end > 0 && end < value.length && splitsPair(value, end) ? end - 1 : end;\n}\n\n/**\n * Where the trigger token under `caret` starts, or -1 when there is none.\n *\n * This is *the* definition of a trigger token, and the only one: both the menus\n * that open on a token and the `insertText` that replaces one measure it here,\n * so the span a menu matched and the span a completion overwrites can never\n * drift apart.\n *\n * The token is the whitespace-delimited run ending at the caret, and it counts\n * as a trigger only when it opens with something other than a letter or a digit.\n * That deliberately avoids a hardcoded trigger vocabulary: `/`, `@`, `#`, `:`\n * and whatever a menu invents all behave the same, while an ordinary word — or a\n * `/` sitting mid-word, as in a path fragment — does not.\n *\n * The walk counts code points rather than code units. Reading one unit would\n * mistake the leading half of an astral character for a non-alphanumeric and\n * swallow the whole word: `𝐀𝐁` is two letters, not a trigger token, and a caret\n * the platform dropped between two halves belongs to the character as a whole.\n */\nexport function findTokenStart(value: string, caret: number): number {\n\tconst end = tokenEnd(value, caret);\n\n\tlet start = end;\n\twhile (start > 0) {\n\t\tconst step = splitsPair(value, start - 1) ? 2 : 1;\n\t\tif (/\\s/u.test(value.slice(start - step, start))) break;\n\t\tstart -= step;\n\t}\n\n\t// The caret sits at the very start of a word boundary: there is no token.\n\tif (start === end) return -1;\n\tconst first = String.fromCodePoint(value.codePointAt(start) as number);\n\treturn /[\\p{L}\\p{N}]/u.test(first) ? -1 : start;\n}\n\n/**\n * The trigger token under `caret`, or `null` when there is none.\n *\n * The same run `findTokenStart` reports, narrowed to the menus that own it: the\n * run has to open with the exact `trigger` string. So `/de` and `@jo` open their\n * own menu and nobody else's; `src/lib` opens neither, because its slash is\n * mid-word, and neither does a caret sitting before the trigger it would\n * otherwise match.\n *\n * The token deliberately ends *at the caret* rather than at the end of the word:\n * completing `@jo|rdan` should search for `jo`, which is what the reader has\n * actually committed to.\n */\nexport function findTriggerToken(\n\tvalue: string,\n\tcaret: number,\n\ttrigger: string\n): TriggerToken | null {\n\tif (trigger === \"\") return null;\n\n\tconst end = tokenEnd(value, caret);\n\tconst start = findTokenStart(value, end);\n\tif (start < 0) return null;\n\n\tconst token = value.slice(start, end);\n\t// Also covers a trigger only half typed: `:` is not yet `::`.\n\tif (!token.startsWith(trigger)) return null;\n\treturn { start, query: token.slice(trigger.length) };\n}\n\n/** Copied onto the mirror so it wraps text exactly the way the textarea does. */\nconst MIRRORED_STYLES = [\n\t\"border-bottom-width\",\n\t\"border-left-width\",\n\t\"border-right-width\",\n\t\"border-top-width\",\n\t\"direction\",\n\t\"font-family\",\n\t\"font-size\",\n\t\"font-stretch\",\n\t\"font-style\",\n\t\"font-variant\",\n\t\"font-weight\",\n\t\"letter-spacing\",\n\t\"line-height\",\n\t\"padding-bottom\",\n\t\"padding-left\",\n\t\"padding-right\",\n\t\"padding-top\",\n\t\"tab-size\",\n\t\"text-align\",\n\t\"text-indent\",\n\t\"text-transform\",\n\t\"word-spacing\",\n] as const;\n\n/** Used when the platform reports no usable line-height, e.g. under jsdom. */\nconst FALLBACK_LINE_HEIGHT = 20;\n/** Multiplier applied to the font size when only that is measurable. */\nconst NORMAL_LINE_RATIO = 1.5;\n\nfunction px(input: string): number {\n\tconst parsed = Number.parseFloat(input);\n\treturn Number.isFinite(parsed) ? parsed : 0;\n}\n\n/**\n * Where character `index` of a textarea sits, in viewport coordinates.\n *\n * The mirror-div technique: a hidden div inherits the textarea's font, padding,\n * borders and content width, receives the text up to `index`, and then a marker\n * span holding the rest. The span's offset is the caret's offset, because the\n * two layouts are the same layout. The rest is bookkeeping — the textarea's own\n * position, its border, and how far it has been scrolled.\n *\n * Returns a zero-width rect one line tall, which is exactly what a float wants\n * as a virtual anchor. Under jsdom nothing has a layout, so everything measures\n * zero; the shape still holds and nothing throws.\n */\nexport function measureCaretRect(textarea: HTMLTextAreaElement, index: number): FloatRect {\n\tconst box = textarea.getBoundingClientRect();\n\tconst doc = textarea.ownerDocument;\n\tconst win = doc.defaultView;\n\tif (!win) return { x: box.left, y: box.top, width: 0, height: box.height };\n\n\tconst style = win.getComputedStyle(textarea);\n\tconst declaredLine = Number.parseFloat(style.lineHeight);\n\tconst fontSize = Number.parseFloat(style.fontSize);\n\tconst line =\n\t\tNumber.isFinite(declaredLine) && declaredLine > 0\n\t\t\t? declaredLine\n\t\t\t: Number.isFinite(fontSize) && fontSize > 0\n\t\t\t\t? fontSize * NORMAL_LINE_RATIO\n\t\t\t\t: FALLBACK_LINE_HEIGHT;\n\n\tconst mirror = doc.createElement(\"div\");\n\tfor (const property of MIRRORED_STYLES) {\n\t\tmirror.style.setProperty(property, style.getPropertyValue(property));\n\t}\n\t// Content-box on purpose: `clientWidth` already excludes the borders and the\n\t// scrollbar, so subtracting the padding leaves the exact width the text wraps\n\t// inside — no double counting whichever box model the textarea itself uses.\n\tconst inner = textarea.clientWidth || box.width;\n\tmirror.style.boxSizing = \"content-box\";\n\tmirror.style.width = `${Math.max(0, inner - px(style.paddingLeft) - px(style.paddingRight))}px`;\n\tmirror.style.height = \"auto\";\n\tmirror.style.position = \"absolute\";\n\tmirror.style.top = \"0\";\n\tmirror.style.left = \"-9999px\";\n\tmirror.style.visibility = \"hidden\";\n\tmirror.style.overflow = \"hidden\";\n\t// A textarea wraps on soft breaks and breaks long words rather than scrolling\n\t// sideways; the mirror has to agree or the two layouts drift apart.\n\tmirror.style.whiteSpace = \"pre-wrap\";\n\tmirror.style.overflowWrap = \"break-word\";\n\n\tconst value = textarea.value;\n\tconst clamped = Math.max(0, Math.min(index, value.length));\n\tmirror.textContent = value.slice(0, clamped);\n\n\tconst marker = doc.createElement(\"span\");\n\t// The remainder, so the span occupies a real line box; a trailing caret has\n\t// nothing left to hold it open, hence the dot.\n\tmarker.textContent = value.slice(clamped) || \".\";\n\tmirror.appendChild(marker);\n\n\tdoc.body.appendChild(mirror);\n\tlet top = 0;\n\tlet left = 0;\n\ttry {\n\t\ttop = marker.offsetTop;\n\t\tleft = marker.offsetLeft;\n\t} finally {\n\t\tmirror.remove();\n\t}\n\n\t// `offsetTop` is measured from the mirror's padding edge, so it already carries\n\t// the padding; only the border is left to add back.\n\treturn {\n\t\tx: box.left + px(style.borderLeftWidth) + left - textarea.scrollLeft,\n\t\ty: box.top + px(style.borderTopWidth) + top - textarea.scrollTop,\n\t\twidth: 0,\n\t\theight: line,\n\t};\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/caret.ts"},{"path":"react/src/components/composer/composer-attachment.css","content":".ft-composer-attachment {\n\tborder-radius: var(--ft-composer-attachment-radius, 0.5rem);\n\tborder: 1px solid\n\t\tvar(--ft-composer-attachment-border, color-mix(in oklab, currentColor 14%, transparent));\n\tbackground: var(--ft-composer-attachment-bg, color-mix(in oklab, currentColor 5%, transparent));\n}\n\n/*\n * The failed chip is read at the point of use through two hooks: the\n * component's own `--ft-composer-attachment-error`, then the `--ft-status-*`\n * vocabulary the whole AI family shares, then the literal. Setting either\n * anywhere up the tree retints it without a specificity fight, and the shared\n * one recolours every failure in the family at once.\n */\n.ft-composer-attachment.ft-failed {\n\t--ft-attachment-failed: var(\n\t\t--ft-composer-attachment-error,\n\t\tvar(--ft-status-error, light-dark(oklch(0.5 0.19 25), oklch(0.7 0.18 25)))\n\t);\n\tborder-color: color-mix(in oklab, var(--ft-attachment-failed) 45%, transparent);\n\tbackground: color-mix(in oklab, var(--ft-attachment-failed) 10%, transparent);\n\tcolor: var(--ft-attachment-failed);\n}\n\n/* Pinned to the chip's own bottom edge: the upload belongs to this file, and\n   the row it sits in must not grow a line while the bar is up. */\n.ft-composer-attachment-track {\n\tposition: absolute;\n\tright: 0.25rem;\n\tbottom: 0.1875rem;\n\tleft: 0.25rem;\n\tdisplay: block;\n\theight: var(--ft-composer-attachment-track-height, 2px);\n\toverflow: hidden;\n\tborder-radius: 9999px;\n\tbackground: var(--ft-composer-attachment-track, color-mix(in oklab, currentColor 15%, transparent));\n}\n\n.ft-composer-attachment-bar {\n\tdisplay: block;\n\theight: 100%;\n\tborder-radius: inherit;\n\tbackground: var(\n\t\t--ft-composer-attachment-progress,\n\t\tvar(--ft-status-running, light-dark(oklch(0.5 0.18 265), oklch(0.72 0.15 265)))\n\t);\n}\n\n/*\n * The only thing that moves is the bar catching up to its width. With this\n * rule gone it simply lands there, which is the same information without the\n * travel — nothing here needs a reduced-motion counterpart.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-composer-attachment-bar {\n\t\ttransition: width var(--ft-composer-attachment-fill-duration, 220ms)\n\t\t\tcubic-bezier(0.4, 0, 0.2, 1);\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer-attachment.css"},{"path":"react/src/components/composer/composer-attachments.css","content":"/* Visually hidden rather than `display: none`: a clipped input still opens its\n   picker on every engine, and still belongs to the form it sits in. */\n.ft-composer-file-input {\n\tposition: absolute;\n\twidth: 1px;\n\theight: 1px;\n\tpadding: 0;\n\tmargin: -1px;\n\toverflow: hidden;\n\tclip-path: inset(50%);\n\twhite-space: nowrap;\n\tborder: 0;\n}\n\n/*\n * The entrance belongs to the row, not to the chip: a chip appears because\n * this list grew one. With the rule gone a new chip is simply there, which is\n * the same information without the travel.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-composer-attachment-slot {\n\t\tanimation: ft-composer-attachment-enter var(--ft-composer-attachment-enter-duration, 180ms)\n\t\t\tcubic-bezier(0.16, 1, 0.3, 1) both;\n\t}\n\n\t@keyframes ft-composer-attachment-enter {\n\t\tfrom {\n\t\t\topacity: 0;\n\t\t\ttransform: translateY(4px) scale(0.96);\n\t\t}\n\t\tto {\n\t\t\topacity: 1;\n\t\t\ttransform: none;\n\t\t}\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer-attachments.css"},{"path":"react/src/components/composer/composer-command-menu.css","content":".ft-composer-command-menu {\n\tz-index: var(--ft-composer-menu-z, 50);\n\tmin-width: var(--ft-composer-menu-min-width, 14rem);\n\tmax-width: min(var(--ft-composer-menu-max-width, 24rem), calc(100vw - 1rem));\n\tmax-height: var(--ft-composer-menu-max-height, 15rem);\n\toverflow-y: auto;\n\tgap: 0.0625rem;\n\tborder-radius: var(--ft-composer-menu-radius, 0.625rem);\n\tborder: 1px solid\n\t\tvar(--ft-composer-menu-border, color-mix(in oklab, currentColor 14%, transparent));\n\t/* Opaque on purpose: the menu covers the draft it is completing, and a\n\t   translucent one would leave the reader deciphering two texts at once. */\n\tbackground: var(--ft-composer-menu-bg, light-dark(oklch(1 0 0), oklch(0.21 0.01 264)));\n\tbox-shadow: var(--ft-composer-menu-shadow, 0 10px 30px -12px rgb(0 0 0 / 0.45));\n\tpadding: 0.25rem;\n\tscrollbar-width: thin;\n}\n\n/*\n * The active row is the one Enter would take, and it is set by the arrow keys\n * rather than by the pointer — so it is painted, not left to `:hover`. Hover\n * gets the same tint at half strength so the pointer still has a target.\n */\n.ft-composer-command {\n\tcolor: var(--ft-composer-menu-color, inherit);\n}\n\n.ft-composer-command:hover {\n\tbackground: var(--ft-composer-menu-hover, color-mix(in oklab, currentColor 6%, transparent));\n}\n\n/*\n * Eleven percent of the row's own colour is the strongest tint the secondary\n * text can still be read on. The description and the hint are `foreground/70`,\n * which lands at #5F5F5F over the tinted row on a light menu (4.5:1 needs\n * #767676 or darker there) and at #C2C2C2 on a dark one — 5.2:1 and 5.8:1,\n * both clear of the 4.5:1 a 12px caption owes. Pushing the tint further, or\n * putting the muted token back on these two spans, drops the description under\n * the threshold on exactly the row the reader is about to take.\n */\n.ft-composer-command.ft-active {\n\tbackground: var(--ft-composer-menu-active, color-mix(in oklab, currentColor 11%, transparent));\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-composer-command {\n\t\ttransition: background-color 120ms ease;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer-command-menu.css"},{"path":"react/src/components/composer/composer-input.css","content":".ft-composer-input {\n\t/* Kept out of the auto-grow arithmetic: scrollbar-gutter would change the\n\t   content box mid-measurement. */\n\tscrollbar-width: thin;\n\tcolor: var(--ft-composer-input-color, inherit);\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer-input.css"},{"path":"react/src/components/composer/composer-model-picker.css","content":"/*\n * The floating surface reads its colours at the point of use with a fallback\n * rather than declaring them on a root, so a value set anywhere up the tree —\n * a wrapper's `style`, a theme class, `:root` — wins without having to\n * out-specify these rules.\n */\n/*\n * The rail's controls sit on the composer's own tinted surface, not on the\n * page, so the ring has to be read against that surface rather than against\n * white. Fifty-five percent of the row's colour lands at #828282 on the light\n * composer (3.5:1) and #9E9E9E on the dark one (5.1:1); a 1px ring in the\n * theme's `--ring` token measured 2.4:1 there, under the 3:1 a control's focus\n * indicator owes. Two pixels rather than one for the same reason a thin ring\n * disappears against a tint: the ring is the only thing saying where focus is.\n */\n.ft-composer-model:focus-visible {\n\tbox-shadow: 0 0 0 2px\n\t\tvar(--ft-composer-control-ring, color-mix(in oklab, currentColor 55%, transparent));\n}\n\n.ft-composer-model-menu {\n\tmax-width: min(20rem, calc(100vw - 1rem));\n\tbackground: var(--ft-composer-menu-bg, var(--color-popover, canvas));\n\tborder-color: var(--ft-composer-menu-border, var(--color-border, currentColor));\n\tcolor: var(--ft-composer-menu-fg, var(--color-popover-foreground, inherit));\n}\n\n/*\n * One highlight for both roads into a row — the arrow keys and the pointer —\n * so the option Enter is about to take is the option under the cursor.\n */\n.ft-composer-model-option[data-active] {\n\tbackground: var(\n\t\t--ft-composer-option-active-bg,\n\t\tcolor-mix(in oklab, currentColor 10%, transparent)\n\t);\n}\n\n.ft-composer-model-description {\n\tcolor: var(--ft-composer-menu-muted, color-mix(in oklab, currentColor 60%, transparent));\n}\n\n/* Mixed from currentColor, so retinting the picker retints its pills with it. */\n.ft-composer-model-badge {\n\tbackground: var(--ft-composer-badge-bg, color-mix(in oklab, currentColor 14%, transparent));\n\ttext-transform: uppercase;\n}\n\n.ft-composer-model[data-open] .ft-composer-model-chevron {\n\ttransform: rotate(180deg);\n}\n\n/*\n * Both movements live entirely inside `no-preference`, so reduced motion is\n * not a second variant to keep in sync: the chevron flips and the menu appears,\n * already placed.\n */\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-composer-model-chevron {\n\t\ttransition: transform 150ms ease;\n\t}\n\n\t.ft-composer-model-menu {\n\t\tanimation: ft-composer-menu-in 120ms cubic-bezier(0.4, 0, 0.2, 1);\n\t}\n}\n\n@keyframes ft-composer-menu-in {\n\tfrom {\n\t\topacity: 0;\n\t\ttransform: translateY(4px);\n\t}\n\tto {\n\t\topacity: 1;\n\t\ttransform: none;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer-model-picker.css"},{"path":"react/src/components/composer/composer.css","content":"/*\n * The surface is a form, not a card: it reads as one input the whole row of\n * controls lives inside, which is why the focus ring belongs to the container\n * rather than to the textarea buried in it.\n */\n.ft-composer {\n\tborder-radius: var(--ft-composer-radius, 0.75rem);\n\tborder-color: var(--ft-composer-border, color-mix(in oklab, currentColor 14%, transparent));\n\tbackground: var(--ft-composer-bg, color-mix(in oklab, currentColor 4%, transparent));\n}\n\n.ft-composer:focus-within {\n\tborder-color: var(--ft-composer-border-focus, color-mix(in oklab, currentColor 32%, transparent));\n\tbox-shadow: 0 0 0 1px var(--ft-composer-ring, color-mix(in oklab, currentColor 22%, transparent));\n}\n\n.ft-composer-disabled {\n\topacity: var(--ft-composer-disabled-opacity, 0.6);\n}\n\n@media (prefers-reduced-motion: no-preference) {\n\t.ft-composer {\n\t\ttransition:\n\t\t\tborder-color 150ms ease,\n\t\t\tbox-shadow 150ms ease;\n\t}\n}\n","type":"registry:ui","target":"@ui/fancy/components/composer/composer.css"},{"path":"react/src/components/composer/index.ts","content":"\"use client\";\n\nexport { Composer } from \"./Composer\";\nexport type { ComposerProps } from \"./Composer\";\nexport { ComposerInput } from \"./ComposerInput\";\nexport type { ComposerInputProps } from \"./ComposerInput\";\nexport { ComposerSubmit } from \"./ComposerSubmit\";\nexport type { ComposerSubmitProps } from \"./ComposerSubmit\";\nexport { ComposerToolbar } from \"./ComposerToolbar\";\nexport type { ComposerToolbarProps } from \"./ComposerToolbar\";\nexport { ComposerModelPicker } from \"./ComposerModelPicker\";\nexport type { ComposerModelPickerProps } from \"./ComposerModelPicker\";\nexport { ComposerAttachments } from \"./ComposerAttachments\";\nexport type { ComposerAttachmentsProps } from \"./ComposerAttachments\";\nexport { ComposerAttachment } from \"./ComposerAttachment\";\nexport type { ComposerAttachmentProps } from \"./ComposerAttachment\";\nexport { ComposerCommandMenu } from \"./ComposerCommandMenu\";\nexport type { ComposerCommandMenuProps } from \"./ComposerCommandMenu\";\nexport { COMPOSER_CONTEXT_KEY } from \"./types\";\nexport type { ComposerContext } from \"./types\";\n","type":"registry:ui","target":"@ui/fancy/components/composer/index.ts"},{"path":"react/src/components/composer/types.ts","content":"\"use client\";\n\n/**\n * The contract between the composer root and its parts.\n *\n * `Composer` owns the draft — the text, the attachments, the textarea element —\n * and publishes a live, read-only view of it plus the handful of commands a part\n * may issue. Every part reads it through context instead of having six values\n * and four callbacks threaded back down as props, and every part must render\n * harmlessly (inert, no-op) when the context is absent, so a part dropped\n * outside a composer degrades instead of throwing.\n *\n * The root rebuilds the object on every render, which is what makes a part that\n * reads `value.current` re-render when the draft changes. The commands are\n * identity-stable and always call the root's latest logic, so a handle captured\n * once — a menu's long-lived listener, a test's probe — never goes stale.\n *\n * `textareaRef` is typed read-only because exactly one part writes it —\n * `ComposerInput`, which registers its own element through a documented cast to\n * the writable shape the root actually publishes. Everyone else reads. It is the\n * one member of the object that is NOT rebuilt per render: the root publishes a\n * single accessor pair backed by a ref, so a write lands immediately and still\n * wakes the parts that render off it.\n */\n\nimport { createContext } from \"react\";\n\nimport type { AttachmentData } from \"../../internals/ai-types\";\n\n/** What the root publishes. Parts read it; only the root writes it. */\nexport interface ComposerContext {\n\t/** The current draft text, exactly as typed — untrimmed. */\n\treadonly value: { readonly current: string };\n\t/** The attachments riding along with the draft. The consumer owns this list. */\n\treadonly attachments: { readonly current: AttachmentData[] };\n\t/** Nothing may be typed, sent, or attached. */\n\treadonly disabled: boolean;\n\t/** A response is arriving: send becomes stop. */\n\treadonly streaming: boolean;\n\t/**\n\t * Whether `stop()` reaches anyone. The root always publishes a callable\n\t * `stop`, so a control asking \"is there a handler behind it\" has to ask this\n\t * instead — otherwise a stop button offers itself with nothing to stop.\n\t */\n\treadonly stoppable: boolean;\n\t/** The live textarea, once `ComposerInput` has mounted one. */\n\treadonly textareaRef: { readonly current: HTMLTextAreaElement | null };\n\t/**\n\t * Whether this composer plays sound cues — forwarded from the root's own\n\t * `sound` prop. Parts read `composer?.sound ?? false`, so a part rendered\n\t * outside a `Composer` stays silent rather than throwing.\n\t */\n\treadonly sound?: boolean;\n\t/** Send the draft. No-ops while disabled, while streaming, and on an empty draft. */\n\tsubmit(): void;\n\t/** Interrupt the stream. No-ops unless the composer is streaming. */\n\tstop(): void;\n\t/** Replace the draft outright. */\n\tsetValue(next: string): void;\n\t/**\n\t * Splice `text` in at the caret. With `replaceTriggerToken`, the trigger token\n\t * the caret sits in — the whitespace-delimited run before it, when that run\n\t * opens with a non-alphanumeric character — is replaced instead, and the\n\t * completion closes with a trailing space.\n\t */\n\tinsertText(text: string, replaceTriggerToken?: boolean): void;\n\t/** Hand files to the consumer. It uploads them and pushes onto `attachments`. */\n\taddFiles(files: File[]): void;\n\t/** Drop one attachment from the draft by id. */\n\tremoveAttachment(id: string): void;\n}\n\n/**\n * The context the parts read to find their root.\n *\n * The source publishes it under a `Symbol` context key; React's own context\n * object plays that role here, so the exported name is kept and the value is a\n * `React.Context` rather than a symbol:\n *\n * ```tsx\n * const composer = useContext(COMPOSER_CONTEXT_KEY);\n * ```\n *\n * Read it as optional — a part rendered outside a `Composer` gets `undefined`\n * rather than throwing.\n */\nexport const COMPOSER_CONTEXT_KEY = createContext<ComposerContext | undefined>(undefined);\nCOMPOSER_CONTEXT_KEY.displayName = \"ComposerContext\";\n","type":"registry:ui","target":"@ui/fancy/components/composer/types.ts"}],"meta":{"license":"MIT","package":"fancy-ui-react","version":"0.7.0","source":"https://fancy-ui.rama.app/docs/components/composer?framework=react"}}