Adding a component
Every item follows the same shape. Deviating from it is what produces components that install cleanly here and break in a consumer's project.
1. Write the source under registry/
Everything published lives under registry/, mirroring the tree a consumer ends up with — registry/components/custom/ for components, registry/hooks/ for hooks, registry/lib/ for utilities. Nothing outside registry/ is ever distributed.
Author against the standard aliases, exactly as a consumer would write them:
'use client'
import * as React from 'react'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { formatMoney } from '@/lib/kiota-format'
export function PayoutSummary({ className }: { className?: string }) {
return <div className={cn('text-sm', className)}>{formatMoney(125050)}</div>
}Rules that matter for distribution:
- Named exports, so consumers can find things without guessing a default.
'use client'on anything with state, effects or handlers. Consumers may drop the file into a server component tree; without the directive it fails at their build, not yours.- Never import anything from outside
registry/other than shadcn's own files (@/components/ui/*,@/lib/utils). Everything undercomponents/site/,components/examples/andlib/is docs-site scaffolding; importing it ships a file that cannot resolve in a consumer's project. - Never open a file with a load-bearing comment. The CLI strips the leading comment block —
/** … */or a run of//lines — from every file it installs. Put an import or an export above anything a consumer needs to read, or it silently never reaches them. - Never register a binary file. Item contents travel as UTF-8 JSON strings, so fonts and raster images arrive corrupted.
npm run registry:buildfails on these — see Theming for how to ship fonts instead. - Use the token utilities, not raw colours.
bg-success-light,text-danger,border-warning/25andbg-primaryall resolve from design tokens the CLI installs alongside your item. Never hardcode a hex or a Tailwind palette colour such asbg-emerald-500— it will not follow a consumer's theme. - Declare new tokens in
app/globals.css, never incssVars. The blocks markedregistry:beginare the source of truth;npm run tokens:buildwrites them into the registry. Hand-editedcssVarsare overwritten on the next build.
2. Register it in the nearest sub-catalog
Add an entry to the registry.json in that directory — registry/components/custom/registry.json for a component. Paths are relative to that file and cannot use ../.
{
"name": "payout-summary",
"type": "registry:component",
"title": "Payout Summary",
"description": "One-line summary of a payout's amount and destination.",
"author": "KiotaPay <engineering@kiotapay.com>",
"dependencies": ["lucide-react"],
"registryDependencies": ["card", "{REGISTRY_URL}/r/kiota-theme.json"],
"files": [
{
"path": "payout-summary.tsx",
"type": "registry:component",
"target": "@components/custom/payout-summary.tsx"
}
],
"categories": ["display", "payments"],
"docs": "Anything a consumer must do after installing."
}Checklist for the entry:
typeisregistry:component, neverregistry:uitargetstarts with@components/custom/- every npm import appears in
dependencies - every shadcn primitive appears in
registryDependenciesas a bare name - dependencies on our own items use
{REGISTRY_URL}/r/<name>.json, never@kiota/…or./…— see How it works for why {REGISTRY_URL}/r/kiota-theme.jsonis listed if the file uses any design token (bg-primary,bg-success-light,text-danger, …) orkiota-format
A missing registryDependencies entry is invisible here — the primitive already exists in this repository, so
previews render fine. It only fails in a consumer's project, as an unresolved import. Check the list against the
file's imports rather than against whether the preview works.
3. Add a new sub-catalog if you need a new directory
Creating a directory the root catalog does not know about means adding it to registry.json:
{
"include": [
"registry/theme/registry.json",
"registry/lib/registry.json",
"registry/hooks/registry.json",
"registry/components/custom/registry.json",
"registry/components/charts/registry.json"
]
}Split by directory rather than by theme. The no-../ rule means a catalog can only describe files beneath itself, so directory boundaries are the only ones that actually work.
If the new directory is one consumers will receive files into, mirror their path under registry/ — registry/components/charts/ targeting @components/charts/. Add a matching paths entry in tsconfig.json so it resolves locally with no further configuration.
4. Write the example, then the page
Examples live in components/examples/ as default exports, one per file. The generator turns the filename into the identifier, so payout-summary-demo.tsx becomes the example named payout-summary-demo.
import { PayoutSummary } from '@/components/custom/payout-summary'
export default function PayoutSummaryDemo() {
return <PayoutSummary />
}Regenerate the map, then reference the example by name in MDX:
npm run examples:build<ComponentPreview name="payout-summary-demo" />The Code tab renders that file's real source. It cannot drift from the preview above it, which is the reason examples are separate files rather than snippets pasted into the page.
npm run dev and npm run build both regenerate the map first, so you only need examples:build by hand when adding
a file while the dev server is already running.
Then add app/(docs)/components/<name>/page.mdx following the existing pages — description, <InstallCommand />, usage, props table, examples — and a sidebar entry in lib/nav.ts.
(docs) is a Next.js route group: the parentheses keep the directory out of the URL, so the page above is served at /components/<name>. Every MDX page lives inside it, which leaves the app/ root free for anything that is not documentation — a route handler, an OG-image endpoint, a playground.
5. Verify before publishing
npm run typecheck
npm run registry:buildThe build fails loudly on a missing file or a malformed entry, but it cannot tell you that a registryDependencies list is incomplete. The only real check is installing into a scratch project:
npx shadcn@latest add http://localhost:3005/r/payout-summary.json