asanshay
componentchatsource: ok · hit
Chat
A complete chat component that composes chat-message and chat-input and wires to /api/chat.
npx shadcn add "https://ds.asanshay.com/r/chat.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 · lightui-primitives@radix-ui/react-select
iconslucide-react
Registry dependencies
accordion
Files11 · source cached 10/14/2026, 11:19:39 AM
registry/new-york/routes/chat/route.ts
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText, UIMessage, stepCountIs, smoothStream } from "ai";
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: openai("gpt-5"),
messages: convertToModelMessages(messages),
providerOptions: {
openai: {
reasoningSummary: "auto",
},
},
stopWhen: stepCountIs(100),
experimental_transform: smoothStream(),
});
return result.toUIMessageStreamResponse();
}
registry/new-york/blocks/chat/chat.tsx
"use client";
import * as React from "react";
import ChatMessage from "@/registry/new-york/blocks/chat-message/chat-message";
import ChatInput from "@/registry/new-york/blocks/chat-input/chat-input";
import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, UIMessage } from "ai";
import { cn } from "@/lib/utils";
import { AlertTriangle, Loader } from "lucide-react";
export default function Chat({
defaultModel = "gpt-5-nano",
className,
}: {
defaultModel?: string;
className?: string;
}) {
const { messages, sendMessage, status, error, stop } = useChat({
transport: new DefaultChatTransport({
api: "/api/chat",
}),
});
const [input, setInput] = React.useState<string>("");
const models = React.useMemo(
() => [
{ id: "gpt-5", name: "GPT-5", description: "Reasoning-capable, efficient" },
{ id: "gpt-5-mini", name: "GPT-5 mini", description: "Fast and affordable" },
{ id: "gpt-5-nano", name: "GPT-5 nano", description: "Reasoning-capable, efficient" },
],
[]
);
const [model, setModel] = React.useState<string>(defaultModel || models[0]?.id);
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!input.trim()) return;
sendMessage({ text: input.trim() }, { body: { model } });
setInput("");
};
const handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setInput(e.target.value);
};
return (
<div className={cn("relative flex flex-col", className)}>
<div className="flex flex-col gap-2 flex-1 min-h-0 overflow-y-auto px-2">
{messages.map((m: UIMessage) => (
<ChatMessage key={m.id} message={m} />
))}
<div className="min-h-32"></div>
</div>
{status === "streaming" ||
(status === "submitted" && (
<div className="text-sm text-muted-foreground">
<Loader className="size-4 animate-spin" />
</div>
))}
{status === "error" && error && (
<div className="text-destructive text-sm flex items-center gap-2 px-4 py-2 rounded-md bg-destructive/10">
<AlertTriangle className="size-4" />
{error.message}
</div>
)}
<ChatInput
title=""
placeholder="Ask anything..."
models={models}
allowFileUpload={false}
loading={status === "streaming"}
handleSubmit={handleSubmit}
handleInputChange={handleInputChange}
input={input}
stop={stop}
className="absolute bottom-0"
model={model}
onModelChange={setModel}
/>
</div>
);
}
registry/new-york/blocks/chat-message/chat-message.tsx
"use client";
import { cn } from "@/lib/utils";
import { VariantProps, cva } from "class-variance-authority";
import Markdown from "@/registry/new-york/blocks/markdown/markdown";
import React from "react";
import { ToolUIPart, UIMessage } from "ai";
import ChatReasoning from "@/registry/new-york/blocks/chat-reasoning/chat-reasoning";
import ChatTool from "@/registry/new-york/blocks/chat-tool/chat-tool";
const userMessageVariants = cva("flex flex-col gap-2", {
variants: {
variant: {
raised:
"bg-gradient-to-b from-primary to-primary/80 border border-primary text-primary-foreground shadow-lg shadow-shade max-w-2xs ml-auto rounded-xl px-4 py-2 font-medium",
title: "font-semibold tracking-tight text-3xl mt-4 border-b border-primary/30 pb-1",
default: "bg-primary/20 max-w-2xs ml-auto rounded-xl px-4 py-2",
},
},
});
const assistantMessageVariants = cva("flex flex-col gap-2", {
variants: {
variant: {
raised:
"bg-gradient-to-b from-muted to-muted/50 rounded-xl px-4 py-2 border shadow-md shadow-shade mr-8",
paragraph: "",
default: "bg-muted rounded-xl px-4 py-2 mr-8",
},
},
});
// Helper function to render a single message part
const renderMessagePart = (part: any, key: string | number) => {
if (part.type.includes("tool")) {
return (
<ChatTool
toolMessagePart={part as ToolUIPart}
className="my-1 border-none px-0 py-0 shadow-none text-muted-foreground"
/>
);
} else if (part.type === "text") {
return <Markdown key={key}>{part.text}</Markdown>;
} else if (part.type === "reasoning") {
return (
<Markdown key={key} className="text-sm text-muted-foreground">
{part.text}
</Markdown>
);
}
return <div key={key}>{part.type}</div>;
};
export default function ChatMessage({
message,
className,
userMessageVariant = "default",
assistantMessageVariant = "default",
}: {
message: UIMessage;
className?: string;
userMessageVariant?: VariantProps<typeof userMessageVariants>["variant"];
assistantMessageVariant?: VariantProps<typeof assistantMessageVariants>["variant"];
}) {
if (message.role === "user") {
return (
<div
className={cn(userMessageVariants({ variant: userMessageVariant }), className)}
role="user"
>
{message.parts.map((part) => part.type === "text" && part.text).join("")}
</div>
);
}
const firstTextIndex = message.parts.findIndex((part) => part.type === "text");
const hasTextPart = firstTextIndex !== -1;
const shouldShowAccordion = firstTextIndex !== 0;
const accordionDefaultValue = !hasTextPart ? "reasoning" : null;
const partsInAccordion = shouldShowAccordion ? message.parts.slice(0, firstTextIndex) : [];
const partsAfter = hasTextPart ? message.parts.slice(firstTextIndex) : [];
return (
<div className={cn("flex items-start gap-3", className)}>
<div
className={cn(
"flex flex-col gap-1 relative",
assistantMessageVariants({ variant: assistantMessageVariant })
)}
>
{shouldShowAccordion && (
<ChatReasoning
renderMessagePart={renderMessagePart}
partsInAccordion={partsInAccordion}
defaultValue={accordionDefaultValue ?? undefined}
/>
)}
{partsAfter.map((part, index) => renderMessagePart(part, firstTextIndex + index))}
</div>
</div>
);
}
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/chat-tool/chat-tool.tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { DynamicToolUIPart, ToolUIPart } from "ai";
import { cn } from "@/lib/utils";
import { Wrench, Loader2, Check, AlertCircle } from "lucide-react";
// Helper function to get state information
function getStateInfo(state: string) {
switch (state) {
case "input-streaming":
return {
icon: Wrench,
label: "Using ",
color: "text-muted-foreground",
};
case "input-available":
return {
icon: Loader2,
label: "Using ",
color: "text-muted-foreground",
};
case "output-available":
return {
icon: Check,
label: "Used ",
color: "text-success",
};
case "output-error":
return {
icon: AlertCircle,
label: "Failed to use",
color: "text-destructive",
};
default:
return {
icon: Wrench,
label: "Using",
color: "text-muted-foreground",
};
}
}
// Helper function to safely render unknown values
function renderValue(value: unknown): React.ReactNode {
if (typeof value === "string") {
return value;
}
return JSON.stringify(value, null, 2);
}
export default function ChatTool({
toolMessagePart,
className,
}: {
toolMessagePart: ToolUIPart | DynamicToolUIPart;
className?: string;
}) {
const toolName =
toolMessagePart.type === "dynamic-tool"
? toolMessagePart.toolName
: toolMessagePart.type.replace("tool-", "");
const stateInfo = getStateInfo(toolMessagePart.state);
const StateIcon = stateInfo.icon;
return (
<Accordion type="single" collapsible className="w-full">
<AccordionItem
value="tool"
className={cn(
"px-2 border rounded-xl hover:no-underline py-2 w-full last:border-b shadow-xs",
className
)}
>
<AccordionTrigger className={cn("py-0 hover:no-underline hover:opacity-70 transition-all")}>
<span className="flex items-center gap-2">
<StateIcon
className={cn(
"w-4 h-4",
stateInfo.color,
toolMessagePart.state === "input-available" && "animate-spin"
)}
/>
<span className="text-sm">
{stateInfo.label} {toolName}
</span>
</span>
</AccordionTrigger>
<AccordionContent className="pb-0">
<div className="flex flex-col gap-3 w-full pt-2">
{/* Error Section - show for failed executions */}
{toolMessagePart.state === "output-error" && toolMessagePart.errorText && (
<div className="bg-destructive/5 border rounded-md p-2 text-sm overflow-x-auto whitespace-pre-wrap text-destructive w-full">
{toolMessagePart.errorText}
</div>
)}
{/* Input Section - always show if available */}
{"input" in toolMessagePart &&
toolMessagePart.input !== undefined &&
toolMessagePart.input !== null ? (
<div className="w-full">
<div className="text-xs font-semibold text-muted-foreground mb-1">Input</div>
<pre className="bg-muted rounded-md p-2 text-sm overflow-x-auto whitespace-pre-wrap w-full">
{renderValue(toolMessagePart.input)}
</pre>
</div>
) : (
<div className="text-xs text-muted-foreground">No input</div>
)}
{/* Output Section - show for successful completion */}
{toolMessagePart.state === "output-available" &&
"output" in toolMessagePart &&
toolMessagePart.output !== undefined &&
toolMessagePart.output !== null && (
<div className="w-full">
<div className="text-xs font-semibold text-muted-foreground mb-1">Output</div>
<pre className="bg-muted rounded-md p-2 text-sm overflow-x-auto whitespace-pre-wrap w-full">
{renderValue(toolMessagePart.output)}
</pre>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
registry/new-york/blocks/chat-reasoning/chat-reasoning.tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { cn } from "@/lib/utils";
import { UIDataTypes, UIMessagePart, UITools } from "ai";
import React from "react";
export default function ChatReasoning({
partsInAccordion,
defaultValue,
renderMessagePart,
className,
}: {
partsInAccordion: UIMessagePart<UIDataTypes, UITools>[];
defaultValue?: string;
renderMessagePart: (
part: UIMessagePart<UIDataTypes, UITools>,
key: string | number
) => React.ReactNode;
className?: string;
}) {
const [value, setValue] = React.useState<string | undefined>(defaultValue);
React.useEffect(() => {
setValue(defaultValue);
}, [defaultValue]);
return (
<Accordion
type="single"
collapsible
value={value}
onValueChange={setValue}
className={cn("w-full", className)}
>
<AccordionItem value="reasoning" className="w-full">
<AccordionTrigger className="text-md text-muted-foreground hover:no-underline hover:opacity-70 py-2 w-full">
{defaultValue === "reasoning" ? "Reasoning..." : `Done reasoning.`}
</AccordionTrigger>
<AccordionContent className="p-0 -mt-1">
<div className="flex flex-col gap-0">
{partsInAccordion.map(
(part, index) =>
part.type !== "step-start" && (
<div key={index} className="flex gap-2 pl-2">
<div className="flex flex-col items-center gap-1 pt-2 -mb-1">
<div className="w-2 h-2 bg-muted-foreground/50 rounded-full" />
<div
className={cn(
"w-0.5 min-h-0 flex-1 bg-border rounded-full",
index === partsInAccordion.length - 1 &&
"bg-gradient-to-b from-border to-transparent"
)}
/>
</div>
<div className="flex-1">{renderMessagePart(part, `accordion-${index}`)}</div>
</div>
)
)}
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
);
}
registry/new-york/blocks/markdown/markdown.tsx
import ReactMarkdown, { Components } from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "@/lib/utils";
import { CodeBlock } from "@/registry/new-york/blocks/code-block/code-block";
import { Button } from "@/registry/new-york/blocks/button/button";
const components: Components = {
// Text components
p: ({ children }) => <p className="text-base mb-3 last:mb-0">{children}</p>,
h1: ({ children }) => (
<h1 className="text-4xl font-semibold tracking-tight mb-4 mt-2">{children}</h1>
),
h2: ({ children }) => (
<h2 className="text-3xl font-semibold tracking-tight mt-4 mb-2 border-b border-border pb-2">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-2xl font-semibold tracking-tight mt-4 mb-2">{children}</h3>
),
h4: ({ children }) => (
<h4 className="text-xl font-semibold tracking-tight mt-3 mb-2">{children}</h4>
),
h5: ({ children }) => (
<h5 className="text-lg font-semibold tracking-tight mt-2 mb-1">{children}</h5>
),
h6: ({ children }) => (
<h6 className="text-base font-semibold tracking-tight mt-2 mb-1">{children}</h6>
),
// Lists
ul: ({ children }) => (
<ul className="text-base my-2 pl-6 list-disc marker:text-primary/70">{children}</ul>
),
ol: ({ children }) => <ol className="text-base my-2 pl-6 list-decimal">{children}</ol>,
li: ({ children }) => <li className="my-0.5">{children}</li>,
// Inline formatting
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
em: ({ children }) => <em className="italic">{children}</em>,
del: ({ children }) => <del className="line-through">{children}</del>,
// Block elements
blockquote: ({ children }) => (
<blockquote className="pl-4 border-l-4 border-primary/20 my-2 italic text-primary/70">
{children}
</blockquote>
),
hr: () => <hr className="my-4 border-t border-border" />,
// Tables
table: ({ children }) => (
<div className="overflow-x-auto my-4">
<table className="min-w-full divide-y divide-border border border-border">{children}</table>
</div>
),
thead: ({ children }) => <thead className="bg-muted/50">{children}</thead>,
tbody: ({ children }) => <tbody className="divide-y divide-border">{children}</tbody>,
tr: ({ children }) => <tr>{children}</tr>,
th: ({ children }) => (
<th className="px-3 py-2 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
{children}
</th>
),
td: ({ children }) => <td className="px-3 py-2 whitespace-nowrap text-sm">{children}</td>,
// Links and images
a: ({ children, href, title }) => (
<Button
variant="link"
size="sm"
className="text-primary hover:underline p-0 m-0"
title={title}
onClick={() => {
if (href) {
window.open(href, "_blank", "noopener noreferrer");
}
}}
disabled={!href}
asChild
>
{children}
</Button>
),
img: ({ src, alt, title }) => (
<div className="flex flex-col items-center w-fit">
<img
src={src || ""}
alt={alt || ""}
title={title || alt || ""}
className="max-w-sm h-auto my-2 rounded-xl"
/>
<p className="text-sm text-center text-muted-foreground">{alt}</p>
</div>
),
// Code blocks and inline code
code: ({ className, children }) => {
const match = /language-(\w+)/.exec(className || "");
const isInline = !match;
return !isInline && match ? (
<CodeBlock code={String(children).replace(/\n$/, "")} language={match[1]} />
) : (
<code className="bg-muted px-1.5 py-0.5 rounded-md text-sm text-primary font-mono">
{children}
</code>
);
},
pre: ({ children }) => <>{children}</>,
};
export default function Markdown({
children,
className,
}: {
children: string;
className?: string;
}) {
return (
<div className={cn("w-full", className)}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
{children}
</ReactMarkdown>
</div>
);
}
registry/new-york/blocks/code-block/code-block.tsx
"use client";
import { cn } from "@/lib/utils";
import React, { useEffect, useState } from "react";
import { Button } from "@/registry/new-york/blocks/button/button";
import { Check, Copy } from "lucide-react";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneLight, a11yDark as oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import { cva } from "class-variance-authority";
import { useTheme } from "next-themes";
const customSyntaxTheme = {
...oneLight,
'pre[class*="language-"]': {
...oneLight['pre[class*="language-"]'],
background: "hsl(var(--muted))",
borderRadius: "0.5rem",
fontFamily: "Geist Mono, monospace",
textShadow: "none",
padding: 0,
margin: 0,
},
'code[class*="language-"]': {
...oneLight['code[class*="language-"]'],
background: "none",
fontFamily: "Geist Mono, monospace",
textShadow: "none",
fontSize: "13px",
padding: 0,
},
};
const customDarkSyntaxTheme = {
...oneDark,
'pre[class*="language-"]': {
...oneDark['pre[class*="language-"]'],
background: "hsl(var(--muted))",
borderRadius: "0.5rem",
fontFamily: "Geist Mono, monospace",
textShadow: "none",
padding: 0,
margin: 0,
},
'code[class*="language-"]': {
...oneDark['code[class*="language-"]'],
background: "none",
fontFamily: "Geist Mono, monospace",
textShadow: "none",
fontSize: "13px",
padding: 0,
},
};
const codeBlockVariants = cva("text-card-foreground rounded-xl p-4 flex flex-col gap-2", {
variants: {
variant: {
flat: "bg-muted",
default: "border border-border bg-card",
},
},
defaultVariants: {
variant: "default",
},
});
export type CodeBlockProps = {
code: string;
language?: string;
theme?: string;
className?: string;
title?: string;
children?: React.ReactNode;
variant?: "default" | "flat";
} & React.HTMLProps<HTMLDivElement>;
export function CodeBlock({
code,
language = "tsx",
theme = "github-light",
className,
title,
children,
variant = "default",
...props
}: CodeBlockProps) {
const { theme: themeName } = useTheme();
const codeClassNames = cn("w-full overflow-x-auto text-[13px] font-mono");
const [copied, setCopied] = useState(false);
return (
<div className={cn("relative group", codeBlockVariants({ variant }), className)} {...props}>
<Button
onClick={() => {
navigator.clipboard.writeText(String(code));
setCopied(true);
setTimeout(() => {
setCopied(false);
}, 1000);
}}
variant="ghost"
size="icon"
className="absolute right-2 top-2 focus-visible:ring-0 text-muted-foreground hover:bg-muted active:bg-muted"
aria-label={copied ? "Copied" : "Copy to clipboard"}
>
<span className="sr-only">{copied ? "Copied" : "Copy"}</span>
<Copy
className={`size-4 transition-all duration-300 ${copied ? "scale-0" : "scale-100"}`}
/>
<Check
className={`absolute inset-0 m-auto size-4 transition-all duration-300 text-success ${
copied ? "scale-100" : "scale-0"
}`}
/>
</Button>
{title && <h3 className="text-sm text-muted-foreground tracking-tight font-mono">{title}</h3>}
<SyntaxHighlighter
language={language}
style={themeName === "dark" ? customDarkSyntaxTheme : customSyntaxTheme}
PreTag="div"
className={codeClassNames}
>
{code}
</SyntaxHighlighter>
</div>
);
}
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 };