components.json
Every field opsinjs reads, what it does, and the one entry you have to add by hand.
Overview
components.json is the shadcn CLI's configuration file, and opsinjs reuses it
rather than introducing one of its own. If you already have shadcn/ui in your
project, you already have this file and adding opsinjs is one entry.
The URL below is not serving yet
opsinjs.pensievelabs.org is the intended canonical host and does not resolve
today, so an add against the entry below fails before the CLI reaches a registry.
The sixty built components resolve from any host that is serving the registry
items, which are /r/status-pill.json and the rest. Point the entry at one, and
everything on this page applies unchanged.
Troubleshooting.
Add the @opsinjs entry
Run npx shadcn@latest init first if this project has never had it. That is
what writes components.json, and it is also what writes lib/utils.ts, which
exports the cn helper every opsinjs component imports. The registry does not
ship that file on purpose. A third copy would collide with the one you already
have and may have extended, which is
ADR 0010.
So a project that skips init gets components that land in the right place and
then every one of them fails to typecheck on the same missing module.
The registries entry is then the only change opsinjs strictly requires, and the only one you have to make by hand:
{
"registries": {
"@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
}
}{name} is substituted by the CLI, so @opsinjs/result-card resolves to
https://opsinjs.pensievelabs.org/r/result-card.json. The URL is guessable on
purpose: an
agent that knows a component id can fetch its registry item without discovering an
index first.
Two safety notes about this line, because a registry entry is a remote code source:
opsinjs.pensievelabs.orgis the only canonical registry host. Anything else serving@opsinjsis not us. The canonical domain, npm scope and registry URL are published in Official resources precisely so impersonation is checkable.shadcn addwrites source files into your repository. Review the diff the first time you add from any registry, exactly as you would review a dependency.
Know what the other fields do
The rest of the file is shadcn's, but opsinjs reads several fields and gets them wrong quietly if they are wrong.
| Field | What it does | What opsinjs needs from it |
|---|---|---|
$schema | Editor validation | Nothing, but keep it |
style | Selects a style variant within a registry | Required: the CLI validates against a schema that demands it, so a file without it is rejected whole. It reaches opsinjs only through the URL template. The CLI substitutes it wherever a registry URL contains {style}. So it matters if you point the entry at /r/styles/{style}/{name}.json, which Registry documents, and not with the /r/{name}.json entry this page publishes |
rsc | Whether the project uses server components | Controls whether a "use client" directive is written into files that need one. Wrong value, wrong build errors |
tsx | TypeScript or JavaScript output | opsinjs source is TypeScript; false is not supported |
tailwind.css | Path to your main stylesheet | Where the token layer import is written |
tailwind.baseColor | The neutral ramp | shadcn's, not opsinjs's. The opsinjs token layer defines its own neutrals |
tailwind.cssVariables | Variables versus utility classes | Must be true. The whole token system is CSS custom properties |
tailwind.prefix | Utility prefix | Supported, but set it before you copy anything in. Changing it later means editing copied source |
iconLibrary | Which icon set | opsinjs targets lucide. A different set means substituting icons in copied source by hand |
aliases.components | Where components go | Must match your tsconfig.json paths |
aliases.ui | Where primitives go | Same |
aliases.utils | Where cn lives | Copied components import it from here |
aliases.lib, aliases.hooks | Supporting code | Same rule: must match tsconfig |
registries | Namespaced registries | Where @opsinjs goes |
The alias rule that causes the most trouble
aliases and your tsconfig.json paths are two independent statements of the
same fact, and nothing reconciles them. The CLI writes files to the alias; your
bundler and typechecker resolve the tsconfig path. When they disagree, files land
somewhere real and imports fail somewhere else, and the error message names
neither file.
In a workspace, both must be correct in the app being written to, not in the repository root. Monorepo has the details.
A complete example
A consumer project's file, with the opsinjs entry in place. It is complete, and
the style key is the reason it has to be: the CLI validates the whole file
against its schema before it fetches anything, style is required by that
schema, and a components.json it rejects writes nothing at all rather than
failing on the item you asked for. new-york is what shadcn init writes; keep
whatever value it wrote for you rather than copying this one over it.
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"ui": "@/components/ui",
"utils": "@/lib/utils",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@opsinjs": "https://opsinjs.pensievelabs.org/r/{name}.json"
}
}Verify it worked
- A namespaced add resolves.
@opsinjs/status-pillfetches rather than erroring on an unknown registry. - The file lands where the alias says. If it does not, the alias and the tsconfig path disagree.
- The
cnimport inside the copied file resolves. It comes fromaliases.utilsand it breaks two ways: the alias points somewhere real but wrong, orlib/utils.tswas never written becauseinitwas never run. IdenticalTS2307errors on every copied file at once is the second one; a single file resolving to the wrong place is the first. "use client"appears only where expected. If it is on everything,rscis probablyfalsewhen it should betrue.
Troubleshooting
Unknown registry @opsinjs. The registries entry is missing, or the key is
missing its @. The key includes the @; the URL template must include {name}.
Components land in the wrong folder. aliases versus tsconfig paths.
cn is not exported, or Cannot find module '@/lib/utils'. aliases.utils
points somewhere that does not export it, or shadcn init was never run and the
file is not there at all. Nothing in the registry creates it.
Icons are missing or wrong. iconLibrary is not lucide. There is no automatic
substitution; you are editing copied source.
Everything is a client component. rsc is false.
Next
- Monorepo covers the same fields, one directory level harder.
- Registry says what the namespace resolves to and what a registry item contains.
- Official resources has the canonical hosts, so you can tell a real registry from a lookalike.