diff --git a/.changeset/shaggy-cats-smile.md b/.changeset/shaggy-cats-smile.md
new file mode 100644
index 0000000..1867172
--- /dev/null
+++ b/.changeset/shaggy-cats-smile.md
@@ -0,0 +1,5 @@
+---
+'@openai/create-sites': minor
+---
+
+Add a shadcn/ui add-on with the complete component set.
diff --git a/packages/create-sites/README.md b/packages/create-sites/README.md
index 9d29e67..adbe54f 100644
--- a/packages/create-sites/README.md
+++ b/packages/create-sites/README.md
@@ -5,18 +5,24 @@ Create ChatGPT Sites projects.
```bash
npm create @openai/sites@latest my-site
-npm create @openai/sites@latest my-site -- --yes --add-ons d1,r2
+npm create @openai/sites@latest my-site -- --yes --add-ons d1,r2,shadcn
-pnpm create @openai/sites@latest my-site --yes --add-ons d1,r2
+pnpm create @openai/sites@latest my-site --yes --add-ons d1,r2,shadcn
```
Use `--yes` for noninteractive generation. Dependencies are not installed by
default; pass `--install` to install them. The `d1` add-on adds a D1 binding and
-Drizzle, `r2` adds an R2 binding, and `auth` adds ChatGPT authentication helpers.
+Drizzle, `r2` adds an R2 binding, `auth` adds ChatGPT authentication helpers,
+and `shadcn` adds shadcn/ui with its complete component set.
+
+The `shadcn` add-on uses the Base UI Nova preset with neutral colors, Geist,
+Lucide icons, and CSS variables. It includes all 61 entries available through
+`shadcn add --all`: 60 component source files plus the fileless `form`
+compatibility entry.
```bash
npx --yes @openai/create-sites@latest . \
- --yes --add-ons d1,r2
+ --yes --add-ons d1,r2,shadcn
```
The generated `.openai/hosting.json` is the source of truth for Sites bindings.
diff --git a/packages/create-sites/src/index.ts b/packages/create-sites/src/index.ts
index 0f61862..de4f283 100644
--- a/packages/create-sites/src/index.ts
+++ b/packages/create-sites/src/index.ts
@@ -14,13 +14,17 @@ import { cancel, group, multiselect, text } from '@clack/prompts';
import { Command, InvalidArgumentError, Option } from 'commander';
import { xSync } from 'tinyexec';
-type AddOn = 'd1' | 'r2' | 'auth';
+type AddOn = 'd1' | 'r2' | 'auth' | 'shadcn';
type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
const addOns: { name: AddOn; description: string }[] = [
{ name: 'd1', description: 'Add a Cloudflare D1 database and Drizzle.' },
{ name: 'r2', description: 'Add a Cloudflare R2 object storage binding.' },
{ name: 'auth', description: 'Add ChatGPT authentication helpers.' },
+ {
+ name: 'shadcn',
+ description: 'Add shadcn/ui with its complete component set.',
+ },
];
const packageManagers: PackageManager[] = ['npm', 'pnpm', 'yarn', 'bun'];
const allowedExistingEntries = new Set(['.git', '.DS_Store']);
@@ -171,6 +175,27 @@ async function createSite(
};
}
+ if (selected.includes('shadcn')) {
+ manifest.dependencies = {
+ ...manifest.dependencies,
+ '@base-ui/react': '1.7.0',
+ '@shadcn/react': '0.3.0',
+ 'class-variance-authority': '0.7.1',
+ clsx: '2.1.1',
+ cmdk: '1.1.1',
+ 'date-fns': '4.1.0',
+ 'embla-carousel-react': '8.5.2',
+ 'input-otp': '1.4.2',
+ 'lucide-react': '1.31.0',
+ 'react-day-picker': '9.8.1',
+ 'react-resizable-panels': '4.5.8',
+ recharts: '3.8.0',
+ shadcn: '4.18.0',
+ 'tailwind-merge': '3.6.0',
+ 'tw-animate-css': '1.4.0',
+ };
+ }
+
await mkdir(destination, { recursive: true });
await cp(base, destination, { recursive: true, force: false });
await rename(
@@ -181,7 +206,9 @@ async function createSite(
for (const addOn of selected) {
await cp(join(templates, 'addons', addOn), destination, {
recursive: true,
- force: false,
+ // The shadcn add-on intentionally replaces the base global stylesheet
+ // with its theme variables and Tailwind imports.
+ force: addOn === 'shadcn',
});
}
diff --git a/packages/create-sites/templates/addons/shadcn/app/globals.css b/packages/create-sites/templates/addons/shadcn/app/globals.css
new file mode 100644
index 0000000..7fafef8
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/app/globals.css
@@ -0,0 +1,134 @@
+@import 'tailwindcss';
+@import 'tw-animate-css';
+@import 'shadcn/tailwind.css';
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --font-sans: var(--font-geist-sans);
+ --font-mono: var(--font-geist-mono);
+ --font-heading: var(--font-sans);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar: var(--sidebar);
+ --color-chart-5: var(--chart-5);
+ --color-chart-4: var(--chart-4);
+ --color-chart-3: var(--chart-3);
+ --color-chart-2: var(--chart-2);
+ --color-chart-1: var(--chart-1);
+ --color-ring: var(--ring);
+ --color-input: var(--input);
+ --color-border: var(--border);
+ --color-destructive: var(--destructive);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-accent: var(--accent);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-muted: var(--muted);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-secondary: var(--secondary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-primary: var(--primary);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-popover: var(--popover);
+ --color-card-foreground: var(--card-foreground);
+ --color-card: var(--card);
+ --radius-sm: calc(var(--radius) * 0.6);
+ --radius-md: calc(var(--radius) * 0.8);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) * 1.4);
+ --radius-2xl: calc(var(--radius) * 1.8);
+ --radius-3xl: calc(var(--radius) * 2.2);
+ --radius-4xl: calc(var(--radius) * 2.6);
+}
+
+body {
+ font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
+}
+
+:root {
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --radius: 0.625rem;
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.87 0 0);
+ --chart-2: oklch(0.556 0 0);
+ --chart-3: oklch(0.439 0 0);
+ --chart-4: oklch(0.371 0 0);
+ --chart-5: oklch(0.269 0 0);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+ html {
+ @apply font-sans;
+ }
+}
diff --git a/packages/create-sites/templates/addons/shadcn/components.json b/packages/create-sites/templates/addons/shadcn/components.json
new file mode 100644
index 0000000..f382eb7
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "base-nova",
+ "rsc": true,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "app/globals.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "rtl": false,
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "menuColor": "default",
+ "menuAccent": "subtle",
+ "registries": {}
+}
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx
new file mode 100644
index 0000000..9daf7bf
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/accordion.tsx
@@ -0,0 +1,78 @@
+import { Accordion as AccordionPrimitive } from '@base-ui/react/accordion';
+
+import { cn } from '@/lib/utils';
+import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
+
+function Accordion({ className, ...props }: AccordionPrimitive.Root.Props) {
+ return (
+
+ );
+}
+
+function AccordionItem({ className, ...props }: AccordionPrimitive.Item.Props) {
+ return (
+
+ );
+}
+
+function AccordionTrigger({
+ className,
+ children,
+ ...props
+}: AccordionPrimitive.Trigger.Props) {
+ return (
+
+
+ {children}
+
+
+
+
+ );
+}
+
+function AccordionContent({
+ className,
+ children,
+ ...props
+}: AccordionPrimitive.Panel.Props) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx
new file mode 100644
index 0000000..46d9eb7
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/alert-dialog.tsx
@@ -0,0 +1,187 @@
+'use client';
+
+import * as React from 'react';
+import { AlertDialog as AlertDialogPrimitive } from '@base-ui/react/alert-dialog';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+function AlertDialog({ ...props }: AlertDialogPrimitive.Root.Props) {
+ return ;
+}
+
+function AlertDialogTrigger({ ...props }: AlertDialogPrimitive.Trigger.Props) {
+ return (
+
+ );
+}
+
+function AlertDialogPortal({ ...props }: AlertDialogPrimitive.Portal.Props) {
+ return (
+
+ );
+}
+
+function AlertDialogOverlay({
+ className,
+ ...props
+}: AlertDialogPrimitive.Backdrop.Props) {
+ return (
+
+ );
+}
+
+function AlertDialogContent({
+ className,
+ size = 'default',
+ ...props
+}: AlertDialogPrimitive.Popup.Props & {
+ size?: 'default' | 'sm';
+}) {
+ return (
+
+
+
+
+ );
+}
+
+function AlertDialogHeader({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AlertDialogFooter({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AlertDialogMedia({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AlertDialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogAction({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AlertDialogCancel({
+ className,
+ variant = 'outline',
+ size = 'default',
+ ...props
+}: AlertDialogPrimitive.Close.Props &
+ Pick, 'variant' | 'size'>) {
+ return (
+ }
+ {...props}
+ />
+ );
+}
+
+export {
+ AlertDialog,
+ AlertDialogAction,
+ AlertDialogCancel,
+ AlertDialogContent,
+ AlertDialogDescription,
+ AlertDialogFooter,
+ AlertDialogHeader,
+ AlertDialogMedia,
+ AlertDialogOverlay,
+ AlertDialogPortal,
+ AlertDialogTitle,
+ AlertDialogTrigger,
+};
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/alert.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/alert.tsx
new file mode 100644
index 0000000..abe04d3
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/alert.tsx
@@ -0,0 +1,76 @@
+import * as React from 'react';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const alertVariants = cva(
+ "group/alert relative grid w-full gap-0.5 rounded-lg border px-2.5 py-2 text-left text-sm has-data-[slot=alert-action]:relative has-data-[slot=alert-action]:pr-18 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: 'bg-card text-card-foreground',
+ destructive:
+ 'bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Alert({
+ className,
+ variant,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ );
+}
+
+function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+ svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function AlertDescription({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AlertAction({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export { Alert, AlertTitle, AlertDescription, AlertAction };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/aspect-ratio.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..95d97f8
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/aspect-ratio.tsx
@@ -0,0 +1,22 @@
+import { cn } from '@/lib/utils';
+
+function AspectRatio({
+ ratio,
+ className,
+ ...props
+}: React.ComponentProps<'div'> & { ratio: number }) {
+ return (
+
+ );
+}
+
+export { AspectRatio };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/attachment.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/attachment.tsx
new file mode 100644
index 0000000..108ab4d
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/attachment.tsx
@@ -0,0 +1,207 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Button } from '@/components/ui/button';
+
+const attachmentVariants = cva(
+ 'rounded-xl w-fit focus-within:ring-1 focus-within:ring-ring/50 group/attachment relative flex max-w-full min-w-0 shrink-0 flex-wrap border bg-card text-card-foreground transition-colors has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed',
+ {
+ variants: {
+ size: {
+ default:
+ 'gap-2 has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2 text-sm',
+ sm: 'gap-2.5 has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5 text-xs',
+ xs: 'gap-1.5 has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1 text-xs rounded-lg',
+ },
+ orientation: {
+ horizontal: 'min-w-40 items-center',
+ vertical: 'w-24 has-data-[slot=attachment-content]:w-30 flex-col',
+ },
+ },
+ },
+);
+
+function Attachment({
+ className,
+ state = 'done',
+ size = 'default',
+ orientation = 'horizontal',
+ ...props
+}: React.ComponentProps<'div'> &
+ VariantProps
& {
+ state?: 'idle' | 'uploading' | 'processing' | 'error' | 'done';
+ }) {
+ return (
+
+ );
+}
+
+const attachmentMediaVariants = cva(
+ "bg-muted text-foreground w-10 rounded-lg group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md [&_svg:not([class*='size-'])]:size-4 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5 group-data-[orientation=vertical]/attachment:w-full group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! relative flex aspect-square shrink-0 items-center justify-center overflow-hidden group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none",
+ {
+ variants: {
+ variant: {
+ icon: '',
+ image:
+ 'opacity-60 group-data-[state=idle]/attachment:opacity-100 group-data-[state=done]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover',
+ },
+ },
+ defaultVariants: {
+ variant: 'icon',
+ },
+ },
+);
+
+function AttachmentMedia({
+ className,
+ variant = 'icon',
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ );
+}
+
+function AttachmentContent({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AttachmentTitle({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+function AttachmentDescription({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+function AttachmentActions({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AttachmentAction({
+ className,
+ variant,
+ size = 'icon-xs',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+function AttachmentTrigger({
+ className,
+ render,
+ type,
+ ...props
+}: useRender.ComponentProps<'button'>) {
+ return useRender({
+ defaultTagName: 'button',
+ props: mergeProps<'button'>(
+ {
+ type: render ? type : (type ?? 'button'),
+ className: cn('absolute inset-0 z-10 outline-none', className),
+ },
+ props,
+ ),
+ render,
+ state: {
+ slot: 'attachment-trigger',
+ },
+ });
+}
+
+function AttachmentGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+export {
+ Attachment,
+ AttachmentGroup,
+ AttachmentMedia,
+ AttachmentContent,
+ AttachmentTitle,
+ AttachmentDescription,
+ AttachmentActions,
+ AttachmentAction,
+ AttachmentTrigger,
+};
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/avatar.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/avatar.tsx
new file mode 100644
index 0000000..789a014
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/avatar.tsx
@@ -0,0 +1,109 @@
+'use client';
+
+import * as React from 'react';
+import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
+
+import { cn } from '@/lib/utils';
+
+function Avatar({
+ className,
+ size = 'default',
+ ...props
+}: AvatarPrimitive.Root.Props & {
+ size?: 'default' | 'sm' | 'lg';
+}) {
+ return (
+
+ );
+}
+
+function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
+ return (
+
+ );
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: AvatarPrimitive.Fallback.Props) {
+ return (
+
+ );
+}
+
+function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+ svg]:hidden',
+ 'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
+ 'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+function AvatarGroupCount({
+ className,
+ ...props
+}: React.ComponentProps<'div'>) {
+ return (
+ svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 relative flex shrink-0 items-center justify-center ring-2 ring-background',
+ className,
+ )}
+ {...props}
+ />
+ );
+}
+
+export {
+ Avatar,
+ AvatarImage,
+ AvatarFallback,
+ AvatarGroup,
+ AvatarGroupCount,
+ AvatarBadge,
+};
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/badge.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/badge.tsx
new file mode 100644
index 0000000..8e59eb1
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/badge.tsx
@@ -0,0 +1,52 @@
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const badgeVariants = cva(
+ 'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
+ secondary:
+ 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
+ destructive:
+ 'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
+ outline:
+ 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
+ ghost:
+ 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Badge({
+ className,
+ variant = 'default',
+ render,
+ ...props
+}: useRender.ComponentProps<'span'> & VariantProps
) {
+ return useRender({
+ defaultTagName: 'span',
+ props: mergeProps<'span'>(
+ {
+ className: cn(badgeVariants({ variant }), className),
+ },
+ props,
+ ),
+ render,
+ state: {
+ slot: 'badge',
+ variant,
+ },
+ });
+}
+
+export { Badge, badgeVariants };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/breadcrumb.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/breadcrumb.tsx
new file mode 100644
index 0000000..6f51e3f
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/breadcrumb.tsx
@@ -0,0 +1,122 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+
+import { cn } from '@/lib/utils';
+import { ChevronRightIcon, MoreHorizontalIcon } from 'lucide-react';
+
+function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbLink({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<'a'>) {
+ return useRender({
+ defaultTagName: 'a',
+ props: mergeProps<'a'>(
+ {
+ className: cn('hover:text-foreground transition-colors', className),
+ },
+ props,
+ ),
+ render,
+ state: {
+ slot: 'breadcrumb-link',
+ },
+ });
+}
+
+function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
+ return (
+
+ );
+}
+
+function BreadcrumbSeparator({
+ children,
+ className,
+ ...props
+}: React.ComponentProps<'li'>) {
+ return (
+ svg]:size-3.5', className)}
+ {...props}
+ >
+ {children ?? }
+
+ );
+}
+
+function BreadcrumbEllipsis({
+ className,
+ ...props
+}: React.ComponentProps<'span'>) {
+ return (
+ svg]:size-4 flex items-center justify-center',
+ className,
+ )}
+ {...props}
+ >
+
+ More
+
+ );
+}
+
+export {
+ Breadcrumb,
+ BreadcrumbList,
+ BreadcrumbItem,
+ BreadcrumbLink,
+ BreadcrumbPage,
+ BreadcrumbSeparator,
+ BreadcrumbEllipsis,
+};
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/bubble.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/bubble.tsx
new file mode 100644
index 0000000..61f2030
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/bubble.tsx
@@ -0,0 +1,128 @@
+import * as React from 'react';
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+function BubbleGroup({ className, ...props }: React.ComponentProps<'div'>) {
+ return (
+
+ );
+}
+
+const bubbleVariants = cva(
+ 'gap-1 data-[align=end]:self-end max-w-[80%] data-[variant=ghost]:max-w-full group-data-[align=end]/message:self-end group/bubble relative flex w-fit min-w-0 flex-col',
+ {
+ variants: {
+ variant: {
+ default:
+ '*:data-[slot=bubble-content]:bg-primary *:data-[slot=bubble-content]:text-primary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-primary/80',
+ secondary:
+ '*:data-[slot=bubble-content]:bg-secondary *:data-[slot=bubble-content]:text-secondary-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)]',
+ muted:
+ '*:data-[slot=bubble-content]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[color-mix(in_oklch,var(--muted),var(--foreground)_5%)]',
+ tinted:
+ '*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.93_calc(c*0.4)_h)] dark:*:data-[slot=bubble-content]:bg-[oklch(from_var(--primary)_0.3_calc(c*0.4)_h)] *:data-[slot=bubble-content]:text-foreground [&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.88_calc(c*0.5)_h)] dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-[oklch(from_var(--primary)_0.35_calc(c*0.5)_h)]',
+ outline:
+ '*:data-[slot=bubble-content]:bg-background *:data-[slot=bubble-content]:border-border [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-input/30',
+ ghost:
+ '*:data-[slot=bubble-content]:rounded-none *:data-[slot=bubble-content]:bg-transparent *:data-[slot=bubble-content]:p-0 [&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted [&>[data-slot=bubble-content]:is(button,a):hover]:text-foreground dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-muted/50 border-none',
+ destructive:
+ '*:data-[slot=bubble-content]:bg-destructive/10 dark:*:data-[slot=bubble-content]:bg-destructive/20 *:data-[slot=bubble-content]:text-destructive [&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/20 dark:[&>[data-slot=bubble-content]:is(button,a):hover]:bg-destructive/30',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ },
+ },
+);
+
+function Bubble({
+ variant = 'default',
+ align = 'start',
+ className,
+ ...props
+}: React.ComponentProps<'div'> &
+ VariantProps & {
+ align?: 'start' | 'end';
+ }) {
+ return (
+
+ );
+}
+
+function BubbleContent({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<'div'>) {
+ return useRender({
+ defaultTagName: 'div',
+ props: mergeProps<'div'>(
+ {
+ className: cn(
+ 'rounded-xl border border-transparent px-3 py-2 text-sm leading-relaxed [button,a]:outline-none [button,a]:focus-visible:border-ring [button,a]:focus-visible:ring-3 [button,a]:focus-visible:ring-ring/50 group-data-[align=end]/bubble:self-end w-fit max-w-full min-w-0 overflow-hidden wrap-break-word [button]:text-left [button,a]:transition-colors',
+ className,
+ ),
+ },
+ props,
+ ),
+ render,
+ state: {
+ slot: 'bubble-content',
+ },
+ });
+}
+
+const bubbleReactionsVariants = cva(
+ 'rounded-full ring-3 ring-card bg-muted shrink-0 gap-1 px-1.5 py-0.5 has-[button]:p-0 text-sm absolute z-10 flex w-fit items-center justify-center',
+ {
+ variants: {
+ side: {
+ top: 'top-0 -translate-y-3/4',
+ bottom: 'bottom-0 translate-y-3/4',
+ },
+ align: {
+ start: 'left-3',
+ end: 'right-3',
+ },
+ },
+ defaultVariants: {
+ side: 'bottom',
+ align: 'end',
+ },
+ },
+);
+
+function BubbleReactions({
+ side = 'bottom',
+ align = 'end',
+ className,
+ ...props
+}: React.ComponentProps<'div'> & {
+ align?: 'start' | 'end';
+ side?: 'top' | 'bottom';
+}) {
+ return (
+
+ );
+}
+
+export { BubbleGroup, Bubble, BubbleContent, BubbleReactions };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/button-group.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/button-group.tsx
new file mode 100644
index 0000000..8f8c2d2
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/button-group.tsx
@@ -0,0 +1,87 @@
+import { mergeProps } from '@base-ui/react/merge-props';
+import { useRender } from '@base-ui/react/use-render';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+import { Separator } from '@/components/ui/separator';
+
+const buttonGroupVariants = cva(
+ "has-[>[data-slot=button-group]]:gap-2 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-lg flex w-fit items-stretch *:focus-visible:relative *:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1",
+ {
+ variants: {
+ orientation: {
+ horizontal:
+ '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-r-lg! *:data-slot:rounded-r-none [&>[data-slot]~[data-slot]]:rounded-l-none [&>[data-slot]~[data-slot]]:border-l-0',
+ vertical:
+ '[&>[data-slot]:not(:has(~[data-slot]))]:rounded-b-lg! flex-col *:data-slot:rounded-b-none [&>[data-slot]~[data-slot]]:rounded-t-none [&>[data-slot]~[data-slot]]:border-t-0',
+ },
+ },
+ defaultVariants: {
+ orientation: 'horizontal',
+ },
+ },
+);
+
+function ButtonGroup({
+ className,
+ orientation,
+ ...props
+}: React.ComponentProps<'div'> & VariantProps) {
+ return (
+
+ );
+}
+
+function ButtonGroupText({
+ className,
+ render,
+ ...props
+}: useRender.ComponentProps<'div'>) {
+ return useRender({
+ defaultTagName: 'div',
+ props: mergeProps<'div'>(
+ {
+ className: cn(
+ "bg-muted gap-2 rounded-lg border px-2.5 text-sm font-medium [&_svg:not([class*='size-'])]:size-4 flex items-center [&_svg]:pointer-events-none",
+ className,
+ ),
+ },
+ props,
+ ),
+ render,
+ state: {
+ slot: 'button-group-text',
+ },
+ });
+}
+
+function ButtonGroupSeparator({
+ className,
+ orientation = 'vertical',
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ );
+}
+
+export {
+ ButtonGroup,
+ ButtonGroupSeparator,
+ ButtonGroupText,
+ buttonGroupVariants,
+};
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/button.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/button.tsx
new file mode 100644
index 0000000..7a8a32c
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/button.tsx
@@ -0,0 +1,58 @@
+import { Button as ButtonPrimitive } from '@base-ui/react/button';
+import { cva, type VariantProps } from 'class-variance-authority';
+
+import { cn } from '@/lib/utils';
+
+const buttonVariants = cva(
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
+ {
+ variants: {
+ variant: {
+ default: 'bg-primary text-primary-foreground hover:bg-primary/80',
+ outline:
+ 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
+ secondary:
+ 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
+ ghost:
+ 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
+ destructive:
+ 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
+ link: 'text-primary underline-offset-4 hover:underline',
+ },
+ size: {
+ default:
+ 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
+ lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
+ icon: 'size-8',
+ 'icon-xs':
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
+ 'icon-sm':
+ 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
+ 'icon-lg': 'size-9',
+ },
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default',
+ },
+ },
+);
+
+function Button({
+ className,
+ variant = 'default',
+ size = 'default',
+ ...props
+}: ButtonPrimitive.Props & VariantProps) {
+ return (
+
+ );
+}
+
+export { Button, buttonVariants };
diff --git a/packages/create-sites/templates/addons/shadcn/components/ui/calendar.tsx b/packages/create-sites/templates/addons/shadcn/components/ui/calendar.tsx
new file mode 100644
index 0000000..5d1cb82
--- /dev/null
+++ b/packages/create-sites/templates/addons/shadcn/components/ui/calendar.tsx
@@ -0,0 +1,231 @@
+'use client';
+
+import * as React from 'react';
+import {
+ DayPicker,
+ getDefaultClassNames,
+ type DayButton,
+ type Locale,
+} from 'react-day-picker';
+
+import { cn } from '@/lib/utils';
+import { Button, buttonVariants } from '@/components/ui/button';
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ ChevronDownIcon,
+} from 'lucide-react';
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ captionLayout = 'label',
+ buttonVariant = 'ghost',
+ locale,
+ formatters,
+ components,
+ ...props
+}: React.ComponentProps & {
+ buttonVariant?: React.ComponentProps['variant'];
+}) {
+ const defaultClassNames = getDefaultClassNames();
+
+ return (
+ svg]:rotate-180`,
+ String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
+ className,
+ )}
+ captionLayout={captionLayout}
+ locale={locale}
+ formatters={{
+ formatMonthDropdown: (date) =>
+ date.toLocaleString(locale?.code, { month: 'short' }),
+ ...formatters,
+ }}
+ classNames={{
+ root: cn('w-fit', defaultClassNames.root),
+ months: cn(
+ 'relative flex flex-col gap-4 md:flex-row',
+ defaultClassNames.months,
+ ),
+ month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
+ nav: cn(
+ 'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
+ defaultClassNames.nav,
+ ),
+ button_previous: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ defaultClassNames.button_previous,
+ ),
+ button_next: cn(
+ buttonVariants({ variant: buttonVariant }),
+ 'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
+ defaultClassNames.button_next,
+ ),
+ month_caption: cn(
+ 'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
+ defaultClassNames.month_caption,
+ ),
+ dropdowns: cn(
+ 'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
+ defaultClassNames.dropdowns,
+ ),
+ dropdown_root: cn(
+ 'cn-calendar-dropdown-root relative rounded-(--cell-radius)',
+ defaultClassNames.dropdown_root,
+ ),
+ dropdown: cn(
+ 'absolute inset-0 bg-popover opacity-0',
+ defaultClassNames.dropdown,
+ ),
+ caption_label: cn(
+ 'font-medium select-none',
+ captionLayout === 'label'
+ ? 'cn-calendar-caption text-sm'
+ : 'cn-calendar-caption-label flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
+ defaultClassNames.caption_label,
+ ),
+ month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
+ weekdays: cn('flex', defaultClassNames.weekdays),
+ weekday: cn(
+ 'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
+ defaultClassNames.weekday,
+ ),
+ week: cn('mt-2 flex w-full', defaultClassNames.week),
+ week_number_header: cn(
+ 'w-(--cell-size) select-none',
+ defaultClassNames.week_number_header,
+ ),
+ week_number: cn(
+ 'text-[0.8rem] text-muted-foreground select-none',
+ defaultClassNames.week_number,
+ ),
+ day: cn(
+ 'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
+ props.showWeekNumber
+ ? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
+ : '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
+ defaultClassNames.day,
+ ),
+ range_start: cn(
+ 'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
+ defaultClassNames.range_start,
+ ),
+ range_middle: cn('rounded-none', defaultClassNames.range_middle),
+ range_end: cn(
+ 'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
+ defaultClassNames.range_end,
+ ),
+ today: cn(
+ 'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
+ defaultClassNames.today,
+ ),
+ outside: cn(
+ 'text-muted-foreground aria-selected:text-muted-foreground',
+ defaultClassNames.outside,
+ ),
+ disabled: cn(
+ 'text-muted-foreground opacity-50',
+ defaultClassNames.disabled,
+ ),
+ hidden: cn('invisible', defaultClassNames.hidden),
+ ...classNames,
+ }}
+ components={{
+ Root: ({ className, rootRef, ...props }) => {
+ return (
+
+ );
+ },
+ Chevron: ({ className, orientation, ...props }) => {
+ if (orientation === 'left') {
+ return (
+
+ );
+ }
+
+ if (orientation === 'right') {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ },
+ DayButton: ({ ...props }) => (
+
+ ),
+ WeekNumber: ({ children, ...props }) => {
+ return (
+
+
+ {children}
+
+ |
+ );
+ },
+ ...components,
+ }}
+ {...props}
+ />
+ );
+}
+
+function CalendarDayButton({
+ className,
+ day,
+ modifiers,
+ locale,
+ ...props
+}: React.ComponentProps & { locale?: Partial }) {
+ const defaultClassNames = getDefaultClassNames();
+
+ const ref = React.useRef(null);
+ React.useEffect(() => {
+ if (modifiers.focused) ref.current?.focus();
+ }, [modifiers.focused]);
+
+ return (
+