# Blocks, installed by the shadcn CLI

_Box Kite 2.1.0 · a markdown copy of https://www.box-kite.dev/registry/_

Finished sections you install into your own repository and then own — a data grid, a settings form and a dashboard shell.

A shadcn registry is JSON over HTTP, and the CLI that reads it does not care which library the code imports. These three items install the `@box-kite/react` package and drop wired code beside it — so the parts you will change are yours from the first commit, and the parts you will not stay a dependency you can upgrade.

## Two ways in

Point the CLI straight at an item, from any project that has a `components.json`:

```shell
npx shadcn@latest add https://www.box-kite.dev/r/data-grid.json
```

Or register the namespace once, and then name the block rather than its address. Add this to `components.json`:

```javascript
{
  "registries": {
    "@box-kite": "https://www.box-kite.dev/r/{name}.json"
  }
}
```

After that, `npx shadcn@latest add @box-kite/data-grid` — and `npx shadcn@latest search @box-kite` lists what is here.

The CLI expects a `components.json` carrying a `tailwind` key, and empty strings satisfy it — these blocks write no CSS file and import no stylesheet, because every style in them is a prop.

## Invoices data grid

A virtualized data grid wired to sample invoices: search, column filters, grouping, totals, an editable amount that is validated, range selection with copy and paste, and CSV/XLSX export.

```shell
npx shadcn@latest add https://www.box-kite.dev/r/data-grid.json
```

