Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions apps/v4/content/docs/changelog/2026-07-dynamic-search.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
---
title: July 2026 - Dynamic Search
description: Registries can now handle search server-side.
date: 2026-07-31
---

**Registries can now handle search server-side.**

When you run `shadcn search`, the CLI forwards the search parameters to your
registry as query params:

```txt
GET /r/registry.json?q=button&limit=50&offset=0
```

Return the matching items with a `pagination` object and the CLI uses your
results as-is. This makes search fast for large registries: no more downloading
the full catalog to search it.

```json title="registry.json?q=button&limit=1"
{
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "button",
"type": "registry:ui",
"description": "A button component."
}
],
"pagination": {
"total": 12,
"offset": 0,
"limit": 1,
"hasMore": true
}
}
```

Dynamic search is opt-in. Static registries ignore the query params and keep
working without any changes.

See the [Dynamic Search](/docs/registry/dynamic-search) docs for the full
guide.
199 changes: 199 additions & 0 deletions apps/v4/content/docs/registry/dynamic-search.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
---
title: Dynamic Search
description: Implement server-side search for large registries.
---

By default, `shadcn search` fetches your entire `registry.json` and filters
items locally. This works well for most registries and requires nothing more
than a static file.

For large registries with thousands of items, you can implement search on your
registry server instead. The CLI forwards the search parameters to your
registry and your server returns only the matching items.

Dynamic search is opt-in and fully backwards compatible. Static registries
keep working without any changes.

## How It Works

When you run `shadcn search`, the CLI appends the search parameters to the
catalog request:

```bash
npx shadcn@latest search @acme --query button --limit 50
```

```txt
GET https://acme.com/r/registry.json?q=button&limit=50&offset=0
```

What happens next depends on your registry:

- **Static registries** ignore the query parameters and return the full
`registry.json`. The CLI filters the items locally. This is the default
behavior and requires no changes.
- **Dynamic registries** filter the items server-side and return the matching
items along with a `pagination` object. When the CLI sees `pagination` in
the response, it trusts the results as pre-filtered and skips local
filtering.

The presence of `pagination` in the response is what tells the CLI your
registry handles search server-side. There is no configuration or capability
negotiation required.

## Query Parameters

The CLI sends the following query parameters with every search request:

| Parameter | Description |
| --------- | -------------------------------------------------------------- |
| `q` | The search query string. |
| `type` | Comma-separated item types, e.g. `registry:ui,registry:block`. |
| `limit` | Maximum number of items to return. |
| `offset` | Number of items to skip. |

All parameters are optional. A request without `q` or `type` should return
all items, paginated.

## Response Format

Return your regular `registry.json` shape with an additional `pagination`
object:

```json title="registry.json?q=button&limit=2"
{
"name": "acme",
"homepage": "https://acme.com",
"items": [
{
"name": "button",
"type": "registry:ui",
"description": "A button component."
},
{
"name": "icon-button",
"type": "registry:ui",
"description": "A button component with an icon."
}
],
"pagination": {
"total": 12,
"offset": 0,
"limit": 2,
"hasMore": true
}
}
```

Search results only need `name`, `type` and `description` for each item. You
do not need to include `files`, `dependencies` or other item properties. The
CLI fetches the full item definition when the user runs `shadcn add`.

### pagination

| Property | Type | Description |
| --------- | --------- | -------------------------------------------------- |
| `total` | `number` | Total number of items matching the query. |
| `offset` | `number` | Number of items skipped. |
| `limit` | `number` | Maximum number of items in this response. |
| `hasMore` | `boolean` | Whether more items are available beyond this page. |

## Server Implementation

Here's an example using a Next.js route handler:

```typescript title="app/r/registry.json/route.ts"
import { NextRequest, NextResponse } from "next/server"

export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl

const query = searchParams.get("q")
const types = searchParams.get("type")?.split(",")
const limit = Number(searchParams.get("limit") ?? 100)
const offset = Number(searchParams.get("offset") ?? 0)

// Filter items using your database or search index.
const { items, total } = await searchItems({ query, types, limit, offset })

return NextResponse.json({
name: "acme",
homepage: "https://acme.com",
items,
pagination: {
total,
offset,
limit,
hasMore: offset + limit < total,
},
})
}
```

