shadimage
asanshay
componentchat-inputsource: ok · hit

Chat Input

A chat input component for a nice chatbot experience. Ready for useChat().

npx shadcn add "https://ds.asanshay.com/r/chat-input.json"
free

Where it goes

chat

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
ui-primitives@radix-ui/react-slot@radix-ui/react-select
iconslucide-react

Files4 · source cached 10/8/2026, 6:50:43 PM

registry/new-york/blocks/chat-input/chat-input.tsx
"use client";

import { Button } from "@/registry/new-york/blocks/button/button";
import { SendIcon, SquareIcon, UploadIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/registry/new-york/blocks/select/select";
import { Textarea } from "@/registry/new-york/blocks/textarea/textarea";

export default function ChatInput({
  title,
  placeholder = "Ask anything...",
  models,
  allowFileUpload,
  loading,
  handleSubmit,
  handleInputChange,
  input = "",
  className,
  model = "",
  onModelChange,
  stop,
}: {
  handleSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;
  handleInputChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
  input?: string;
  className?: string;
  model: string;
  onModelChange?: (model: string) => void;
  title?: string;
  placeholder?: string;
  models: { id: string; name: string; description: string }[];
  allowFileUpload?: boolean;
  loading?: boolean;
  stop?: () => void;
}) {
  return (
    <form
      onSubmit={handleSubmit}
      className={cn("w-full relative flex flex-col gap-4 items-center", className)}
    >
      {title && <h2 className="text-5xl font-medium tracking-tight">{title}</h2>}
      <Textarea
        name="prompt"
        value={input}
        onChange={handleInputChange}
        disabled={loading}
        autoComplete="off"
        placeholder={placeholder}
        className="w-full pb-20 pt-4 px-4 rounded-xl text-md bg-background disabled:opacity-100 disabled:cursor-not-allowed disabled:bg-background"
        onKeyDown={(e) => {
          if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
            e.preventDefault();
            e.currentTarget.form?.requestSubmit();
          }
        }}
      />
      <Button
        type={loading ? "button" : "submit"}
        onClick={() => {
          if (stop) {
            stop();
          }
        }}
        disabled={!stop && loading}
        variant="fancy"
        size="icon"
        className="absolute bottom-2 right-2"
      >
        {loading ? <SquareIcon className="w-4 h-4" /> : <SendIcon className="w-4 h-4" />}
      </Button>
      {onModelChange && models && (
        <div className="flex items-center gap-2 absolute bottom-0 left-0">
          <Select value={model} onValueChange={(value) => onModelChange(value)}>
            <SelectTrigger className="w-[200px] shadow-none pl-4 hover:bg-transparent hover:text-foreground focus-visible:ring-0 border-none bg-transparent text-xs text-muted-foreground dark:bg-transparent">
              <SelectValue>
                {models.find((m) => m.id === model)?.name || "Select a model"}
              </SelectValue>
            </SelectTrigger>
            <SelectContent className="rounded-xl shadow-shade">
              {models.map((model) => (
                <SelectItem
                  key={model.id}
                  value={model.id}
                  className="rounded-lg focus:bg-primary/5"
                >
                  <div className="flex flex-col">
                    <span>{model.name}</span>
                    <span className="text-xs text-muted-foreground">{model.description}</span>
                  </div>
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          {allowFileUpload && (
            <Button variant="outline" size="icon">
              <UploadIcon className="w-4 h-4" />
            </Button>
          )}
        </div>
      )}
    </form>
  );
}
registry/new-york/blocks/button/button.tsx
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";

import { cn } from "@/lib/utils";

const buttonVariants = cva(
  "inline-flex items-center justify-center gap-1 whitespace-nowrap rounded-xl text-md font-medium disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive duration-200 hover:duration-75 transition-all",
  {
    variants: {
      variant: {
        default:
          "border border-primary bg-primary/70 bg-linear-to-t from-primary to-primary/30 px-4 py-2 font-medium text-primary-foreground shadow-md inset-shadow-[0px_1px_0px_1px] inset-shadow-white/30 transition-all duration-75 hover:bg-primary/60 active:translate-y-px active:inset-shadow-black/10 dark:bg-primary dark:from-primary dark:to-primary/80 dark:hover:bg-primary/90",
        fancy:
          "border border-primary bg-primary/30 bg-radial-[at_50%_25%] from-primary/40 to-primary text-primary-foreground shadow-md inset-shadow-sm inset-shadow-white/50 hover:bg-white hover:shadow-lg hover:inset-shadow-white active:bg-primary/30 active:shadow-none active:inset-shadow-primary/80  hover:-translate-y-px active:translate-y-px",
        destructive:
          "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
        outline:
          "border shadow-lg hover:shadow-none shadow-shade bg-transparent text-primary hover:border-transparent hover:bg-primary/10 active:bg-primary/30",
        secondary:
          "border border-transparent bg-primary/10 text-primary hover:border-primary/50 hover:bg-transparent  active:border-transparent active:border-primary active:ring-1 active:scale-95",
        ghost:
          "bg-transparent text-primary hover:border-transparent hover:bg-primary/10 active:bg-primary/30",
        link: "text-foreground underline decoration-primary underline-offset-2 hover:underline-offset-2 active:decoration-4 hover:opacity-70 hover:decoration-2 active:text-foreground",
      },
      size: {
        default: "px-4 py-2",
        xs: "px-2 py-1.5 rounded-lg [&_svg:not([class*='size-'])]:size-3 text-xs",
        sm: "px-3 py-1.5 text-sm rounded-lg",
        lg: "px-5 py-2 text-lg",
        xl: "px-6 py-3 [&_svg:not([class*='size-'])]:size-5 text-xl",
        icon: "size-8 rounded-lg [&_svg:not([class*='size-'])]:size-4",
        iconSm: "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
        iconLg: "size-10 rounded-lg [&_svg:not([class*='size-'])]:size-5",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<"button"> &
  VariantProps<typeof buttonVariants> & {
    asChild?: boolean;
  }) {
  const Comp = asChild ? Slot : "button";

  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

export { Button, buttonVariants };
registry/new-york/blocks/select/select.tsx
"use client";

import * as React from "react";
import * as SelectPrimitive from "@radix-ui/react-select";
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";

import { cn } from "@/lib/utils";

function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
  return <SelectPrimitive.Root data-slot="select" {...props} />;
}

function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
  return <SelectPrimitive.Group data-slot="select-group" {...props} />;
}

function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
  return <SelectPrimitive.Value data-slot="select-value" {...props} />;
}

function SelectTrigger({
  className,
  size = "default",
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
  size?: "sm" | "default";
}) {
  return (
    <SelectPrimitive.Trigger
      data-slot="select-trigger"
      data-size={size}
      className={cn(
        "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex items-center justify-between gap-2 rounded-xl border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-md shadow-shade  transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 data-[size=sm]:rounded-lg *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
        "hover:shadow-none hover:bg-muted hover:border-border/0 transition-all duration-150 ease-out hover:duration-75",
        className
      )}
      {...props}
    >
      {children}
      <SelectPrimitive.Icon asChild>
        <ChevronDownIcon className="size-4 opacity-50" />
      </SelectPrimitive.Icon>
    </SelectPrimitive.Trigger>
  );
}

function SelectContent({
  className,
  children,
  position = "popper",
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
  return (
    <SelectPrimitive.Portal>
      <SelectPrimitive.Content
        data-slot="select-content"
        className={cn(
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2  relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border shadow-lg shadow-shade duration-150 w-full",
          position === "popper" &&
            "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
          className
        )}
        position={position}
        {...props}
      >
        <SelectScrollUpButton />
        <SelectPrimitive.Viewport
          className={cn(
            "p-1",
            position === "popper" &&
              "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
          )}
        >
          {children}
        </SelectPrimitive.Viewport>
        <SelectScrollDownButton />
      </SelectPrimitive.Content>
    </SelectPrimitive.Portal>
  );
}

function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
  return (
    <SelectPrimitive.Label
      data-slot="select-label"
      className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
      {...props}
    />
  );
}

function SelectItem({
  className,
  children,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
  return (
    <SelectPrimitive.Item
      data-slot="select-item"
      className={cn(
        "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
        "transition-all duration-150 ease-out focus:duration-75",
        className
      )}
      {...props}
    >
      <span className="absolute right-2 flex size-3.5 items-center justify-center">
        <SelectPrimitive.ItemIndicator>
          <CheckIcon className="size-4" />
        </SelectPrimitive.ItemIndicator>
      </span>
      <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
    </SelectPrimitive.Item>
  );
}

function SelectSeparator({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
  return (
    <SelectPrimitive.Separator
      data-slot="select-separator"
      className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
      {...props}
    />
  );
}

function SelectScrollUpButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
  return (
    <SelectPrimitive.ScrollUpButton
      data-slot="select-scroll-up-button"
      className={cn("flex cursor-default items-center justify-center py-1", className)}
      {...props}
    >
      <ChevronUpIcon className="size-4" />
    </SelectPrimitive.ScrollUpButton>
  );
}

function SelectScrollDownButton({
  className,
  ...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
  return (
    <SelectPrimitive.ScrollDownButton
      data-slot="select-scroll-down-button"
      className={cn("flex cursor-default items-center justify-center py-1", className)}
      {...props}
    >
      <ChevronDownIcon className="size-4" />
    </SelectPrimitive.ScrollDownButton>
  );
}

export {
  Select,
  SelectContent,
  SelectGroup,
  SelectItem,
  SelectLabel,
  SelectScrollDownButton,
  SelectScrollUpButton,
  SelectSeparator,
  SelectTrigger,
  SelectValue,
};
registry/new-york/blocks/textarea/textarea.tsx
import * as React from "react";

import { cn } from "@/lib/utils";

function Textarea({
  className,
  showHandle = true,
  ...props
}: React.ComponentProps<"textarea"> & { showHandle?: boolean }) {
  return (
    <textarea
      data-slot="textarea"
      className={cn(
        "border-input placeholder:text-muted-foreground focus-visible:border-primary focus-visible:ring-primary/30 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-xl border bg-transparent p-3 text-base shadow-md shadow-shade transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
        showHandle ? "resize-y" : "resize-none",
        "hover:shadow-none hover:bg-foreground/5 hover:border-border/0 transition-all duration-150 ease-out hover:duration-75",
        className
      )}
      {...props}
    />
  );
}

export { Textarea };

More from asanshay

Browse all components in this library →