[Open in v0](https://v0.dev/chat/api/open?url=https%3A%2F%2Fwww.box-kite.dev%2Fr%2Fdata-grid.json)

[The item JSON](https://www.box-kite.dev/r/data-grid.json)

2 files · installs @box-kite/react

## Invoices

Search the table, filter a column, group by Team from a column menu, edit an amount, select a block of cells and copy it, or export what is on screen.

Invoices

Reference

Customer

Team

Status

Amount

Issued

Select...

Select...

INV-2480

Ardent Supply

Northern

paid

$400

2026-01-01

INV-2481

Beacon Freight

Southern

pending

$537

2026-02-08

INV-2482

Corvus Analytics

Eastern

overdue

$674

2026-03-15

INV-2483

Delta Hardware

Western

paid

$811

2026-04-22

INV-2484

Evergreen Mills

Northern

pending

$948

2026-05-02

INV-2485

Fairline Media

Southern

overdue

$1,085

2026-06-09

INV-2486

Granite Foods

Eastern

paid

$1,222

2026-07-16

INV-2487

Harbour Logistics

Western

pending

$1,359

2026-08-23

INV-2488

Ardent Supply

Northern

overdue

$1,496

2026-09-03

INV-2489

Beacon Freight

Southern

paid

$1,633

2026-10-10

INV-2490

Corvus Analytics

Eastern

pending

$1,770

2026-11-17

INV-2491

Delta Hardware

Western

overdue

$1,907

2026-12-24

INV-2492

Evergreen Mills

Northern

paid

$2,044

2026-01-04

INV-2493

Fairline Media

Southern

pending

$2,181

2026-02-11

INV-2494

Granite Foods

Eastern

overdue

$2,318

2026-03-18

INV-2495

Harbour Logistics

Western

paid

$2,455

2026-04-25

INV-2496

Ardent Supply

Northern

pending

$2,592

2026-05-05

INV-2497

Beacon Freight

Southern

overdue

$2,729

2026-06-12

INV-2498

Corvus Analytics

Eastern

paid

$2,866

2026-07-19

INV-2499

Delta Hardware

Western

pending

$3,003

2026-08-26

INV-2500

Evergreen Mills

Northern

overdue

$3,140

2026-09-06

INV-2501

Fairline Media

Southern

paid

$3,277

2026-10-13

INV-2502

Granite Foods

Eastern

pending

$3,414

2026-11-20

INV-2503

Harbour Logistics

Western

overdue

$3,551

2026-12-27

INV-2504

Ardent Supply

Northern

paid

$3,688

2026-01-07

INV-2505

Beacon Freight

Southern

pending

$3,825

2026-02-14

INV-2506

Corvus Analytics

Eastern

overdue

$3,962

2026-03-21

INV-2507

Delta Hardware

Western

paid

$4,099

2026-04-01

INV-2508

Evergreen Mills

Northern

pending

$4,236

2026-05-08

INV-2509

Fairline Media

Southern

overdue

$4,373

2026-06-15

All invoices

200

$1,001,500

Rows: 200

Selected: 0

**invoices-grid.tsx**

```jsx
'use client';
import Box from '@box-kite/react';
import DataGrid, { type CellModel, type GridDefinition } from '@box-kite/react/components/dataGrid';
import Flex from '@box-kite/react/components/flex';
import { H2, P } from '@box-kite/react/components/semantics';
import { type Invoice, invoices } from './invoice-rows';

/**
 * An invoices table with what a real one needs: search, per-column filters, grouping from the column
 * menu, totals, an editable amount that is validated, a block of cells to copy, and CSV/XLSX export.
 *
 * None of it is configuration to keep in sync — the grid is one `def` object. Swap `invoices` for your
 * own rows and edit the columns.
 */
const money = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });

const STATUS_STYLES = {
  paid: { bgColor: 'emerald-100', color: 'emerald-700', theme: { dark: { bgColor: 'emerald-950', color: 'emerald-300' } } },
  pending: { bgColor: 'amber-100', color: 'amber-700', theme: { dark: { bgColor: 'amber-950', color: 'amber-300' } } },
  overdue: { bgColor: 'rose-100', color: 'rose-700', theme: { dark: { bgColor: 'rose-950', color: 'rose-300' } } },
} as const;

// A cell renderer is a component defined outside the render: a new identity on every render remounts
// the whole column on every scroll.
function StatusCell({ cell }: { cell: CellModel<Invoice> }) {
  const status = cell.value as Invoice['status'];

  return (
    <Flex ai="center" px={3} height="fit">
      <Box px={2} py={0.5} borderRadius={4} fontSize={12} fontWeight={500} textTransform="capitalize" {...STATUS_STYLES[status]}>
        {status}
      </Box>
    </Flex>
  );
}

// `fontVariantNumeric` has no prop of its own, so it goes through `css` — still one shared class, not
// a style attribute.
function AmountCell({ cell }: { cell: CellModel<Invoice> }) {
  return (
    <Flex ai="center" jc="end" px={3} height="fit" css={{ fontVariantNumeric: 'tabular-nums' }}>
      {money.format(cell.value as number)}
    </Flex>
  );
}

const definition: GridDefinition<Invoice> = {
  rowKey: 'id',
  title: 'Invoices',
  topBar: true,
  bottomBar: true,
  globalFilter: true,
  rowSelection: { pinned: true },
  rangeSelection: true,
  footer: { label: 'All invoices' },
  export: { fileName: 'invoices' },
  visibleRowsCount: 12,
  rowHeight: 44,
  columns: [
    { key: 'reference', header: 'Reference', width: 120, filterable: true },
    { key: 'customer', header: 'Customer', width: 170, filterable: true },
    { key: 'team', header: 'Team', width: 130, filterable: { type: 'multiselect' } },
    { key: 'status', header: 'Status', width: 120, filterable: { type: 'multiselect' }, Cell: StatusCell, aggregate: 'count' },
    {
      key: 'amount',
      header: 'Amount',
      width: 130,
      align: 'end',
      editable: true,
      filterable: { type: 'number' },
      aggregate: 'sum',
      Cell: AmountCell,
      AggregateCell: ({ cell }) => (
        <Flex ai="center" jc="end" px={3} height="fit" fontWeight={600} css={{ fontVariantNumeric: 'tabular-nums' }}>
          {cell.value === null ? '' : money.format(Number(cell.value))}
        </Flex>
      ),
      // An export runs no React, so the column says what it writes rather than exporting the renderer.
      exportValue: (row) => row.amount,
      exportFormat: '#,##0',
    },
    { key: 'issued', header: 'Issued', width: 120 },
  ],
  // One function both judges an edit and is told about it: a string is a refusal, and the message is
  // what the cell shows.
  onCellEdit: ({ value }) => {
    const amount = Number(value);

    if (!Number.isFinite(amount) || amount <= 0) return 'An invoice is worth more than nothing.';
    if (amount > 100_000) return 'Anything over $100,000 needs an approval.';
  },
};

export default function InvoicesGrid() {
  return (
    <Flex d="column" gap={4}>
      <Box>
        <H2 fontSize={20} fontWeight={600}>
          Invoices
        </H2>
        <P mt={1} fontSize={14} color="slate-600" theme={{ dark: { color: 'slate-400' } }}>
          Search the table, filter a column, group by Team from a column menu, edit an amount, select a block of cells and copy it, or
          export what is on screen.
        </P>
      </Box>
      <DataGrid data={invoices} def={definition} />
    </Flex>
  );
}
```

**invoice-rows.ts**

```jsx
/** The rows the block is wired to. Replace this file with your own fetch — the grid reads plain objects. */
export interface Invoice {
  id: number;
  reference: string;
  customer: string;
  team: string;
  status: 'paid' | 'pending' | 'overdue';
  amount: number;
  issued: string;
}

const TEAMS = ['Northern', 'Southern', 'Eastern', 'Western'];
const STATUSES = ['paid', 'pending', 'overdue'] as const;
const CUSTOMERS = [
  'Ardent Supply',
  'Beacon Freight',
  'Corvus Analytics',
  'Delta Hardware',
  'Evergreen Mills',
  'Fairline Media',
  'Granite Foods',
  'Harbour Logistics',
];

/**
 * Two hundred invoices generated from the index, so the block has something to sort, filter, group and
 * total without shipping a data file — and every reload shows the same table.
 */
export const invoices: Invoice[] = Array.from({ length: 200 }, (_, index) => ({
  id: index + 1,
  reference: `INV-${(2480 + index).toString()}`,
  customer: CUSTOMERS[index % CUSTOMERS.length],
  team: TEAMS[index % TEAMS.length],
  status: STATUSES[index % STATUSES.length],
  amount: 400 + ((index * 137) % 9600),
  issued: new Date(Date.UTC(2026, index % 12, ((index * 7) % 27) + 1)).toISOString().slice(0, 10),
}));
```

## Settings form

A settings panel of real form controls — text fields, a searchable select, a radio group, a slider and a switch — that reads its own fields on submit, so none of it needs state.

```shell
npx shadcn@latest add https://www.box-kite.dev/r/settings-form.json
```

[Open in v0](https://v0.dev/chat/api/open?url=https%3A%2F%2Fwww.box-kite.dev%2Fr%2Fsettings-form.json)

[The item JSON](https://www.box-kite.dev/r/settings-form.json)

1 file · installs @box-kite/react

## Settings

Every control here is a native element with its name attached, so the keyboard, the focus ring and the screen reader are the platform's.

Display nameEmail

AboutShown on your public profile.

Time zone

Europe/Chisinau

ThemeFollow the systemLightDark

Row density

How tall a row is in tables across the app.

Email me when something needs a decision

**settings-form.tsx**

```jsx
'use client';
import Button from '@box-kite/react/components/button';
import Dropdown from '@box-kite/react/components/dropdown';
import Flex from '@box-kite/react/components/flex';
import Form from '@box-kite/react/components/form';
import RadioGroup from '@box-kite/react/components/radioGroup';
import { H2, Label, P, Span } from '@box-kite/react/components/semantics';
import Slider from '@box-kite/react/components/slider';
import Switch from '@box-kite/react/components/switch';
import Textarea from '@box-kite/react/components/textarea';
import Textbox from '@box-kite/react/components/textbox';
import { toast } from '@box-kite/react/components/toaster';
import { type ReactNode } from 'react';

/**
 * A settings panel: text fields, a searchable select, a radio group, a slider and a switch, each a real
 * form control. `Form` reads its own fields when it submits, so none of this needs state — `values` is
 * built from the elements carrying a `name`.
 *
 * `<Toaster />` has to be mounted once near the root of the app for the confirmation to show.
 */
export interface Settings {
  name: string;
  email: string;
  bio: string;
  timezone: string;
  theme: string;
  density: string;
  notify: boolean;
}

const TIMEZONES = ['Europe/Chisinau', 'Europe/London', 'America/New_York', 'Asia/Tokyo'];

/**
 * A text field and its name. The control sits *inside* the `<label>`, so the two are associated with no
 * `htmlFor`/`id` pair to keep in step. `Dropdown`, `RadioGroup` and `Switch` draw their own label from a
 * `label` prop, so they are written bare below.
 */
function Field({ label, hint, children }: { label: string; hint?: string; children: ReactNode }) {
  return (
    <Label display="flex" d="column" gap={2} width="fit">
      <Span fontSize={14} fontWeight={500}>
        {label}
      </Span>
      {children}
      {hint && (
        <Span fontSize={13} color="slate-500" theme={{ dark: { color: 'slate-400' } }}>
          {hint}
        </Span>
      )}
    </Label>
  );
}

/**
 * A `Slider` is the exception: its `label` is the thumb's accessible name and draws nothing, and a
 * `<label>` cannot wrap it because there is no form control inside to attach to. So the caption is an
 * element with an id and `labelledBy` points at it — one name, read and seen.
 */
const DENSITY_LABEL = 'settings-density-label';

export default function SettingsForm() {
  return (
    <Form<Settings>
      p={6}
      b={1}
      borderRadius={3}
      borderColor="slate-200"
      bgColor="white"
      theme={{ dark: { borderColor: 'slate-800', bgColor: 'slate-900' } }}
      onSubmit={(values) => toast.success('Settings saved', { description: `${values.name || 'Your profile'} is up to date.` })}
    >
      <H2 fontSize={20} fontWeight={600}>
        Settings
      </H2>
      <P mt={1} mb={5} fontSize={14} color="slate-600" theme={{ dark: { color: 'slate-400' } }}>
        Every control here is a native element with its name attached, so the keyboard, the focus ring and the screen reader are the
        platform&apos;s.
      </P>

      <Flex d="column" gap={5}>
        <Flex d="column" gap={5} md={{ d: 'row' }}>
          <Field label="Display name">
            <Textbox name="name" defaultValue="Ada Lovelace" width="fit" />
          </Field>
          <Field label="Email">
            <Textbox name="email" type="email" defaultValue="ada@example.com" width="fit" />
          </Field>
        </Flex>

        <Field label="About" hint="Shown on your public profile.">
          <Textarea name="bio" rows={3} placeholder="A sentence or two." width="fit" />
        </Field>

        <Dropdown<string> name="timezone" label="Time zone" defaultValue={TIMEZONES[0]} isSearchable searchPlaceholder="Search zones…">
          {TIMEZONES.map((zone) => (
            <Dropdown.Item key={zone} value={zone}>
              {zone}
            </Dropdown.Item>
          ))}
        </Dropdown>

        <RadioGroup label="Theme" name="theme" defaultValue="system" orientation="horizontal">
          <RadioGroup.Item value="system" label="Follow the system" />
          <RadioGroup.Item value="light" label="Light" />
          <RadioGroup.Item value="dark" label="Dark" />
        </RadioGroup>

        <Flex d="column" gap={2}>
          <Span id={DENSITY_LABEL} fontSize={14} fontWeight={500}>
            Row density
          </Span>
          <Slider name="density" labelledBy={DENSITY_LABEL} defaultValue={44} min={32} max={64} step={4} format={(value) => `${value}px`} />
          <Span fontSize={13} color="slate-500" theme={{ dark: { color: 'slate-400' } }}>
            How tall a row is in tables across the app.
          </Span>
        </Flex>

        <Switch name="notify" label="Email me when something needs a decision" defaultChecked />
      </Flex>

      <Flex mt={6} gap={3} jc="end">
        <Button variant="secondary" type="reset">
          Reset
        </Button>
        <Button type="submit">Save changes</Button>
      </Flex>
    </Form>
  );
}
```

## Dashboard shell

The frame an admin app hangs off: a sidebar, a header with search, a theme toggle and an account menu, and a row of stat tiles drawn with the chart primitives.

```shell
npx shadcn@latest add https://www.box-kite.dev/r/dashboard-shell.json
```

[Open in v0](https://v0.dev/chat/api/open?url=https%3A%2F%2Fwww.box-kite.dev%2Fr%2Fdashboard-shell.json)

[The item JSON](https://www.box-kite.dev/r/dashboard-shell.json)

2 files · installs @box-kite/react, lucide-react

# Overview

Revenue

$48,210+12.4%

Invoices paid

182+4.1%

Overdue

$6,940-8.2%

Collection rate

86%

of this quarter

**dashboard-shell.tsx**

```jsx
'use client';
import Box from '@box-kite/react';
import Button from '@box-kite/react/components/button';
import Flex from '@box-kite/react/components/flex';
import Icon from '@box-kite/react/components/icon';
import Menu from '@box-kite/react/components/menu';
import { H1, Header, Li, Link, Main, Nav, Span, Ul } from '@box-kite/react/components/semantics';
import Textbox from '@box-kite/react/components/textbox';
import { LayoutDashboard, Moon, Receipt, Search, Settings, Sun, Users } from 'lucide-react';
import { type ReactElement, type ReactNode } from 'react';

/**
 * The frame an admin app hangs off: a sidebar, a header with search, a theme toggle and an account
 * menu, and the page itself. The sidebar sticks to the top of its column and folds away below the `md`
 * breakpoint; nothing here is measured or positioned by script.
 *
 * `Box.Theme` is `use="local"` here, so the theme lands on this element and the block can sit inside a
 * page that has its own. Move it to your root layout as `use="global"` for an app-wide switch.
 */
export interface NavItem {
  id: string;
  label: string;
  href: string;
  icon: ReactElement;
}

const dashboardNav: NavItem[] = [
  { id: 'overview', label: 'Overview', href: '#overview', icon: <LayoutDashboard /> },
  { id: 'invoices', label: 'Invoices', href: '#invoices', icon: <Receipt /> },
  { id: 'customers', label: 'Customers', href: '#customers', icon: <Users /> },
  { id: 'settings', label: 'Settings', href: '#settings', icon: <Settings /> },
];

function ThemeToggle() {
  const [theme, setTheme] = Box.useTheme();
  const next = theme === 'dark' ? 'light' : 'dark';

  return (
    <Button variant="ghost" p={2} borderRadius={2} onClick={() => setTheme(next)} props={{ 'aria-label': `Switch to the ${next} theme` }}>
      <Icon size={4}>{theme === 'dark' ? <Sun /> : <Moon />}</Icon>
    </Button>
  );
}

interface SidebarProps {
  current: string;
}

function Sidebar({ current }: SidebarProps) {
  return (
    <Nav
      display="none"
      md={{ display: 'block' }}
      width={64}
      flexShrink={0}
      be={1}
      borderColor="slate-200"
      theme={{ dark: { borderColor: 'slate-800' } }}
      props={{ 'aria-label': 'Sections' }}
    >
      <Box position="sticky" top={0} p={4}>
        <Flex ai="center" gap={2} px={2} py={3} mb={2}>
          <Box width={6} height={6} borderRadius={2} bgGradient={{ linear: 'br', colors: ['violet-500', 'sky-400'] }} />
          <Span fontWeight={600}>Acme Ops</Span>
        </Flex>
        <Ul listStyle="none" m={0} p={0}>
          {dashboardNav.map((item) => (
            <Li key={item.id}>
              <Link
                props={{ href: item.href, 'aria-current': item.id === current ? 'page' : undefined }}
                display="flex"
                ai="center"
                gap={3}
                px={3}
                py={2}
                my={0.5}
                borderRadius={2}
                fontSize={14}
                textDecoration="none"
                color={item.id === current ? 'violet-700' : 'slate-600'}
                bgColor={item.id === current ? 'violet-50' : 'transparent'}
                hover={{ bgColor: item.id === current ? 'violet-50' : 'slate-100' }}
                theme={{
                  dark: {
                    color: item.id === current ? 'violet-300' : 'slate-400',
                    bgColor: item.id === current ? 'violet-950' : 'transparent',
                    hover: { bgColor: item.id === current ? 'violet-950' : 'slate-900' },
                  },
                }}
              >
                <Icon size={4}>{item.icon}</Icon>
                {item.label}
              </Link>
            </Li>
          ))}
        </Ul>
      </Box>
    </Nav>
  );
}

export interface DashboardShellProps {
  /** The heading above the page, and the `id` of the nav item that is marked as current. */
  title: string;
  current?: string;
  children: ReactNode;
}

export default function DashboardShell({ title, current = 'overview', children }: DashboardShellProps) {
  return (
    <Box.Theme storageKey="dashboard-theme">
      <Flex
        minHeight="fit"
        bgColor="slate-50"
        color="slate-900"
        theme={{ dark: { bgColor: 'slate-950', color: 'slate-100' } }}
        overflow="hidden"
        borderRadius={3}
      >
        <Sidebar current={current} />
        <Flex d="column" flexGrow={1} minWidth={0}>
          <Header
            display="flex"
            ai="center"
            gap={3}
            px={5}
            py={3}
            bb={1}
            borderColor="slate-200"
            theme={{ dark: { borderColor: 'slate-800' } }}
          >
            <H1 fontSize={16} fontWeight={600} flexGrow={1}>
              {title}
            </H1>
            {/* Away below `md`, with the sidebar: a fixed-width field is what pushes the header past a
                phone's width, and search deserves a screen of its own there rather than a sliver. */}
            <Flex ai="center" gap={2} position="relative" display="none" md={{ display: 'flex' }}>
              <Icon size={4} position="absolute" insetStart={3} color="slate-400" pointerEvents="none">
                <Search />
              </Icon>
              <Textbox type="search" ps={9} py={2} width={56} placeholder="Search…" props={{ 'aria-label': 'Search' }} />
            </Flex>
            <ThemeToggle />
            <Menu
              trigger={(trigger) => (
                <Button {...trigger} variant="ghost" p={2} borderRadius={2}>
                  AL
                </Button>
              )}
            >
              <Menu.Item>Profile</Menu.Item>
              <Menu.Item>Billing</Menu.Item>
              <Menu.Separator />
              <Menu.Item>Sign out</Menu.Item>
            </Menu>
          </Header>
          <Main p={5} flexGrow={1}>
            {children}
          </Main>
        </Flex>
      </Flex>
    </Box.Theme>
  );
}
```

**dashboard-stats.tsx**

```jsx
import Box from '@box-kite/react';
import { ProgressRing, Sparkline } from '@box-kite/react/components/chart';
import Flex from '@box-kite/react/components/flex';
import Grid from '@box-kite/react/components/grid';
import { Span } from '@box-kite/react/components/semantics';
import { type ReactNode } from 'react';

/**
 * The row of tiles above a dashboard. The shapes are `components/chart` primitives — an SVG each, no
 * chart library — and there is no `'use client'` here on purpose: nothing in this file holds state, so
 * it renders in a Server Component, styles and all.
 */
export interface Stat {
  label: string;
  value: string;
  delta: string;
  up: boolean;
  trend: number[];
}

const dashboardStats: Stat[] = [
  { label: 'Revenue', value: '$48,210', delta: '+12.4%', up: true, trend: [12, 18, 14, 22, 25, 21, 28, 31, 29, 36, 34, 42] },
  { label: 'Invoices paid', value: '182', delta: '+4.1%', up: true, trend: [30, 28, 33, 31, 36, 34, 38, 37, 41, 40, 44, 46] },
  { label: 'Overdue', value: '$6,940', delta: '-8.2%', up: false, trend: [22, 24, 21, 19, 20, 17, 16, 18, 15, 13, 12, 10] },
];

function Card({ children }: { children: ReactNode }) {
  return (
    <Flex
      d="column"
      gap={3}
      p={4}
      b={1}
      borderRadius={3}
      borderColor="slate-200"
      bgColor="white"
      theme={{ dark: { borderColor: 'slate-800', bgColor: 'slate-900' } }}
    >
      {children}
    </Flex>
  );
}

export default function DashboardStats() {
  return (
    <Grid gridTemplateColumns={1} gap={4} md={{ gridTemplateColumns: 4 }}>
      {dashboardStats.map((stat) => (
        <Card key={stat.label}>
          <Span fontSize={13} color="slate-500" theme={{ dark: { color: 'slate-400' } }}>
            {stat.label}
          </Span>
          {/* Stacked rather than side by side: a tile is a quarter of the row, and a long figure next
              to its delta is the first thing to overflow — on one tile, which leaves the row ragged. */}
          <Flex d="column" gap={1}>
            <Span fontSize={24} fontWeight={600}>
              {stat.value}
            </Span>
            <Span
              fontSize={13}
              color={stat.up ? 'emerald-600' : 'rose-600'}
              theme={{ dark: { color: stat.up ? 'emerald-400' : 'rose-400' } }}
            >
              {stat.delta}
            </Span>
          </Flex>
          {/* `width`/`height` on an SVG are the attributes, not the ÷4 layout props — a CSS length, or
              a bare number meaning user units. `color` paints it: the stroke is `currentColor`. */}
          <Sparkline data={stat.trend} height="2rem" color={stat.up ? 'emerald-500' : 'rose-500'} strokeWidth={2} />
        </Card>
      ))}
      <Card>
        <Span fontSize={13} color="slate-500" theme={{ dark: { color: 'slate-400' } }}>
          Collection rate
        </Span>
        <Flex ai="center" gap={3} flexWrap="wrap">
          <ProgressRing value={0.86} width="3rem" height="3rem" color="violet-500" />
          <Box>
            <Span fontSize={24} fontWeight={600}>
              86%
            </Span>
            <Box fontSize={13} color="slate-500" theme={{ dark: { color: 'slate-400' } }}>
              of this quarter
            </Box>
          </Box>
        </Flex>
      </Card>
    </Grid>
  );
}
```

## Where the JSON comes from

The three blocks are ordinary sources in the library's own repository, under `registry/blocks/`. They are type-checked by the same `tsc` run as the library, rendered on this page from those same files, and inlined into [the item JSON](https://www.box-kite.dev/r/data-grid.json) at build time. A block that stops compiling cannot be published, and what you install is what is running above.

The catalog is at [https://www.box-kite.dev/registry.json](https://www.box-kite.dev/registry.json), and each item at `/r/<name>.json`.

The components themselves are a package, not a registry item: [install @box-kite/react](https://www.box-kite.dev/installation.md) and import `Button`, `Dialog` or `DataGrid` directly. A block is for the composition above them — the part that is always half yours.

---

_Every page: https://www.box-kite.dev/llms.txt · every prop, measured: https://www.box-kite.dev/props.md_