You can back `searchItems` with anything: a database query, a full-text
search index or an external search service.

## Multiple Registries

When searching a single registry, the CLI forwards all parameters and uses
your `pagination` response as-is.

When searching multiple registries at once, e.g. `shadcn search @acme @lib`,
a global `offset` cannot be split across registries. The CLI forwards `q` and
`type` to each registry along with a `limit` large enough to fill the
requested page (`offset + limit`), then merges and paginates the combined
results locally. Server-side filtering still applies, so each registry only
returns matching items.

If your registry caps the number of items per response below the requested
`limit`, the CLI treats it as exhausted for deeper pages. Honor the requested
`limit` where possible so all your matches stay reachable through paging.

## Authentication

Dynamic search works with all [authentication](/docs/registry/authentication)
patterns. The CLI sends the configured headers and params with the search
request, so you can scope search results to the authenticated user:

```typescript title="app/r/registry.json/route.ts"
export async function GET(request: NextRequest) {
const token = request.headers.get("authorization")?.replace("Bearer ", "")
const team = await getTeamFromToken(token)

// Only search items this team can access.
const { items, total } = await searchItems({
query: request.nextUrl.searchParams.get("q"),
team,
})

// ...
}
```

## Backwards Compatibility

- **Static registries** require no changes. Query parameters on a static file
are ignored by the file server and the CLI falls back to local filtering.
- **Older CLI versions** fetch the catalog without query parameters and
ignore the `pagination` field. Your registry should return a sensible
default response for requests without parameters, e.g. the first page of
items.
- **Ranking** is up to your server. When your registry returns pre-filtered
results, the CLI preserves your order instead of re-ranking locally.

## Testing

Test your dynamic registry with `curl`:

```bash
curl "https://acme.com/r/registry.json?q=button&limit=10"
```

Then verify with the CLI:

```bash
npx shadcn@latest search @acme --query button
```

To confirm server-side search is active, check that the response includes the
`pagination` object and only the matching items.
1 change: 1 addition & 0 deletions apps/v4/content/docs/registry/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"examples",
"namespace",
"authentication",
"dynamic-search",
"mcp",
"open-in-v0",
"api-reference",
Expand Down
1 change: 1 addition & 0 deletions apps/v4/lib/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const PAGES_NEW = [
"/docs/helpers/ai-sdk",
"/docs/helpers/tanstack-ai",
"/docs/react/message-scroller",
"/docs/registry/dynamic-search",
]

