> ## Documentation Index
> Fetch the complete documentation index at: https://docs.9am.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Components

> The 19 primitives in the 9AM UI kit — props, variants, and usage examples.

Install any of these with `bunx shadcn@latest add @9am/<name>`. They land in `src/components/ui/`.

## Viewport

The house scroll container, and the piece worth understanding properly. It is not a card — it is a scroller with three behaviours layered on:

* A **custom scrollbar thumb** you can drag, which fades in on hover and disappears entirely when the content does not overflow.
* **Layered glass edges** — a gradient blur at the top and bottom that content dissolves into.
* A **pinning header**. Scroll down and the real header blurs out while a sticky copy animates in above the content.

```jsx theme={null}
import {
  Viewport, ViewportHeader, ViewportTitle, ViewportDescription,
  ViewportAction, ViewportContent, ViewportFooter,
} from "@/components/ui/viewport";

<Viewport className="h-[420px]" fadeColor="var(--card)">
  <ViewportHeader className="px-4">
    <ViewportTitle className="!font-[phudu] text-xl">Vehicle stock</ViewportTitle>
    <ViewportDescription>42 vehicles across 3 dealerships</ViewportDescription>
    <ViewportAction>
      <Button size="sm">Add</Button>
    </ViewportAction>
  </ViewportHeader>

  <ViewportContent className="px-4 pt-2 flex flex-col gap-2">
    {rows}
  </ViewportContent>
</Viewport>
```

| Prop             | Type      | Default             | Description                                                                                                                              |
| ---------------- | --------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `fadeColor`      | `string`  | `var(--background)` | Colour the edge fades blend into. Must match the viewport's own background — pass `var(--card)` when the viewport uses the card surface. |
| `hideBottomFade` | `boolean` | `false`             | Suppress the bottom fade                                                                                                                 |

Plus every standard `div` prop.

<Warning>
  Getting `fadeColor` wrong is the most common mistake. The fade is a real gradient painted over your content — if it does not match the background behind it, you get a visible band at the top and bottom edges.
</Warning>

`ViewportAction` is detected at the React level rather than with CSS `:has()`, because [Chromium 103 does not support it](/ui-kit/cef). Put it inside `ViewportHeader` and the header switches to a two-column grid automatically.

## Button

```jsx theme={null}
import { Button } from "@/components/ui/button";

<Button>Confirm purchase</Button>
<Button variant="default2">Cancel</Button>
<Button variant="destructive" size="sm">Delete</Button>
<Button size="icon"><PlusIcon size={16} /></Button>
```

| Variant       | Appearance                                                                                                |
| ------------- | --------------------------------------------------------------------------------------------------------- |
| `default`     | Solid gold with dark text. The primary action.                                                            |
| `default2`    | Translucent fill, gold text on hover. The workhorse secondary — most header and toolbar buttons use this. |
| `destructive` | Solid red                                                                                                 |
| `outline`     | Bordered, transparent fill                                                                                |
| `secondary`   | Muted solid                                                                                               |
| `ghost`       | No chrome until hovered                                                                                   |
| `link`        | Gold text with a tinted hover                                                                             |
| `navigation`  | Phudu display font, larger. For primary page navigation.                                                  |

| Size         | Height                          |
| ------------ | ------------------------------- |
| `sm`         | 32 px                           |
| `default`    | 36 px                           |
| `lg`         | 40 px                           |
| `icon`       | 36 × 36 px square               |
| `catalogNav` | 36 px, catalog-specific padding |

Supports `asChild` for rendering as a different element.

## DataTable

A TanStack-backed table with search, sorting, and pagination already wired up.

```jsx theme={null}
import { DataTable } from "@/components/ui/data-table";

<DataTable
  columns={columns}
  data={coupons}
  searchPlaceholder={t("ui.dashboard.coupons.searchCoupons")}
  countLabel={(count) => t("ui.common.couponCount", { count })}
  toolbar={<Button onClick={handleCreate}>Create coupon</Button>}
/>
```

| Prop                | Type                         | Description                                                            |
| ------------------- | ---------------------------- | ---------------------------------------------------------------------- |
| `columns`           | `ColumnDef<TData, TValue>[]` | Standard TanStack column definitions                                   |
| `data`              | `TData[]`                    | Rows                                                                   |
| `searchPlaceholder` | `string`                     | Defaults to `ui.common.searchPlaceholder`                              |
| `countLabel`        | `(count: number) => string`  | Footer row count. Defaults to `ui.common.rowCount` ("{count} row(s)"). |
| `toolbar`           | `ReactNode`                  | Rendered to the right of the search field                              |

Pass `countLabel` whenever a domain word reads better than "rows" — that is what stops a coupons table from saying "12 rows".

Pagination is ten rows per page.

## MultiSelect

```jsx theme={null}
import { MultiSelect } from "@/components/ui/multi-select";

<MultiSelect
  options={[
    { value: "sports", label: "Sports" },
    { value: "super", label: "Super" },
  ]}
  selected={selected}
  onSelectionChange={setSelected}
/>
```

