├── .gitignore ├── LICENSE ├── README.md ├── app ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx └── registry │ └── [name] │ └── route.ts ├── components.json ├── components ├── mode-toggle.tsx ├── open-in-v0-button.tsx ├── providers.tsx ├── tailwind-indicator.tsx └── ui │ ├── button.tsx │ ├── card.tsx │ ├── input.tsx │ ├── label.tsx │ └── textarea.tsx ├── eslint.config.mjs ├── lib └── utils.ts ├── next.config.ts ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── public ├── file.svg ├── globe.svg ├── next.svg ├── r │ ├── complex-component.json │ ├── example-form.json │ └── hello-world.json ├── vercel.svg └── window.svg ├── registry.json ├── registry ├── layout.tsx └── new-york │ ├── complex-component │ ├── components │ │ ├── pokemon-card.tsx │ │ └── pokemon-image.tsx │ ├── hooks │ │ └── use-pokemon.ts │ ├── lib │ │ └── pokemon.ts │ └── page.tsx │ ├── example-form │ └── example-form.tsx │ └── hello-world │ └── hello-world.tsx ├── tailwind.config.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 shadcn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # registry-template 2 | 3 | You can use the `shadcn` CLI to run your own component registry. Running your own 4 | component registry allows you to distribute your custom components, hooks, pages, and 5 | other files to any React project. 6 | 7 | > [!IMPORTANT] 8 | > This template uses Tailwind v3. For Tailwind v4, see [registry-template](https://github.com/shadcn-ui/registry-template-v4). 9 | 10 | ## Getting Started 11 | 12 | This is a template for creating a custom registry using Next.js. 13 | 14 | - The template uses a `registry.json` file to define components and their files. 15 | - The `shadcn build` command is used to build the registry. 16 | - The registry items are served as static files under `public/r/[name].json`. 17 | - The template also includes a route handler for serving registry items. 18 | - Every registry item are compatible with the `shadcn` CLI. 19 | - We have also added v0 integration using the `Open in v0` api. 20 | 21 | ## Documentation 22 | 23 | Visit the [shadcn documentation](https://ui.shadcn.com/docs/registry) to view the full documentation. 24 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shadcn-ui/registry-template/2794e993dedb5807f57a28f34934f46ae4452d0d/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 10% 3.9%; 26 | --chart-1: 12 76% 61%; 27 | --chart-2: 173 58% 39%; 28 | --chart-3: 197 37% 24%; 29 | --chart-4: 43 74% 66%; 30 | --chart-5: 27 87% 67%; 31 | --radius: 0.5rem; 32 | --sidebar-background: 0 0% 98%; 33 | --sidebar-foreground: 240 5.3% 26.1%; 34 | --sidebar-primary: 240 5.9% 10%; 35 | --sidebar-primary-foreground: 0 0% 98%; 36 | --sidebar-accent: 240 4.8% 95.9%; 37 | --sidebar-accent-foreground: 240 5.9% 10%; 38 | --sidebar-border: 220 13% 91%; 39 | --sidebar-ring: 217.2 91.2% 59.8%; 40 | } 41 | .dark { 42 | --background: 240 10% 3.9%; 43 | --foreground: 0 0% 98%; 44 | --card: 240 10% 3.9%; 45 | --card-foreground: 0 0% 98%; 46 | --popover: 240 10% 3.9%; 47 | --popover-foreground: 0 0% 98%; 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 240 5.9% 10%; 50 | --secondary: 240 3.7% 15.9%; 51 | --secondary-foreground: 0 0% 98%; 52 | --muted: 240 3.7% 15.9%; 53 | --muted-foreground: 240 5% 64.9%; 54 | --accent: 240 3.7% 15.9%; 55 | --accent-foreground: 0 0% 98%; 56 | --destructive: 0 62.8% 30.6%; 57 | --destructive-foreground: 0 0% 98%; 58 | --border: 240 3.7% 15.9%; 59 | --input: 240 3.7% 15.9%; 60 | --ring: 240 4.9% 83.9%; 61 | --chart-1: 220 70% 50%; 62 | --chart-2: 160 60% 45%; 63 | --chart-3: 30 80% 55%; 64 | --chart-4: 280 65% 60%; 65 | --chart-5: 340 75% 55%; 66 | --sidebar-background: 240 5.9% 10%; 67 | --sidebar-foreground: 240 4.8% 95.9%; 68 | --sidebar-primary: 224.3 76.3% 48%; 69 | --sidebar-primary-foreground: 0 0% 100%; 70 | --sidebar-accent: 240 3.7% 15.9%; 71 | --sidebar-accent-foreground: 240 4.8% 95.9%; 72 | --sidebar-border: 240 3.7% 15.9%; 73 | --sidebar-ring: 217.2 91.2% 59.8%; 74 | } 75 | } 76 | 77 | @layer base { 78 | * { 79 | @apply border-border; 80 | } 81 | body { 82 | @apply bg-background text-foreground; 83 | } 84 | } 85 | 86 | [data-rehype-pretty-code-fragment] { 87 | @apply relative text-white; 88 | } 89 | 90 | [data-rehype-pretty-code-fragment] code { 91 | @apply grid min-w-full break-words rounded-none border-0 bg-transparent p-0; 92 | counter-reset: line; 93 | box-decoration-break: clone; 94 | } 95 | 96 | [data-rehype-pretty-code-fragment] .line { 97 | @apply px-4 min-h-[1rem] py-0.5 w-full inline-block; 98 | } 99 | 100 | [data-rehype-pretty-code-fragment] [data-line-numbers] .line { 101 | @apply px-2; 102 | } 103 | 104 | [data-rehype-pretty-code-fragment] [data-line-numbers] > .line::before { 105 | @apply text-zinc-50/40 text-xs; 106 | counter-increment: line; 107 | content: counter(line); 108 | display: inline-block; 109 | width: 1.8rem; 110 | margin-right: 1.4rem; 111 | text-align: right; 112 | } 113 | 114 | [data-rehype-pretty-code-fragment] .line--highlighted { 115 | @apply bg-zinc-700/50; 116 | } 117 | 118 | [data-rehype-pretty-code-fragment] .line-highlighted span { 119 | @apply relative; 120 | } 121 | 122 | [data-rehype-pretty-code-fragment] .word--highlighted { 123 | @apply rounded-md bg-zinc-700/50 border-zinc-700/70 p-1; 124 | } 125 | 126 | .dark [data-rehype-pretty-code-fragment] .word--highlighted { 127 | @apply bg-zinc-900; 128 | } 129 | 130 | [data-rehype-pretty-code-title] { 131 | @apply mt-2 pt-6 px-4 text-sm font-medium text-foreground; 132 | } 133 | 134 | [data-rehype-pretty-code-title] + pre { 135 | @apply mt-2; 136 | } 137 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { OpenInV0Button } from "@/components/open-in-v0-button" 3 | import { HelloWorld } from "@/registry/new-york/hello-world/hello-world" 4 | import { ExampleForm } from "@/registry/new-york/example-form/example-form" 5 | import PokemonPage from "@/registry/new-york/complex-component/page" 6 | 7 | // This page displays items from the custom registry. 8 | // You are free to implement this with your own design as needed. 9 | 10 | export default function Home() { 11 | return ( 12 |
13 |
14 |