export const PAGES_UPDATED = []
2 changes: 1 addition & 1 deletion apps/v4/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@
"rehype-pretty-code": "^0.14.1",
"rimraf": "^6.0.1",
"server-only": "^0.0.1",
"shadcn": "4.16.0",
"shadcn": "4.16.1",
"shiki": "^3.23.0",
"sonner": "^2.0.0",
"streamdown": "^2.5.0",
Expand Down
23 changes: 23 additions & 0 deletions apps/v4/public/schema/registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@
"items": {
"$ref": "https://ui.shadcn.com/schema/registry-item.json"
}
},
"pagination": {
"type": "object",
"description": "Pagination metadata returned by registries that implement dynamic search. Its presence on a catalog response signals that the items are already filtered and paginated server-side.",
"properties": {
"total": {
"type": "number",
"description": "Total number of items matching the query."
},
"offset": {
"type": "number",
"description": "Number of items skipped."
},
"limit": {
"type": "number",
"description": "Maximum number of items in this response."
},
"hasMore": {
"type": "boolean",
"description": "Whether more items are available beyond this page."
}
},
"required": ["total", "offset", "limit", "hasMore"]
}
},
"anyOf": [{ "required": ["items"] }, { "required": ["include"] }]
Expand Down
7 changes: 7 additions & 0 deletions apps/v4/registry/directory.json
Original file line number Diff line number Diff line change
Expand Up @@ -1841,5 +1841,12 @@
"url": "https://ai2.design/r/{name}.json",
"description": "Agent-native design system for the shadcn CLI: 51 base components with a full variant, tone and size matrix on an OKLCH token layer, plus 307 styled variations. MIT.",
"logo": "<svg width=\"2048\" height=\"2048\" viewBox=\"0 0 2048 2048\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"ai2brand\" gradientUnits=\"objectBoundingBox\" gradientTransform=\"rotate(49.49 0.5 0.5)\"><stop offset=\"0.0962\" stop-color=\"#37d7fa\"/><stop offset=\"0.3956\" stop-color=\"#4b72fe\"/><stop offset=\"0.6052\" stop-color=\"#ff8df2\"/><stop offset=\"0.8447\" stop-color=\"#ff8705\"/></linearGradient></defs><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M1021.86 95.068C1090.81 93.3092 1169.18 125.843 1217.9 173.889C1266.09 221.397 1308.01 304.343 1342.46 364.147L1692.25 963.902L1914.53 1343.73L1980.76 1456.37C1992.57 1476.44 2008.32 1501.67 2018.32 1522.18C2037.46 1561.64 2047.6 1604.85 2047.99 1648.7C2048.56 1728.01 2017.13 1804.22 1960.82 1860.08C1917.9 1903.22 1863.31 1932.9 1803.76 1945.45C1760.51 1954.83 1716.84 1952.74 1672.73 1952.77L525.062 1952.74L377.85 1952.92C337.643 1952.98 290.2 1954.31 251.147 1947.19C202.751 1937.99 157.185 1917.55 118.112 1887.55C54.4788 1838.61 12.7718 1769.97 2.65725 1689.96C-4.4767 1637.72 2.81765 1584.52 23.7539 1536.12C35.7099 1508.89 55.529 1478.33 71.0918 1452.32L142.436 1331.75L396.298 897.7L662.575 440.429L744.674 300.162C784.644 232.689 815.284 173.941 886.467 133.932C927.791 110.705 957.103 101.137 1004.17 95.8219C1010.06 95.4893 1015.97 95.2383 1021.86 95.068ZM1121.4 362.7C1092.94 314.076 1077.99 277.383 1011.46 282.175C984.216 287.864 966.67 298.507 951.696 322.735C937.375 345.912 923.706 370.552 910.07 394.164L829.799 531.983C803.289 576.418 777.021 620.997 750.998 665.718C736.824 690.253 720.573 716.211 708.904 742.005C657.811 854.956 638.419 965.643 664.116 1087.87C690.838 1214.17 766.608 1324.71 874.776 1395.18C939.377 1436.87 1021.96 1465.54 1098.75 1468.75C871.614 1553.74 613.422 1452.5 480.489 1255.81C467.571 1236.69 455.683 1215.92 444.65 1195.68C384.251 1296.92 324.375 1398.45 265.023 1500.31C245.246 1534.16 212.564 1582.08 198.625 1616.98C174.473 1677.44 225.902 1751.44 290.256 1754.23C315.242 1756.22 339.723 1755.23 364.228 1755.19L656.912 1754.91C695.559 1754.91 735.084 1755.79 773.553 1752.83C808.822 1749.99 843.725 1743.67 877.753 1733.96C1021.19 1693.39 1142.5 1597.22 1214.71 1466.81C1261.27 1381.92 1277.98 1297.65 1274.25 1201.71C1270.49 1163.73 1264.95 1135.15 1253.38 1098.23C1423.08 1283.31 1425.32 1553.72 1283.81 1755.07L1621.46 1755.04C1653.41 1754.96 1745.6 1757.1 1771.77 1751.8C1791.11 1747.69 1808.81 1737.96 1822.63 1723.82C1842.15 1704.32 1852.95 1677.76 1852.61 1650.18C1852.32 1614.06 1826.91 1578.5 1809.34 1547.75L1741.83 1430.14L1669.23 1300.49C1656.88 1278.47 1644.63 1256.32 1631.73 1234.63C1531.36 1066.02 1360.1 949.096 1158.32 952.836C1041.67 954.922 930.663 1003.44 849.897 1087.63C828.581 1110.14 809.459 1134.63 792.788 1160.78C828.987 928.386 1040.74 772.55 1265.68 755.294C1294 753.121 1321.69 753.475 1350.05 753.9L1121.4 362.7Z\" fill=\"url(#ai2brand)\"/></svg>"
},
{
"name": "@whiskeyjack",
"homepage": "https://whiskeyjack.net",
"url": "https://whiskeyjack.net/r/{name}.json",
"description": "A Tauri-first design system: thumb-first components with Metro-style pivot navigation, frosted bottom nav, and tap-again confirmations, on a CSS-variable token pipeline. Hardened across nine shipping apps before extraction.",
"logo": "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 256 256' width='24' height='24'><rect x='0' y='0' width='256' height='256' rx='57.5' ry='57.5' fill='var(--foreground)'/><g fill='var(--background)'><path d='M63,229.55h175.28l-10.39-10.22h-52.54l-2.87-2.82-18.78-18.48c24.86-10.06,42.4-34.44,42.4-62.91l-.37-7.09v-.02s0-.06,0-.06l-.92-17.82-1.6-30.76,25.11-8.99-25.77-3.73-5.23-.76h0c-7.9-13.71-22.69-22.94-39.65-22.94-15.49,0-29.18,7.7-37.45,19.49l-1.41,2.51c3.08.85,6.08,1.89,8.97,3.13.72.31,1.43.62,2.13.95.38-.47.76-.93,1.16-1.37,6.17-6.94,15.18-11.31,25.2-11.31,18.63,0,33.73,15.1,33.73,33.73,0,.39,0,.78-.02,1.16-.07,2-.31,3.95-.71,5.85-4.87-2.99-10.14-5.37-15.72-7.06,4.86,5,8.84,10.86,11.69,17.33,1.97,4.47,3.41,9.22,4.21,14.19.51,3.1.77,6.3.77,9.54,0,28.23-19.85,51.82-46.36,57.58,0,0,0,0,0,0q-1.05.23,0,0s0,0,0,0c16.06-12.68,26.47-32.18,26.89-54.13v-.09c0-.12,0-.24,0-.36,0-.31,0-.62,0-.93,0-.11,0-.22,0-.33v-.11c0-.07,0-.15,0-.22,0-.1,0-.2,0-.3,0-.04,0-.08,0-.12v-.03h0v-.12c0-.34-.02-.68-.04-1.01-.16-3.33-.67-6.56-1.5-9.67-4.1-15.38-15.94-27.62-31.1-32.25h0c-4.28-1.31-8.82-2.02-13.53-2.02-.64,0-1.27.01-1.91.04,0-.01,0-.02,0-.04h-16.21l-2.8,5-4.67,8.32h21.41c.22,0,.45,0,.67,0h3.02c.02.07.04.14.07.22,4.93.64,9.62,2.68,13.47,5.91.05.04.12.09.17.14.59.5,1.15,1.02,1.7,1.58.18.18.36.37.53.55.18.2.36.4.54.6.14.16.29.33.42.5.05.05.1.11.14.17.16.19.32.39.46.59.13.17.26.34.39.52.07.09.13.18.19.27.12.16.24.34.35.5.03.04.06.08.08.12.1.15.2.3.3.45.13.21.26.42.39.63.02.03.04.06.06.1.1.16.2.33.29.5.21.37.41.74.6,1.13.78,1.55,1.38,3.13,1.83,4.74,0,.01,0,.03.01.05.18.65.35,1.3.48,1.97.23,1.11.39,2.24.47,3.39.16,2.26.03,4.58-.42,6.91-.07.39-.16.78-.26,1.16.18.05.36.09.53.14-.18-.04-.36-.09-.53-.13-1.54,6.4-5.34,11.71-10.37,15.26-2.15,1.51-4.53,2.71-7.05,3.52l-33.86,16.93-6.34,3.17h0l-.55.28-1.94.97-30.42,15.21-8.59,4.29h0s0,0,0,0l-14.96,26.71-5.73,10.22h35.9l5.58-10.22,10.31-18.88,7.66-14.03,16.49,12.87h.01s5.27,4.13,5.27,4.13h0l20.36,15.9h-42.1l-13.08,10.22ZM116.03,201.86,128.26,202.96c3.99,0,7.9-.35,11.7-1l17.65,17.37h-19.22l-22.36-17.47c3.96.72,8.05,1.1,12.22,1.1Z'/><path d='M158.77,70.94c0-.06,0-.12,0-.18,0-1.37.51-2.63,1.36-3.58.49-.56,1.1-1.01,1.79-1.32-1.29-.7-2.77-1.11-4.34-1.11-2.71,0-5.14,1.18-6.82,3.06-1.44,1.61-2.31,3.74-2.31,6.07,0,.1,0,.21,0,.31.16,4.89,4.18,8.82,9.12,8.82,2.04,0,3.93-.67,5.45-1.8,1.8-1.34,3.09-3.34,3.52-5.63-.72.36-1.54.57-2.4.57-2.91,0-5.28-2.31-5.37-5.2Z'/><path d='M65.42,160.73l3.88-1.94,5.25-2.63,1.5-.75,17.82-8.91,21.18-10.59c4.09-2.05,6.05-6.63,4.94-10.88h0c-.16-.63-.39-1.25-.69-1.86-1.35-2.7-3.82-4.48-6.56-5.05-.06-.06-.12-.13-.17-.2h-33.43l-10.58,18.87-4.54,8.1-2.21,3.95-10.66,19.02,14.27-7.13h0Z'/></g></svg>"
}
]
8 changes: 8 additions & 0 deletions packages/shadcn/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# shadcn

