The registry format
When shadcn released its registry spec, it gave the community something more valuable than a component library: a standard JSON schema for distributable UI components. Any server that returns JSON in this shape becomes a first-class citizen of the npx shadcn add workflow. ParticleUI is built entirely on top of this. Understanding the schema is the first step to building your own registry.
Each registry item is a JSON object with a handful of fields. The required ones are name (unique identifier, kebab-case), type (either registry:ui for standalone components or registry:block for composed sections), and files (an array of file objects). Optional but important fields include dependencies (npm packages to install), registryDependencies (other registry items this one depends on), title, description, and categories for search and navigation.
{
"name": "marquee",
"type": "registry:ui",
"title": "Marquee",
"description": "A GPU-accelerated horizontal scroll marquee with pause-on-hover.",
"categories": ["animation", "display"],
"dependencies": [],
"registryDependencies": [],
"files": [
{
"path": "components/ui/marquee.tsx",
"type": "registry:ui",
"content": ""use client"\n\nimport * as React from \"react\"\n..."
}
]
}The content field inside each file is the raw source code of the component, escaped as a JSON string. When the CLI writes the file to the user's project, it uses the path field to determine where to put it — relative to the project root by default, or adjusted by the user's components.json aliases.
How npx shadcn add works
The shadcn CLI is remarkably straightforward once you read its source. When you run npx shadcn add https://particleui.dev/r/marquee.json, it does exactly five things in order: fetch the JSON from your URL, read the files array and write each file to your project (respecting your components.json path aliases), run npm install (or your detected package manager) for everything in dependencies, recursively fetch and install each item in registryDependencies, and finally print a confirmation.
The one-line install for any ParticleUI component looks like this:
npx shadcn add https://particleui.dev/r/marquee.json # Or multiple components at once: npx shadcn add \ https://particleui.dev/r/marquee.json \ https://particleui.dev/r/glow-card.json \ https://particleui.dev/r/beam.json
Because the CLI handles recursive dependencies, you never need to manually track what a component needs. If glow-card depends on card which depends on cn utility, all three are installed in the right order automatically. The user's project always ends up in a clean state.
Building the index
A registry needs two layers of JSON: the full item files (with source code) and a lightweight index for navigation and search. The index lives at /r/index.json and is an array of objects that describe each component without embedding their source. This keeps the index small — fast to fetch, easy to parse — while the full files are fetched on demand.
// /r/index.json — lightweight catalog (no "files[].content")
[
{
"name": "marquee",
"type": "registry:ui",
"title": "Marquee",
"description": "GPU-accelerated horizontal scroll marquee.",
"categories": ["animation", "display"]
},
{
"name": "glow-card",
"type": "registry:ui",
"title": "GlowCard",
"description": "Card with animated radial glow on hover.",
"categories": ["animation", "layout"]
}
]Your docs site fetches the index once at build time and uses it to render the component gallery, power search, and generate static pages. The full item JSON is fetched per-component for preview rendering. This separation of concerns means you can cache the index aggressively at the CDN level while still serving fresh component source on each item endpoint.
The multi-framework challenge
React, Vue, and Svelte are not interchangeable. A React component is JSX; a Vue component is a Single File Component with <template>, <script>, and <style> blocks; a Svelte component is .svelte with its own reactivity syntax. You cannot serve one JSON file to all three ecosystems.
ParticleUI handles this with separate registry paths per framework, each built from the same logical component specification. The React registry lives at /r/react/, Vue at /r/vue/, Svelte at /r/svelte/. Internally, the build script reads a single component.config.ts file that defines the component's interface, then generates three output files — one per framework — from shared logic and framework-specific templates.
# React npx shadcn add https://particleui.dev/r/react/marquee.json # Vue npx shadcn add https://particleui.dev/r/vue/marquee.json # Svelte npx shadcn add https://particleui.dev/r/svelte/marquee.json
The docs site detects which framework a user has configured (via their components.json or an explicit toggle) and serves the right install command. This is not just a different file extension — the CSS class approach, event binding syntax, and slot/children patterns differ meaningfully across frameworks, so each output is a genuine adaptation rather than a naive find-and-replace.
Pro gating
Some ParticleUI components are available only on paid plans. The implementation is simple and fits naturally into the registry pattern. Each pro component endpoint is an API route rather than a static JSON file. When the CLI requests it, the route reads the Authorization header that shadcn passes from components.json:
// app/api/r/[slug]/route.ts (simplified)
export async function GET(req: Request, { params }: { params: { slug: string } }) {
const token = req.headers.get("Authorization")?.replace("Bearer ", "")
if (!token) {
return Response.json(
{ error: "Unauthorized", message: "A license key is required." },
{ status: 401 }
)
}
const license = await db.licenses.findByTokenHash(sha256(token))
if (!license || license.status !== "active") {
return Response.json(
{ error: "Forbidden", message: "Invalid or expired license key." },
{ status: 403 }
)
}
// Fire-and-forget install tracking (non-blocking)
void db.installs.record({ slug: params.slug, licenseId: license.id })
return Response.json(await getComponentJson(params.slug))
}components.json under the headers field. The CLI passes it automatically on every install — no manual token management per component.The entire auth path adds roughly 10–30ms of latency (a DB lookup and a hash comparison), which is imperceptible during an install. Install tracking is fire-and-forget — the response is never delayed waiting for it. This keeps the developer experience fast while giving you meaningful usage analytics.