{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-input",
  "title": "Chat Input",
  "description": "A chat input component.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [
    "@lumi-ui/button",
    "@lumi-ui/dialog",
    "@lumi-ui/scroll-area"
  ],
  "files": [
    {
      "path": "registry/ai/chat-input.tsx",
      "content": "\"use client\";\n\nimport { CornerRightUp, Loader2, Paperclip } from \"lucide-react\";\nimport {\n  type HTMLMotionProps,\n  MotionConfig,\n  motion,\n  type Transition,\n} from \"motion/react\";\nimport * as React from \"react\";\nimport { cn } from \"@/registry/lib/utils\";\nimport { Button } from \"@/registry/ui/button\";\nimport { Tooltip, TooltipContent, TooltipTrigger } from \"@/registry/ui/tooltip\";\n\nconst chatInputTransition: Transition = {\n  bounce: 0.1,\n  duration: 0.4,\n  ease: [0.32, 0.72, 0, 1],\n  type: \"spring\",\n};\n\nconst textareaTransition: Transition = {\n  duration: 0.18,\n  ease: \"easeOut\",\n};\n\nfunction composeRefs<T>(\n  ...refs: Array<React.ForwardedRef<T> | undefined>\n): React.RefCallback<T> {\n  return (node) => {\n    for (const ref of refs) {\n      if (!ref) continue;\n      if (typeof ref === \"function\") {\n        ref(node);\n        continue;\n      }\n      ref.current = node;\n    }\n  };\n}\n\nfunction useChatInputAutoResize({\n  maxHeight = 220,\n  minHeight = 84,\n}: {\n  maxHeight?: number;\n  minHeight?: number;\n}) {\n  const textareaRef = React.useRef<HTMLTextAreaElement>(null);\n  const [height, setHeight] = React.useState(minHeight);\n\n  const recalculateHeight = React.useCallback(() => {\n    const textarea = textareaRef.current;\n    if (!textarea) return;\n\n    const isCaretAtEnd =\n      textarea.selectionStart === textarea.value.length &&\n      textarea.selectionEnd === textarea.value.length;\n\n    textarea.style.height = `${minHeight}px`;\n\n    const nextHeight = Math.max(\n      minHeight,\n      Math.min(textarea.scrollHeight, maxHeight),\n    );\n\n    textarea.style.height = `${nextHeight}px`;\n    if (isCaretAtEnd) {\n      textarea.scrollTop = textarea.scrollHeight;\n    }\n    setHeight((prev) => (prev === nextHeight ? prev : nextHeight));\n  }, [maxHeight, minHeight]);\n\n  return {\n    height,\n    recalculateHeight,\n    textareaRef,\n  };\n}\n\nfunction ChatInputRoot({ className, ...props }: HTMLMotionProps<\"div\">) {\n  return (\n    <motion.div\n      className={cn(\n        \"w-full overflow-hidden rounded-md border bg-background\",\n        className,\n      )}\n      layout\n      transition={chatInputTransition}\n      {...props}\n    />\n  );\n}\n\ninterface ChatInputTextareaProps\n  extends Omit<HTMLMotionProps<\"textarea\">, \"value\"> {\n  maxHeight?: number;\n  minHeight?: number;\n  onValueChange: (value: string) => void;\n  value: string;\n}\n\nfunction ChatInputTextarea({\n  className,\n  maxHeight = 220,\n  minHeight = 84,\n  onValueChange,\n  value,\n  ref,\n  ...props\n}: ChatInputTextareaProps) {\n  const { height, recalculateHeight, textareaRef } = useChatInputAutoResize({\n    maxHeight,\n    minHeight,\n  });\n\n  const mergedRef = composeRefs(textareaRef, ref);\n\n  React.useLayoutEffect(() => {\n    const isEmpty = value.trim().length === 0;\n\n    if (isEmpty) {\n      setTimeout(() => {\n        recalculateHeight();\n      }, 0);\n      return;\n    }\n\n    recalculateHeight();\n  }, [value, recalculateHeight]);\n\n  const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {\n    onValueChange(event.target.value);\n    requestAnimationFrame(recalculateHeight);\n  };\n\n  return (\n    <motion.textarea\n      animate={{ height }}\n      className={cn(\n        \"w-full resize-none overflow-y-auto overscroll-contain rounded-none border-none outline-none bg-transparent px-3 py-4 text-sm shadow-none focus-visible:outline-none will-change-height placeholder:text-muted-foreground\",\n        className,\n      )}\n      onChange={handleChange}\n      ref={mergedRef}\n      style={{ height }}\n      transition={textareaTransition}\n      value={value}\n      {...props}\n    />\n  );\n}\n\nfunction ChatInputToolbar({ className, ...props }: HTMLMotionProps<\"div\">) {\n  return (\n    <motion.div\n      className={cn(\n        \"flex items-center justify-between gap-2 bg-background px-2 py-2\",\n        className,\n      )}\n      layout\n      transition={chatInputTransition}\n      {...props}\n    />\n  );\n}\n\nfunction ChatInputActions({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      className={cn(\"flex items-center gap-2\", className)}\n      data-slot=\"chat-input-actions\"\n      {...props}\n    />\n  );\n}\n\nfunction ChatInputAttachButton({\n  children,\n  className,\n  size = \"icon-sm\",\n  type = \"button\",\n  variant = \"ghost\",\n  tooltipContent = \"Attach files\",\n  ...props\n}: React.ComponentProps<typeof Button> & {\n  tooltipContent?: React.ReactNode;\n}) {\n  return (\n    <Tooltip>\n      <TooltipTrigger\n        render={\n          <Button\n            className={cn(\"group\", className)}\n            size={size}\n            type={type}\n            variant={variant}\n            {...props}\n          >\n            {children ?? (\n              <Paperclip className=\"size-4 text-muted-foreground group-hover:text-foreground\" />\n            )}\n            <span className=\"sr-only\">Attach files</span>\n          </Button>\n        }\n      />\n      <TooltipContent showArrow={false} side=\"bottom\" sideOffset={4}>\n        {tooltipContent}\n      </TooltipContent>\n    </Tooltip>\n  );\n}\n\nfunction ChatInputSubmitButton({\n  ariaLabel = \"Send message\",\n  className,\n  disabled = true,\n  isLoading = false,\n  size = \"icon-sm\",\n  variant = \"secondary\",\n  ...props\n}: Omit<React.ComponentProps<typeof Button>, \"children\"> & {\n  ariaLabel?: string;\n  isLoading?: boolean;\n}) {\n  return (\n    <Button\n      aria-label={ariaLabel}\n      className={cn(\"rounded-lg\", className)}\n      disabled={disabled}\n      size={size}\n      variant={variant}\n      {...props}\n    >\n      {isLoading ? (\n        <Loader2 className=\"size-4 animate-spin\" />\n      ) : (\n        <CornerRightUp className=\"size-4\" />\n      )}\n    </Button>\n  );\n}\n\ninterface ChatInputProps extends Omit<HTMLMotionProps<\"div\">, \"onSubmit\"> {\n  leftActions?: React.ReactNode;\n  children?: React.ReactNode;\n}\n\nfunction ChatInput({\n  className,\n  leftActions,\n  children,\n  ...props\n}: ChatInputProps) {\n  return (\n    <MotionConfig transition={chatInputTransition}>\n      <ChatInputRoot className={className} {...props}>\n        {children}\n      </ChatInputRoot>\n    </MotionConfig>\n  );\n}\n\nexport {\n  ChatInputRoot,\n  ChatInputTextarea,\n  ChatInputToolbar,\n  ChatInputActions,\n  ChatInputSubmitButton,\n  ChatInputAttachButton,\n  // Composite component\n  ChatInput,\n};\n",
      "type": "registry:ui",
      "target": "components/ai-elements/chat-input.tsx"
    }
  ],
  "categories": [
    "ai"
  ],
  "type": "registry:ui"
}