├── .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 |
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 |
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 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import { dirname } from "path";
2 | import { fileURLToPath } from "url";
3 | import { FlatCompat } from "@eslint/eslintrc";
4 |
5 | const __filename = fileURLToPath(import.meta.url);
6 | const __dirname = dirname(__filename);
7 |
8 | const compat = new FlatCompat({
9 | baseDirectory: __dirname,
10 | });
11 |
12 | const eslintConfig = [
13 | ...compat.extends("next/core-web-vitals", "next/typescript"),
14 | ];
15 |
16 | export default eslintConfig;
17 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.ts:
--------------------------------------------------------------------------------
1 | import type { NextConfig } from "next";
2 |
3 | const nextConfig: NextConfig = {
4 | outputFileTracingIncludes: {
5 | registry: ["./registry/**/*"],
6 | },
7 | /* config options here */
8 | };
9 |
10 | export default nextConfig;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "registry-template",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "registry:build": "shadcn build"
11 | },
12 | "dependencies": {
13 | "@radix-ui/react-label": "^2.1.1",
14 | "@radix-ui/react-slot": "^1.1.1",
15 | "@tanstack/react-query": "^5.64.1",
16 | "class-variance-authority": "^0.7.1",
17 | "clsx": "^2.1.1",
18 | "lucide-react": "^0.471.1",
19 | "next": "15.1.4",
20 | "next-themes": "^0.4.4",
21 | "react": "^19.0.0",
22 | "react-dom": "^19.0.0",
23 | "shadcn": "2.4.0-canary.12",
24 | "tailwind-merge": "^2.6.0",
25 | "tailwindcss-animate": "^1.0.7",
26 | "zod": "^3.24.1"
27 | },
28 | "devDependencies": {
29 | "@eslint/eslintrc": "^3",
30 | "@types/node": "^20",
31 | "@types/react": "^19",
32 | "@types/react-dom": "^19",
33 | "eslint": "^9",
34 | "eslint-config-next": "15.1.4",
35 | "postcss": "^8",
36 | "tailwindcss": "^3.4.1",
37 | "typescript": "^5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/r/complex-component.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "complex-component",
4 | "type": "registry:component",
5 | "title": "Complex Component",
6 | "description": "A complex component showing hooks, libs and components.",
7 | "registryDependencies": [
8 | "card"
9 | ],
10 | "files": [
11 | {
12 | "path": "registry/new-york/complex-component/page.tsx",
13 | "content": "import { cache } from \"react\"\nimport { PokemonCard } from \"@/registry/new-york/complex-component/components/pokemon-card\"\nimport { getPokemonList } from \"@/registry/new-york/complex-component/lib/pokemon\"\n\nconst getCachedPokemonList = cache(getPokemonList)\n\nexport default async function Page() {\n const pokemons = await getCachedPokemonList({ limit: 12 })\n\n if (!pokemons) {\n return null\n }\n\n return (\n \n
\n {pokemons.results.map((p) => (\n
\n ))}\n
\n
\n )\n}\n",
14 | "type": "registry:page",
15 | "target": "app/pokemon/page.tsx"
16 | },
17 | {
18 | "path": "registry/new-york/complex-component/components/pokemon-card.tsx",
19 | "content": "import { cache } from \"react\"\nimport { getPokemon } from \"@/registry/new-york/complex-component/lib/pokemon\"\nimport { Card, CardContent } from \"@/components/ui/card\"\nimport { PokemonImage } from \"@/registry/new-york/complex-component/components/pokemon-image\"\n\nconst cachedGetPokemon = cache(getPokemon)\n\nexport async function PokemonCard({ name }: { name: string }) {\n const pokemon = await cachedGetPokemon(name)\n\n if (!pokemon) {\n return null\n }\n\n return (\n \n \n \n {pokemon.name}
\n \n \n )\n}\n",
20 | "type": "registry:component"
21 | },
22 | {
23 | "path": "registry/new-york/complex-component/components/pokemon-image.tsx",
24 | "content": "\"use client\"\n\n/* eslint-disable @next/next/no-img-element */\nimport { usePokemonImage } from \"@/registry/new-york/complex-component/hooks/use-pokemon\"\n\nexport function PokemonImage({\n name,\n number,\n}: {\n name: string\n number: number\n}) {\n const imageUrl = usePokemonImage(number)\n\n if (!imageUrl) {\n return null\n }\n\n return
\n}\n",
25 | "type": "registry:component"
26 | },
27 | {
28 | "path": "registry/new-york/complex-component/lib/pokemon.ts",
29 | "content": "import { z } from \"zod\"\n\nexport async function getPokemonList({ limit = 10 }: { limit?: number }) {\n try {\n const response = await fetch(\n `https://pokeapi.co/api/v2/pokemon?limit=${limit}`\n )\n return z\n .object({\n results: z.array(z.object({ name: z.string() })),\n })\n .parse(await response.json())\n } catch (error) {\n console.error(error)\n return null\n }\n}\n\nexport async function getPokemon(name: string) {\n try {\n const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)\n\n if (!response.ok) {\n throw new Error(\"Failed to fetch pokemon\")\n }\n\n return z\n .object({\n name: z.string(),\n id: z.number(),\n sprites: z.object({\n front_default: z.string(),\n }),\n stats: z.array(\n z.object({\n base_stat: z.number(),\n stat: z.object({\n name: z.string(),\n }),\n })\n ),\n })\n .parse(await response.json())\n } catch (error) {\n console.error(error)\n return null\n }\n}\n",
30 | "type": "registry:lib"
31 | },
32 | {
33 | "path": "registry/new-york/complex-component/hooks/use-pokemon.ts",
34 | "content": "\"use client\"\n\n// Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry.\n\nexport function usePokemonImage(number: number) {\n return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png`\n}\n",
35 | "type": "registry:hook"
36 | }
37 | ]
38 | }
--------------------------------------------------------------------------------
/public/r/example-form.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "example-form",
4 | "type": "registry:component",
5 | "title": "Example Form",
6 | "description": "A contact form with Zod validation.",
7 | "dependencies": [
8 | "zod"
9 | ],
10 | "registryDependencies": [
11 | "button",
12 | "input",
13 | "label",
14 | "textarea",
15 | "card"
16 | ],
17 | "files": [
18 | {
19 | "path": "registry/new-york/example-form/example-form.tsx",
20 | "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n Card,\n CardTitle,\n CardHeader,\n CardDescription,\n CardContent,\n CardFooter,\n} from \"@/components/ui/card\"\nimport { Input } from \"@/components/ui/input\"\nimport { Label } from \"@/components/ui/label\"\nimport { Button } from \"@/components/ui/button\"\nimport { Textarea } from \"@/components/ui/textarea\"\nimport { z } from \"zod\"\n\nconst exampleFormSchema = z.object({\n name: z.string().min(1),\n email: z.string().email(),\n message: z.string().min(1),\n})\n\nexport function ExampleForm() {\n const [pending, setPending] = React.useState(false)\n const [state, setState] = React.useState({\n defaultValues: {\n name: \"\",\n email: \"\",\n message: \"\",\n },\n success: false,\n errors: {\n name: \"\",\n email: \"\",\n message: \"\",\n },\n })\n\n const handleSubmit = React.useCallback(\n (e: React.FormEvent) => {\n e.preventDefault()\n setPending(true)\n\n const formData = new FormData(e.target as HTMLFormElement)\n const data = Object.fromEntries(formData.entries())\n const result = exampleFormSchema.safeParse(data)\n\n if (!result.success) {\n setState({\n ...state,\n errors: Object.fromEntries(\n Object.entries(result.error.flatten().fieldErrors).map(\n ([key, value]) => [key, value?.[0] ?? \"\"]\n )\n ) as Record,\n })\n setPending(false)\n return\n }\n\n setPending(false)\n },\n [state]\n )\n\n return (\n \n \n How can we help?\n \n Need help with your project? We're here to assist you.\n \n \n \n \n )\n}\n",
21 | "type": "registry:component"
22 | }
23 | ]
24 | }
--------------------------------------------------------------------------------
/public/r/hello-world.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3 | "name": "hello-world",
4 | "type": "registry:component",
5 | "title": "Hello World",
6 | "description": "A simple hello world component",
7 | "registryDependencies": [
8 | "button"
9 | ],
10 | "files": [
11 | {
12 | "path": "registry/new-york/hello-world/hello-world.tsx",
13 | "content": "export function HelloWorld() {\n return Hello World
\n}\n",
14 | "type": "registry:component"
15 | }
16 | ]
17 | }
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/registry.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema/registry.json",
3 | "name": "acme",
4 | "homepage": "https://acme.com",
5 | "items": [
6 | {
7 | "name": "hello-world",
8 | "type": "registry:component",
9 | "title": "Hello World",
10 | "description": "A simple hello world component",
11 | "registryDependencies": ["button"],
12 | "files": [
13 | {
14 | "path": "registry/new-york/hello-world/hello-world.tsx",
15 | "type": "registry:component"
16 | }
17 | ]
18 | },
19 | {
20 | "name": "example-form",
21 | "type": "registry:component",
22 | "title": "Example Form",
23 | "description": "A contact form with Zod validation.",
24 | "dependencies": ["zod"],
25 | "registryDependencies": ["button", "input", "label", "textarea", "card"],
26 | "files": [
27 | {
28 | "path": "registry/new-york/example-form/example-form.tsx",
29 | "type": "registry:component"
30 | }
31 | ]
32 | },
33 | {
34 | "name": "complex-component",
35 | "type": "registry:component",
36 | "title": "Complex Component",
37 | "description": "A complex component showing hooks, libs and components.",
38 | "registryDependencies": ["card"],
39 | "files": [
40 | {
41 | "path": "registry/new-york/complex-component/page.tsx",
42 | "type": "registry:page",
43 | "target": "app/pokemon/page.tsx"
44 | },
45 | {
46 | "path": "registry/new-york/complex-component/components/pokemon-card.tsx",
47 | "type": "registry:component"
48 | },
49 | {
50 | "path": "registry/new-york/complex-component/components/pokemon-image.tsx",
51 | "type": "registry:component"
52 | },
53 | {
54 | "path": "registry/new-york/complex-component/lib/pokemon.ts",
55 | "type": "registry:lib"
56 | },
57 | {
58 | "path": "registry/new-york/complex-component/hooks/use-pokemon.ts",
59 | "type": "registry:hook"
60 | }
61 | ]
62 | }
63 | ]
64 | }
65 |
--------------------------------------------------------------------------------
/registry/layout.tsx:
--------------------------------------------------------------------------------
1 | import "@/app/globals.css"
2 |
3 | export default function Layout({ children }: { children: React.ReactNode }) {
4 | return <>{children}>
5 | }
6 |
--------------------------------------------------------------------------------
/registry/new-york/complex-component/components/pokemon-card.tsx:
--------------------------------------------------------------------------------
1 | import { cache } from "react"
2 | import { getPokemon } from "@/registry/new-york/complex-component/lib/pokemon"
3 | import { Card, CardContent } from "@/components/ui/card"
4 | import { PokemonImage } from "@/registry/new-york/complex-component/components/pokemon-image"
5 |
6 | const cachedGetPokemon = cache(getPokemon)
7 |
8 | export async function PokemonCard({ name }: { name: string }) {
9 | const pokemon = await cachedGetPokemon(name)
10 |
11 | if (!pokemon) {
12 | return null
13 | }
14 |
15 | return (
16 |
17 |
18 |
21 | {pokemon.name}
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/registry/new-york/complex-component/components/pokemon-image.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | /* eslint-disable @next/next/no-img-element */
4 | import { usePokemonImage } from "@/registry/new-york/complex-component/hooks/use-pokemon"
5 |
6 | export function PokemonImage({
7 | name,
8 | number,
9 | }: {
10 | name: string
11 | number: number
12 | }) {
13 | const imageUrl = usePokemonImage(number)
14 |
15 | if (!imageUrl) {
16 | return null
17 | }
18 |
19 | return
20 | }
21 |
--------------------------------------------------------------------------------
/registry/new-york/complex-component/hooks/use-pokemon.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Totally unnecessary hook, but it's a good example of how to use a hook in a custom registry.
4 |
5 | export function usePokemonImage(number: number) {
6 | return `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png`
7 | }
8 |
--------------------------------------------------------------------------------
/registry/new-york/complex-component/lib/pokemon.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod"
2 |
3 | export async function getPokemonList({ limit = 10 }: { limit?: number }) {
4 | try {
5 | const response = await fetch(
6 | `https://pokeapi.co/api/v2/pokemon?limit=${limit}`
7 | )
8 | return z
9 | .object({
10 | results: z.array(z.object({ name: z.string() })),
11 | })
12 | .parse(await response.json())
13 | } catch (error) {
14 | console.error(error)
15 | return null
16 | }
17 | }
18 |
19 | export async function getPokemon(name: string) {
20 | try {
21 | const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)
22 |
23 | if (!response.ok) {
24 | throw new Error("Failed to fetch pokemon")
25 | }
26 |
27 | return z
28 | .object({
29 | name: z.string(),
30 | id: z.number(),
31 | sprites: z.object({
32 | front_default: z.string(),
33 | }),
34 | stats: z.array(
35 | z.object({
36 | base_stat: z.number(),
37 | stat: z.object({
38 | name: z.string(),
39 | }),
40 | })
41 | ),
42 | })
43 | .parse(await response.json())
44 | } catch (error) {
45 | console.error(error)
46 | return null
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/registry/new-york/complex-component/page.tsx:
--------------------------------------------------------------------------------
1 | import { cache } from "react"
2 | import { PokemonCard } from "@/registry/new-york/complex-component/components/pokemon-card"
3 | import { getPokemonList } from "@/registry/new-york/complex-component/lib/pokemon"
4 |
5 | const getCachedPokemonList = cache(getPokemonList)
6 |
7 | export default async function Page() {
8 | const pokemons = await getCachedPokemonList({ limit: 12 })
9 |
10 | if (!pokemons) {
11 | return null
12 | }
13 |
14 | return (
15 |
16 |
17 | {pokemons.results.map((p) => (
18 |
19 | ))}
20 |
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/registry/new-york/example-form/example-form.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import {
5 | Card,
6 | CardTitle,
7 | CardHeader,
8 | CardDescription,
9 | CardContent,
10 | CardFooter,
11 | } from "@/components/ui/card"
12 | import { Input } from "@/components/ui/input"
13 | import { Label } from "@/components/ui/label"
14 | import { Button } from "@/components/ui/button"
15 | import { Textarea } from "@/components/ui/textarea"
16 | import { z } from "zod"
17 |
18 | const exampleFormSchema = z.object({
19 | name: z.string().min(1),
20 | email: z.string().email(),
21 | message: z.string().min(1),
22 | })
23 |
24 | export function ExampleForm() {
25 | const [pending, setPending] = React.useState(false)
26 | const [state, setState] = React.useState({
27 | defaultValues: {
28 | name: "",
29 | email: "",
30 | message: "",
31 | },
32 | success: false,
33 | errors: {
34 | name: "",
35 | email: "",
36 | message: "",
37 | },
38 | })
39 |
40 | const handleSubmit = React.useCallback(
41 | (e: React.FormEvent) => {
42 | e.preventDefault()
43 | setPending(true)
44 |
45 | const formData = new FormData(e.target as HTMLFormElement)
46 | const data = Object.fromEntries(formData.entries())
47 | const result = exampleFormSchema.safeParse(data)
48 |
49 | if (!result.success) {
50 | setState({
51 | ...state,
52 | errors: Object.fromEntries(
53 | Object.entries(result.error.flatten().fieldErrors).map(
54 | ([key, value]) => [key, value?.[0] ?? ""]
55 | )
56 | ) as Record,
57 | })
58 | setPending(false)
59 | return
60 | }
61 |
62 | setPending(false)
63 | },
64 | [state]
65 | )
66 |
67 | return (
68 |
69 |
70 | How can we help?
71 |
72 | Need help with your project? We're here to assist you.
73 |
74 |
75 |
162 |
163 | )
164 | }
165 |
--------------------------------------------------------------------------------
/registry/new-york/hello-world/hello-world.tsx:
--------------------------------------------------------------------------------
1 | export function HelloWorld() {
2 | return Hello World
3 | }
4 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 | import tailwindAnimate from "tailwindcss-animate"
3 |
4 | export default {
5 | darkMode: ["class"],
6 | content: [
7 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
8 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
9 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
10 | "./registry/**/*.{js,ts,jsx,tsx,mdx}",
11 | ],
12 | theme: {
13 | extend: {
14 | colors: {
15 | background: "hsl(var(--background))",
16 | foreground: "hsl(var(--foreground))",
17 | card: {
18 | DEFAULT: "hsl(var(--card))",
19 | foreground: "hsl(var(--card-foreground))",
20 | },
21 | popover: {
22 | DEFAULT: "hsl(var(--popover))",
23 | foreground: "hsl(var(--popover-foreground))",
24 | },
25 | primary: {
26 | DEFAULT: "hsl(var(--primary))",
27 | foreground: "hsl(var(--primary-foreground))",
28 | },
29 | secondary: {
30 | DEFAULT: "hsl(var(--secondary))",
31 | foreground: "hsl(var(--secondary-foreground))",
32 | },
33 | muted: {
34 | DEFAULT: "hsl(var(--muted))",
35 | foreground: "hsl(var(--muted-foreground))",
36 | },
37 | accent: {
38 | DEFAULT: "hsl(var(--accent))",
39 | foreground: "hsl(var(--accent-foreground))",
40 | },
41 | destructive: {
42 | DEFAULT: "hsl(var(--destructive))",
43 | foreground: "hsl(var(--destructive-foreground))",
44 | },
45 | border: "hsl(var(--border))",
46 | input: "hsl(var(--input))",
47 | ring: "hsl(var(--ring))",
48 | chart: {
49 | "1": "hsl(var(--chart-1))",
50 | "2": "hsl(var(--chart-2))",
51 | "3": "hsl(var(--chart-3))",
52 | "4": "hsl(var(--chart-4))",
53 | "5": "hsl(var(--chart-5))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | },
62 | },
63 | plugins: [tailwindAnimate],
64 | } satisfies Config
65 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2017",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "noEmit": true,
9 | "esModuleInterop": true,
10 | "module": "esnext",
11 | "moduleResolution": "bundler",
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "jsx": "preserve",
15 | "incremental": true,
16 | "plugins": [
17 | {
18 | "name": "next"
19 | }
20 | ],
21 | "paths": {
22 | "@/*": ["./*"]
23 | }
24 | },
25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
26 | "exclude": ["node_modules"]
27 | }
28 |
--------------------------------------------------------------------------------