## 4.16.1

### Patch Changes

- [#11322](https://github.com/shadcn-ui/ui/pull/11322) [`bfa1b5e9a69a155b2f590523d50fda810bde1a9a`](https://github.com/shadcn-ui/ui/commit/bfa1b5e9a69a155b2f590523d50fda810bde1a9a) Thanks [@AndrewBarba](https://github.com/AndrewBarba)! - fix `shadcn build` failing with ENOENT when registry item names contain path segments (e.g. `extension/foo`) by creating nested output directories before writing

- [#11352](https://github.com/shadcn-ui/ui/pull/11352) [`5ca53ca7c7dea390e0e78091ff7c54adc48c773a`](https://github.com/shadcn-ui/ui/commit/5ca53ca7c7dea390e0e78091ff7c54adc48c773a) Thanks [@shadcn](https://github.com/shadcn)! - forward search params to registries for server-side dynamic search

## 4.16.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/shadcn/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "shadcn",
"version": "4.16.0",
"version": "4.16.1",
"description": "Add components to your apps.",
"publishConfig": {
"access": "public"
Expand Down
42 changes: 42 additions & 0 deletions packages/shadcn/src/commands/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,48 @@ describe("build command", () => {
],
})
})

it("creates nested output directories for item names with path segments", async () => {
const cwd = await createFixture({
"registry.json": JSON.stringify({
name: "example",
homepage: "https://example.com",
items: [
{
name: "extension/foo",
type: "registry:item",
files: [
{
path: "registry/extensions/foo.tsx",
type: "registry:file",
target: "extensions/foo.tsx",
},
],
},
],
}),
"registry/extensions/foo.tsx": "export function Foo() {}",
})

await build.parseAsync(
["node", "shadcn", "registry.json", "--cwd", cwd, "--output", "public/r"],
{ from: "node" }
)

const item = JSON.parse(
await fs.readFile(path.join(cwd, "public/r/extension/foo.json"), "utf-8")
)

expect(item).toMatchObject({
name: "extension/foo",
files: [
{
path: "registry/extensions/foo.tsx",
content: "export function Foo() {}",
},
],
})
})
})

async function createFixture(files: Record<string, string>) {
Expand Down
Loading
Loading