{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "composer",
  "type": "registry:ui",
  "title": "Composer",
  "description": "The prompt input — a debossed well with send/stop, attachments, and Enter-to-send.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "https://seamui.dev/r/utils.json",
    "https://seamui.dev/r/motion.json",
    "https://seamui.dev/r/button.json",
    "https://seamui.dev/r/textarea.json",
    "https://seamui.dev/r/badge.json"
  ],
  "files": [
    {
      "path": "registry/seam/ui/composer.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { AnimatePresence, motion } from \"motion/react\"\nimport { ArrowUp, Square, X } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n  springs,\n  fades,\n  depth,\n  reduced,\n  useMounted,\n  useReducedMotion,\n} from \"@/lib/motion\"\nimport { Badge } from \"./badge\"\nimport { Button } from \"./button\"\nimport { Textarea } from \"./textarea\"\n\ntype ComposerStatus = \"ready\" | \"streaming\"\n\ntype ComposerContextValue = {\n  status: ComposerStatus\n  onStop?: () => void\n}\n\nconst ComposerContext = React.createContext<ComposerContextValue | null>(null)\n\nfunction useComposer() {\n  const ctx = React.useContext(ComposerContext)\n  if (!ctx) {\n    throw new Error(\"Composer parts must be used within <Composer>.\")\n  }\n  return ctx\n}\n\n// The signature seam control: the whole surface is a debossed well the user\n// acts into. The send key inside it is the embossed token that fires the\n// action — slot vs token, the design language in one component.\nfunction Composer({\n  className,\n  status = \"ready\",\n  onStop,\n  ...props\n}: React.ComponentProps<\"form\"> & {\n  status?: ComposerStatus\n  onStop?: () => void\n}) {\n  return (\n    <ComposerContext.Provider value={{ status, onStop }}>\n      <form\n        data-slot=\"composer\"\n        data-status={status}\n        className={cn(\n          \"bg-muted flex w-full flex-col gap-1.5 rounded-2xl squircle border border-border/60 p-2 shadow-well\",\n          \"focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/50\",\n          className\n        )}\n        {...props}\n      />\n    </ComposerContext.Provider>\n  )\n}\n\n// Reuses Textarea, stripped of its own border/shadow/background — the well\n// provides all three. Enter submits, Shift+Enter inserts a newline; submitting\n// never steals focus (requestSubmit leaves the caret in place).\nfunction ComposerTextarea({\n  className,\n  onKeyDown,\n  ...props\n}: React.ComponentProps<typeof Textarea>) {\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n    onKeyDown?.(e)\n    if (e.defaultPrevented) return\n    if (e.key === \"Enter\" && !e.shiftKey && !e.nativeEvent.isComposing) {\n      e.preventDefault()\n      e.currentTarget.form?.requestSubmit()\n    }\n  }\n\n  return (\n    <Textarea\n      data-slot=\"composer-textarea\"\n      rows={1}\n      onKeyDown={handleKeyDown}\n      className={cn(\n        // shadow-none! (important) is required: `shadow-well` is a custom\n        // utility twMerge can't dedupe against `shadow-none`, so both emit and\n        // the inset well would otherwise win the cascade — a double-well.\n        \"max-h-40 min-h-9 border-0 bg-transparent px-2 py-1.5 shadow-none!\",\n        \"focus-visible:border-0 focus-visible:ring-0\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction ComposerToolbar({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"composer-toolbar\"\n      className={cn(\"flex items-center gap-1\", className)}\n      {...props}\n    />\n  )\n}\n\nfunction ComposerTools({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"composer-tools\"\n      className={cn(\"flex items-center gap-0.5\", className)}\n      {...props}\n    />\n  )\n}\n\n// The embossed send key. While streaming it becomes a stop control (type\n// switches to button so it no longer submits the form); the icon crossfades\n// on opacity, which is identical under reduced motion.\nfunction ComposerSubmit({\n  className,\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  const { status, onStop } = useComposer()\n  const streaming = status === \"streaming\"\n\n  return (\n    <Button\n      data-slot=\"composer-submit\"\n      type={streaming ? \"button\" : \"submit\"}\n      size=\"icon\"\n      className={cn(\"ml-auto size-8 rounded-full\", className)}\n      onClick={streaming ? onStop : undefined}\n      aria-label={streaming ? \"Stop generating\" : \"Send message\"}\n      {...props}\n    >\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.span\n          key={streaming ? \"stop\" : \"send\"}\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          exit={{ opacity: 0 }}\n          transition={fades.fast}\n          className=\"flex items-center justify-center\"\n        >\n          {streaming ? <Square className=\"size-3.5\" /> : <ArrowUp />}\n        </motion.span>\n      </AnimatePresence>\n    </Button>\n  )\n}\n\nfunction ComposerAttachments({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"composer-attachments\"\n      className={cn(\"flex flex-wrap gap-1.5 px-1 pt-1\", className)}\n      {...props}\n    />\n  )\n}\n\n// A miniature embossed key resting inside the well. The remove affordance\n// dogfoods Button (ghost, resized) rather than a hand-rolled close element.\nfunction ComposerAttachment({\n  className,\n  children,\n  onRemove,\n  ...props\n}: React.ComponentProps<typeof Badge> & { onRemove?: () => void }) {\n  const reduceMotion = useReducedMotion()\n  const mounted = useMounted()\n\n  return (\n    <motion.div\n      layout={!reduceMotion}\n      // gate the moving entrance behind mount (SSR hydration safety); chips\n      // added after mount still animate in, and exit is unaffected.\n      initial={\n        mounted\n          ? reduceMotion\n            ? reduced.fadeIn.initial\n            : depth.overlay.initial\n          : false\n      }\n      animate={depth.overlay.animate}\n      exit={reduceMotion ? reduced.fadeIn.exit : depth.overlay.exit}\n      transition={reduceMotion ? fades.normal : springs.snappy}\n    >\n      <Badge\n        data-slot=\"composer-attachment\"\n        variant=\"secondary\"\n        className={cn(\"gap-1 py-1 pr-1 pl-2\", className)}\n        {...props}\n      >\n        <span className=\"truncate\">{children}</span>\n        {onRemove && (\n          <Button\n            type=\"button\"\n            variant=\"ghost\"\n            size=\"icon\"\n            onClick={onRemove}\n            aria-label=\"Remove attachment\"\n            className=\"-mr-0.5 size-4 rounded-full [&_svg:not([class*='size-'])]:size-3\"\n          >\n            <X />\n          </Button>\n        )}\n      </Badge>\n    </motion.div>\n  )\n}\n\nexport {\n  Composer,\n  ComposerTextarea,\n  ComposerToolbar,\n  ComposerTools,\n  ComposerSubmit,\n  ComposerAttachments,\n  ComposerAttachment,\n  useComposer,\n}\n",
      "type": "registry:ui",
      "target": "components/ui/composer.tsx"
    }
  ]
}