AI prompt composer that starts as a compact pill and springs open into a full control surface. Auto-growing textarea, shimmer placeholder, drag-and-drop, staggered controls, attachment chips, pinned submit.
In the preview: click the pill to expand it, type a prompt and hit Enter to see the loading glow, drop or paste an image to attach a chip.
npx shadcn@latest add @visualsandbox-ui/composerRequires the one-time registry entry from Installation — or use the full URL with zero setup:
npx shadcn@latest add https://ui.visualsandbox.com/r/composer.jsonCompose the pieces you need inside <Composer>. Everything app-specific — uploads, model options, cost estimates, the submit button — comes from your side as props and children.
import {
Composer,
ComposerAttachments,
ComposerChip,
ComposerControl,
ComposerControls,
ComposerHint,
ComposerSubmit,
ComposerTextarea,
} from "@/components/visualsandbox-ui/composer";
<Composer
onSubmit={generate}
loading={generating}
disabled={!prompt.trim()}
keepExpanded={images.length > 0}
onFiles={uploadFiles}
dropHint="Drop image to add"
>
<ComposerHint>~$0.04</ComposerHint>
{images.length > 0 && (
<ComposerAttachments>
{images.map((src) => (
<ComposerChip key={src} onRemove={() => remove(src)}>
<img src={src} alt="" className="block h-full w-full object-cover" />
</ComposerChip>
))}
</ComposerAttachments>
)}
<ComposerTextarea
value={prompt}
onValueChange={setPrompt}
placeholder="Describe what you want to see…"
/>
<ComposerControls>
<ComposerControl>{/* any pill / select / toggle */}</ComposerControl>
</ComposerControls>
<ComposerSubmit />
</Composer>;| Piece | What it does |
|---|---|
Composer | Root card. Owns expand/collapse, outside-click collapse, drag-and-drop, loading glow, locked overlay. |
ComposerTextarea | Auto-growing prompt input. Shimmer placeholder, collapsed overflow fade, Enter submits, Escape collapses. |
ComposerSection | Expanded-only block (media slot rows, banners). Collapses to height 0 while staying mounted. |
ComposerAttachments | Chip row above the prompt. Render only when non-empty. |
ComposerChip | Square media chip with hover remove and an uploading spinner state. |
ComposerControls | Bottom pill row — slides up, children pop in with a stagger. |
ComposerControl | One staggered item inside ComposerControls. |
ComposerHint | Floating top-right label (cost estimate, token count). |
ComposerSubmit | Bottom-right pinned slot. Default arrow button; pass children to replace it. |
useComposer() | Context hook: { expanded, setExpanded, loading, disabled, submit, textareaRef } for custom pieces. |
| Prop | Type | Default | Notes |
|---|---|---|---|
onSubmit | () => void | — | Fired on Enter and by the default submit button. |
loading | boolean | false | Shows the glow ring; default submit button spins. |
disabled | boolean | false | Gates Enter-to-submit; hides the default submit button. |
keepExpanded | boolean | false | Force-open — attachments present, inline editor open, etc. |
onFiles | (files: File[]) => void | — | Dropped/pasted files. Omit to disable file drag-and-drop. |
dropTypes | string[] | [] | Extra DataTransfer types (custom drag payloads) that open the dropzone. |
onDropData | (dt: DataTransfer) => void | — | Handler for drops carrying one of dropTypes. |
dropDisabled | boolean | false | Kill the body dropzone without removing onFiles. |
dropHint | ReactNode | "Drop to add" | Label inside the drop overlay. |
dropIcon | ReactNode | <ImagePlus /> | Icon inside the drop overlay. |
glowClassName | string | "bg-primary" | Background class for the loading glow + ring. Gradients welcome. |
locked | boolean | false | Blocks interaction behind a blurred overlay. |
lockedContent | ReactNode | — | Message inside the locked overlay. |
collapsedMaxWidth | number | 560 | |
expandedMaxWidth | number | 960 | |
cardClassName | string | — | Re-skin the card surface (background, border). |
| Prop | Type | Default | Notes |
|---|---|---|---|
value | string | — | |
onValueChange | (value: string) => void | — | |
shimmer | boolean | true | Animated sheen sweeping across the placeholder. |
collapsedCap | number | 68 | Collapsed height cap in px (~3 lines), then the tail fades out. |
padClassName | string | — | Right-padding class reserving space under overlays. |
Plus all standard <textarea> props — your onKeyDown / onPaste run first; call preventDefault() to override the built-in Enter/Escape/paste-to-attach behavior.
layoutId across the collapse boundary. An open interrupting a still-running exit wedges Motion's projection tree and freezes every animation on the card.corner-shape: superellipse(1.6) tracks the animated box exactly; JS squircle masks lag a frame behind. Border radius is Motion-driven (33 ↔ 44px) because rounded-full can't interpolate.gap-0; spacing lives inside each block so it collapses with them.