| Prop                | Type                                 | Description                               |
| ------------------- | ------------------------------------ | ----------------------------------------- |
| `options`           | `{ value: string; label: string }[]` | Available choices                         |
| `selected`          | `string[]`                           | Currently selected values                 |
| `onSelectionChange` | `(selected: string[]) => void`       | Change handler                            |
| `placeholder`       | `string`                             | Defaults to `ui.common.selectPlaceholder` |
| `searchPlaceholder` | `string`                             | Search field placeholder                  |
| `emptyMessage`      | `string`                             | Shown when the filter matches nothing     |

## Tabs

Two variants. `default` is a segmented control; `line` is the underlined style the dashboard header uses, where the active tab turns gold and grows an underline.

```jsx theme={null}
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";

<Tabs value={currentTab} onValueChange={setCurrentTab}>
  <TabsList variant="line">
    <TabsTrigger value="stock">Stock</TabsTrigger>
    <TabsTrigger value="sales">Sales</TabsTrigger>
  </TabsList>
  <TabsContent value="stock">…</TabsContent>
</Tabs>
```

`Tabs` also accepts `orientation="vertical"`.

## Field

Composable form-row primitives. Use these instead of hand-rolling label and helper-text layout.

```jsx theme={null}
import { Field, FieldLabel, FieldDescription, FieldError } from "@/components/ui/field";

<Field>
  <FieldLabel htmlFor="name">Dealership name</FieldLabel>
  <Input id="name" placeholder="Premium Deluxe Motorsport" />
  <FieldDescription>Shown on the showroom stand.</FieldDescription>
  <FieldError>{errors.name}</FieldError>
</Field>
```

`Field` takes `orientation="vertical"` (default) or `"horizontal"`. `FieldError` renders nothing when it has no children, so you can leave it in unconditionally.

Also exported: `FieldSet`, `FieldLegend`, `FieldGroup`, `FieldContent`, `FieldTitle`, `FieldSeparator`.

## Dialog

```jsx theme={null}
import {
  Dialog, DialogTrigger, DialogContent, DialogHeader,
  DialogTitle, DialogDescription, DialogFooter, DialogClose,
} from "@/components/ui/dialog";

<Dialog>
  <DialogTrigger asChild><Button>Sell vehicle</Button></DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Confirm purchase</DialogTitle>
      <DialogDescription>This debits the buyer and registers the vehicle.</DialogDescription>
    </DialogHeader>
    <DialogFooter>
      <DialogClose asChild><Button variant="default2">Cancel</Button></DialogClose>
      <Button onClick={confirm}>Confirm</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>
```

## InfoTip

A small `?` glyph that reveals an explanation on hover. Use it instead of cramming caveats into a label.

```jsx theme={null}
import { InfoTip } from "@/components/ui/info-tip";

<InfoTip side="right">Players may drive a vehicle before purchase.</InfoTip>
```

| Prop        | Type                                     | Default |
| ----------- | ---------------------------------------- | ------- |
| `side`      | `"top"`, `"right"`, `"bottom"`, `"left"` | `"top"` |
| `className` | `string`                                 | —       |

## Chart

A Recharts wrapper that maps series to theme tokens.

```jsx theme={null}
import { ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart";

const config = {
  revenue: { label: "Revenue", color: "var(--chart-1)" },
};

<ChartContainer config={config} className="h-[240px]">
  <AreaChart data={data}>
    <ChartTooltip content={<ChartTooltipContent />} />
    <Area dataKey="revenue" fill="var(--color-revenue)" />
  </AreaChart>
</ChartContainer>
```

Each key in `config` produces a `--color-<key>` custom property. Also exported: `ChartLegend`, `ChartLegendContent`, `ChartStyle`.

Five chart tokens are defined per palette: `--chart-1` through `--chart-5`.

## Everything else

These follow their shadcn equivalents, restyled to the 9AM surface language.

| Component   | Exports                                                                                                                  |
| ----------- | ------------------------------------------------------------------------------------------------------------------------ |
| `card`      | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardAction`, `CardContent`, `CardFooter`                          |
| `input`     | `Input`                                                                                                                  |
| `label`     | `Label`                                                                                                                  |
| `select`    | `Select`, `SelectTrigger`, `SelectValue`, `SelectContent`, `SelectItem`, `SelectGroup`, `SelectLabel`, `SelectSeparator` |
| `switch`    | `Switch`                                                                                                                 |
| `separator` | `Separator`                                                                                                              |
| `table`     | `Table`, `TableHeader`, `TableBody`, `TableFooter`, `TableHead`, `TableRow`, `TableCell`, `TableCaption`                 |
| `popover`   | `Popover`, `PopoverTrigger`, `PopoverContent`, `PopoverAnchor`, `PopoverHeader`, `PopoverTitle`, `PopoverDescription`    |
| `tooltip`   | `Tooltip`, `TooltipTrigger`, `TooltipContent`, `TooltipProvider`                                                         |
| `calendar`  | `Calendar`, `CalendarDayButton`                                                                                          |

<Note>
  `Tooltip` and `InfoTip` need a `TooltipProvider` above them in the tree. Put one at the root of your app.
</Note>
