KKiotaPay UI
Browse documentation

How the registry works

A shadcn registry is not a package. It is a set of static JSON files served over HTTP, each containing the literal source of a component as a string alongside its dependencies. The CLI fetches one, writes the files into your repository, rewrites import aliases to match your components.json, and installs the npm packages it names.

That is the whole mechanism. No runtime, no resolver, no node_modules entry.

The two schemas

registry.json is the catalog at the repository root. Ours only lists where the real entries live:

{
  "$schema": "https://ui.shadcn.com/schema/registry.json",
  "name": "kiota",
  "homepage": "https://ui.kiotapay.com",
  "include": [
    "registry/lib/registry.json",
    "registry/hooks/registry.json",
    "registry/components/custom/registry.json"
  ]
}

include composes the catalog from several files instead of one growing array. Each included path must point at a file literally named registry.json, and paths inside it cannot use ../ — an included catalog can only describe files at or below its own directory. That constraint is what determines the layout below.

registry-item.json is one entry per item:

{
  "name": "status-badge",
  "type": "registry:component",
  "title": "Status Badge",
  "description": "Payment status pill.",
  "dependencies": ["class-variance-authority"],
  "registryDependencies": ["badge", "{REGISTRY_URL}/r/kiota-theme.json"],
  "files": [
    {
      "path": "status-badge.tsx",
      "type": "registry:component",
      "target": "@components/custom/status-badge.tsx"
    }
  ]
}

Repository layout

Everything published lives under registry/, which mirrors the tree a consumer ends up with. Everything outside it belongs to the docs site and is never distributed:

  registry.json                          ← root catalog, include[] only

  registry/                              ← the published surface
    components/custom/
      registry.json                      ← sub-catalog
      money-input.tsx
      status-badge.tsx
      copy-field.tsx
    hooks/
      registry.json
      use-copy.ts
    lib/
      registry.json                      ← also holds kiota-theme + kiota-style
      kiota-format.ts

  components/                            ← docs site, never published
    ui/                                  ← vendored shadcn primitives
    site/                                ← CodeBlock, ComponentPreview, sidebar, …
    examples/                            ← the demos rendered in previews
  lib/
    utils.ts                             ← vendored shadcn `cn`
    nav.ts  highlight.ts  registry.ts    ← docs site
  app/(docs)/                            ← documentation pages
  public/r/                              ← generated output, served at /r/

The rule is one directory deep: if it is under registry/, it ships; if it is not, it does not. Reviewing "what am I publishing?" means reading one directory, and components/site/ may import from registry/ while nothing in registry/ may import back out.

components/ui/ and lib/utils.ts are vendored rather than ours. They sit at exactly those paths because that is where components.json aliases point and where a consumer's copies land — which is what makes @/components/ui/input name the same file on both sides.

Because registry/ mirrors the consumer tree, a component here imports exactly what a consumer's copy imports:

import { Input } from '@/components/ui/input'    // shadcn primitive
import { formatMoney } from '@/lib/kiota-format' // ours, published

Neither needs rewriting to be correct on the other side. tsconfig.json maps the published prefixes into registry/ and leaves everything else at the project root:

{
  "paths": {
    "@/components/custom/*": ["./registry/components/custom/*"],
    "@/hooks/*": ["./registry/hooks/*"],
    "@/lib/kiota-format": ["./registry/lib/kiota-format"],
    "@/*": ["./*"]
  }
}

TypeScript matches the most specific pattern, so @/lib/kiota-format resolves into the registry while @/lib/utils and @/lib/nav stay at the root.

These aliases are deliberately narrow rather than a blanket "@/*": ["./registry/*", "./*"] fallback. Blanket shadowing silently resolved @/lib/registry to the catalog registry/lib/registry.json instead of the docs module of the same name. Listing prefixes explicitly costs one line per published lib file and fails loudly — module not found — instead of quietly resolving to the wrong thing.

kiota-theme and kiota-style live in registry/lib/registry.json rather than a directory of their own. A catalog cannot reference files above itself, and kiota-theme ships kiota-format.ts, so its entry has to sit beside that file.

Landing in custom/ instead of ui/

Two fields control the destination.

type sets the default: registry:ui writes to your ui alias, registry:component to your components alias, registry:hook to hooks, registry:lib to lib.

files[].target overrides it, and alias-prefixed targets resolve against the consumer's own components.json:

TargetResolves to
@components/custom/foo.tsx<aliases.components>/custom/foo.tsx
@ui/foo.tsx<aliases.ui>/foo.tsx
@hooks/use-foo.ts<aliases.hooks>/use-foo.ts
@lib/format.ts<aliases.lib>/format.ts
~/some/path.tsproject root, verbatim

Every item here uses registry:component with a @components/custom/… target.

Do not use registry:ui for your own components. That type writes into components/ui/, the directory shadcn owns — an item named button would overwrite a consumer's shadcn button.tsx without warning.

Depending on shadcn

Two separate mechanisms, worth keeping distinct.

registryDependencies pulls the primitives in. A bare name always resolves against the official shadcn registry, never against ours — "badge" means shadcn's badge.

For dependencies on our own items we use absolute URLs, stamped at build time from NEXT_PUBLIC_REGISTRY_URL. The two shorter-looking alternatives both fail:

Absolute URLs work in every case, so items here install correctly with no configuration at all. The catalogs carry a {REGISTRY_URL} placeholder, and scripts/stamp-registry-url.mjs substitutes it after shadcn build.

Import rewriting handles the source. Author against the standard aliases:

import { Badge } from '@/components/ui/badge'
import { cn } from '@/lib/utils'

The CLI rewrites those to match the consumer's aliases, so a project using src/ gets correct paths.

Cross-references between our own items are resolved through each file's declared target, not by string-matching the source directory. An import of @/registry/kiota/probe-b in a file whose sibling targets @components/custom/probe-b.tsx is delivered as @/components/custom/probe-b. Source layout and destination layout are therefore free to differ.

We do not rely on that here: because registry/ already mirrors the consumer tree, our imports are consumer-correct as written and pass through untouched. The rewriting is a safety net rather than a load-bearing mechanism, which is one less thing to get wrong.

Building and serving

npm run registry:build

That is four steps in order:

  1. tokens:build — parses the marked blocks in app/globals.css into each item's cssVars.
  2. registry:clean — deletes public/r/ outright.
  3. shadcn build — reads the root catalog, resolves every include, inlines each file's contents, and writes flat JSON into public/r/.
  4. stamp + check — substitutes {REGISTRY_URL} and fails on any binary asset.

Because this is a Next.js app, the output is served statically: the docs site and the registry are one deployment.

Step 2 exists because shadcn build writes but never deletes. Renaming or removing an item leaves its old JSON sitting in public/r/, still served and still installable, describing something that no longer exists — and the build reports success. Wiping the directory first makes the published set always equal the catalog.

Only public/r/ is removed. public/theme-files/ and anything else in public/ is untouched, and public/r/ is gitignored so a stale file cannot be committed either.

Renaming an item is still a breaking change for anyone who already installed it: their next add of the old URL gets a 404. Clearing the directory only stops you serving a stale copy — it cannot fix a consumer's pinned command. Prefer adding a new item and leaving the old one in place until nothing references it.

https://kiotapay-ui-staging.staging.kiotapay.com/r/registry.json     # the catalog
https://kiotapay-ui-staging.staging.kiotapay.com/r/money-input.json   # one item

The origin comes from NEXT_PUBLIC_REGISTRY_URL. Every install command shown across these docs is generated from it, so moving hosts is a one-line environment change with no snippets to update.

Known limits