---
title: "components.json"
description: "Every field opsinjs reads, what it does, and the one entry you have to add by hand."
url: "https://opsinjs.pensievelabs.org/start/installation/components-json"
source: "https://opsinjs.pensievelabs.org/start/installation/components-json.md"
section: "Start here"
kind: "guide"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["components.json", "registry configuration", "shadcn config", "opsinjs namespace"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="guide" />

## Overview [#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.

<Callout title="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](../troubleshooting.mdx#nothing-is-published-yet).
</Callout>

## Add the `@opsinjs` entry [#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](../../project/decisions/0010-shared-code-ships-with-every-item.mdx).
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:

```json title="components.json"
{
  "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.org` is the only canonical registry host.** Anything else serving
  `@opsinjs` is not us. The canonical domain, npm scope and registry URL are
  published in [Official resources](../../project/official-resources.mdx)
  precisely so impersonation is checkable.
* **`shadcn add` writes 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 [#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](../../registry/index.mdx) 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 [#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](./monorepo.mdx) has the details.

## A complete example [#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.

```json title="components.json"
{
  "$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 [#verify-it-worked]

1. **A namespaced add resolves.** `@opsinjs/status-pill` fetches rather than
   erroring on an unknown registry.
2. **The file lands where the alias says.** If it does not, the alias and the
   tsconfig path disagree.
3. **The `cn` import inside the copied file resolves.** It comes from
   `aliases.utils` and it breaks two ways: the alias points somewhere real but
   wrong, or `lib/utils.ts` was never written because `init` was never run.
   Identical `TS2307` errors on every copied file at once is the second one; a
   single file resolving to the wrong place is the first.
4. **`"use client"` appears only where expected.** If it is on everything, `rsc` is
   probably `false` when it should be `true`.

## Troubleshooting [#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 [#next]

* [Monorepo](./monorepo.mdx) covers the same fields, one directory level harder.
* [Registry](../../registry/index.mdx) says what the namespace resolves to and
  what a registry item contains.
* [Official resources](../../project/official-resources.mdx) has the canonical
  hosts, so you can tell a real registry from a lookalike.
