{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-composer",
  "title": "Chat composer",
  "description": "the arrow sends · `busy` turns Send into Stop · `onAttach` turns on attachments (paperclip, chips)",
  "dependencies": [
    "lucide-react-native@^1.21.0"
  ],
  "registryDependencies": [
    "@kit-native/attachment-chip",
    "@kit-native/icon",
    "@kit-native/lib-haptics",
    "@kit-native/lib-utils",
    "@kit-native/text"
  ],
  "files": [
    {
      "path": "components/kit/chat-composer.tsx",
      "content": "import { AttachmentChip, type Attachment } from '@/components/kit/attachment-chip';\nimport { Icon } from '@/components/ui/icon';\nimport { useFontStyle } from '@/components/ui/text';\nimport { haptic } from '@/lib/haptics';\nimport { cn } from '@/lib/utils';\nimport { ArrowUpIcon, PaperclipIcon, SquareIcon } from 'lucide-react-native';\nimport * as React from 'react';\nimport { Platform, Pressable, TextInput, View } from 'react-native';\n\ntype Props = {\n  /** The message, and any files attached to it (an empty array when attachments are off or none). */\n  onSend: (text: string, files: Attachment[]) => void;\n  /** While the assistant answers: the send button becomes Stop. */\n  busy?: boolean;\n  onStop?: () => void;\n  placeholder?: string;\n  /**\n   * Turns on attachments: a paperclip button whose press returns the files to attach (from a picker, or\n   * mock files in a prototype). They show as chips to review and remove before sending.\n   */\n  onAttach?: () => Attachment[] | undefined | Promise<Attachment[] | undefined>;\n  /** At most this many files per message (default 5). */\n  maxFiles?: number;\n  className?: string;\n};\n\n/** Where the user writes. The arrow sends (Enter on a hardware keyboard on web; Shift+Enter breaks a line). */\nexport function ChatComposer({ onSend, busy, onStop, placeholder = 'Ask anything', onAttach, maxFiles = 5, className }: Props) {\n  const [value, setValue] = React.useState('');\n  const [files, setFiles] = React.useState<Attachment[]>([]);\n  const font = useFontStyle('text-base');\n\n  const attach = async () => {\n    const picked = await onAttach?.();\n    if (picked?.length) setFiles((fs) => [...fs, ...picked].slice(0, maxFiles));\n  };\n\n  const canSend = !!value.trim() || files.length > 0;\n  const send = () => {\n    if (!canSend || busy) return;\n    haptic('light');\n    onSend(value.trim(), files);\n    setValue('');\n    setFiles([]);\n  };\n\n  return (\n    <View className={cn('border-input bg-card gap-2 rounded-2xl border p-2', className)}>\n      {files.length ? (\n        <View accessibilityLabel=\"Attachments\" className=\"flex-row flex-wrap gap-2 px-1 pt-1\">\n          {files.map((f, i) => (\n            <AttachmentChip key={`${f.name}-${i}`} file={f} onRemove={() => setFiles((fs) => fs.filter((_, j) => j !== i))} />\n          ))}\n        </View>\n      ) : null}\n      <View className=\"flex-row items-end gap-1\">\n        {onAttach ? (\n          <Pressable\n            onPress={attach}\n            disabled={files.length >= maxFiles}\n            accessibilityRole=\"button\"\n            accessibilityLabel=\"Attach files\"\n            className={cn('active:bg-accent size-10 items-center justify-center rounded-full', files.length >= maxFiles && 'opacity-50')}>\n            <Icon as={PaperclipIcon} size={18} className=\"text-muted-foreground\" />\n          </Pressable>\n        ) : null}\n        <TextInput\n          value={value}\n          onChangeText={setValue}\n          placeholder={placeholder}\n          placeholderClassName=\"text-muted-foreground\"\n          accessibilityLabel=\"Message\"\n          multiline\n          // One line to start on web (rows), growing with the text; native grows on its own.\n          numberOfLines={Platform.OS === 'web' ? 1 : undefined}\n          submitBehavior=\"newline\"\n          onKeyPress={(e) => {\n            // Web only: Enter sends, Shift+Enter adds a line. A phone keyboard's return key always breaks a line.\n            const ev = e.nativeEvent as { key: string; shiftKey?: boolean };\n            if (Platform.OS === 'web' && ev.key === 'Enter' && !ev.shiftKey) {\n              e.preventDefault();\n              send();\n            }\n          }}\n          className={cn('text-foreground max-h-40 min-h-10 flex-1 px-2 py-2.5 text-base', Platform.select({ web: 'field-sizing-content resize-none outline-none' }))}\n          style={font}\n        />\n        {busy ? (\n          <Pressable onPress={onStop} accessibilityRole=\"button\" accessibilityLabel=\"Stop answering\" className=\"bg-secondary active:bg-secondary/80 size-10 items-center justify-center rounded-full\">\n            <Icon as={SquareIcon} size={14} className=\"text-secondary-foreground\" />\n          </Pressable>\n        ) : (\n          <Pressable\n            onPress={send}\n            disabled={!canSend}\n            accessibilityRole=\"button\"\n            accessibilityLabel=\"Send\"\n            accessibilityState={{ disabled: !canSend }}\n            className={cn('bg-primary active:bg-primary/90 size-10 items-center justify-center rounded-full', !canSend && 'opacity-50')}>\n            <Icon as={ArrowUpIcon} size={18} className=\"text-primary-foreground\" />\n          </Pressable>\n        )}\n      </View>\n    </View>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/kit/chat-composer.tsx"
    }
  ],
  "meta": {
    "exports": [
      "ChatComposer"
    ],
    "api": "<ChatComposer onSend={(text, files) => …} busy? onStop? placeholder? onAttach? maxFiles? />"
  },
  "docs": "Run `npx expo install --fix` afterwards so Expo SDK packages match your SDK. Needs the kit theme (`@kit-native/theme`) and `KitThemeProvider` from `@kit-native/lib-theme-context` at the root.",
  "categories": [
    "ai"
  ],
  "type": "registry:component"
}