Custom Registry

15 |

16 | A custom registry for distributing code using shadcn. 17 |

18 |
19 |
20 |
21 |
22 |

23 | A simple hello world component 24 |

25 | 26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |
34 |

35 | A contact form with Zod validation. 36 |

37 | 38 |
39 |
40 | 41 |
42 |
43 | 44 |
45 |
46 |

47 | A complex component showing hooks, libs and components. 48 |

49 | 50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /app/registry/[name]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | import path from "path" 3 | import { promises as fs } from "fs" 4 | import { registryItemSchema } from "shadcn/registry" 5 | 6 | // Use the registry.json file to generate static paths. 7 | export const generateStaticParams = async () => { 8 | const registryData = await import("@/registry.json"); 9 | const registry = registryData.default; 10 | 11 | return registry.items.map((item) => ({ 12 | name: item.name, 13 | })); 14 | }; 15 | 16 | // This route shows an example for serving a component using a route handler. 17 | export async function GET( 18 | request: Request, 19 | { params }: { params: Promise<{ name: string }> } 20 | ) { 21 | try { 22 | const { name } = await params 23 | // Cache the registry import 24 | const registryData = await import("@/registry.json") 25 | const registry = registryData.default 26 | 27 | // Find the component from the registry. 28 | const component = registry.items.find((c) => c.name === name) 29 | 30 | // If the component is not found, return a 404 error. 31 | if (!component) { 32 | return NextResponse.json( 33 | { error: "Component not found" }, 34 | { status: 404 } 35 | ) 36 | } 37 | 38 | // Validate before file operations. 39 | const registryItem = registryItemSchema.parse(component) 40 | 41 | // If the component has no files, return a 400 error. 42 | if (!registryItem.files?.length) { 43 | return NextResponse.json( 44 | { error: "Component has no files" }, 45 | { status: 400 } 46 | ) 47 | } 48 | 49 | // Read all files in parallel. 50 | const filesWithContent = await Promise.all( 51 | registryItem.files.map(async (file) => { 52 | const filePath = path.join(process.cwd(), file.path) 53 | const content = await fs.readFile(filePath, "utf8") 54 | return { ...file, content } 55 | }) 56 | ) 57 | 58 | // Return the component with the files. 59 | return NextResponse.json({ ...registryItem, files: filesWithContent }) 60 | } catch (error) { 61 | console.error("Error processing component request:", error) 62 | return NextResponse.json({ error: "Something went wrong" }, { status: 500 }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /components/mode-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { useTheme } from "next-themes" 5 | 6 | import { Moon, Sun } from "lucide-react" 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function ModeToggle() { 10 | const { setTheme, resolvedTheme } = useTheme() 11 | const [, startTransition] = React.useTransition() 12 | 13 | return ( 14 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/open-in-v0-button.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button" 2 | import { cn } from "@/lib/utils" 3 | 4 | export function OpenInV0Button({ 5 | name, 6 | className, 7 | }: { name: string } & React.ComponentProps) { 8 | return ( 9 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ThemeProvider } from "next-themes" 4 | 5 | export function Providers({ children }: { children: React.ReactNode }) { 6 | return ( 7 | 13 | {children} 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if ( 3 | process.env.NODE_ENV === "production" || 4 | process.env.HIDE_TAILWIND_INDICATOR === "1" 5 | ) 6 | return null 7 | 8 | return ( 9 |
10 |
xs
11 |
sm
12 |
md
13 |
lg
14 |
xl
15 |
2xl
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
41 | )) 42 | CardTitle.displayName = "CardTitle" 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
53 | )) 54 | CardDescription.displayName = "CardDescription" 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
61 | )) 62 | CardContent.displayName = "CardContent" 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
73 | )) 74 | CardFooter.displayName = "CardFooter" 75 | 76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 77 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Textarea = React.forwardRef< 6 | HTMLTextAreaElement, 7 | React.ComponentProps<"textarea"> 8 | >(({ className, ...props }, ref) => { 9 | return ( 10 |