# twelfth UI: full docs

> Open-source (MIT) shadcn components for category managers and retail buying teams, by twelfth (https://twelfth.ai). React and TypeScript on shadcn's Base UI styles and Tailwind CSS v4. Components take plain props and fetch no data. Index: https://ui.twelfth.ai/llms.txt · Source: https://github.com/twelfth-ai/catman-ui

Install any block with `npx shadcn@latest add https://ui.twelfth.ai/r/<name>.json`; each one brings the pieces it builds on. Or register `"@twelfth": "https://ui.twelfth.ai/r/{name}.json"` under `registries` in components.json and run `npx shadcn@latest add @twelfth/<name>`.

## Price position

Where our prices sit against one competitor: 12% above, 4% below, in line. As a chip for a table cell, or a badge that opens the index and the matched-line evidence on hover or focus.

Docs: https://ui.twelfth.ai/price-position · Category: Pricing · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/price-position.json
```

```tsx
import { PricePositionBadge, PricePositionChip } from "@/components/ui/price-position"
```

### Example

```tsx
<PricePositionBadge
  competitor="JB Hi-Fi"
  positionPct={12.4}
  evidence={{ matched: 1284, above: 806, below: 402 }}
  period="8-week comparison"
  channel="Direct retail"
  action={<Link href="/matched">View matched items</Link>}
/>
```

### Badge in a row

Sweep across them: one card open at a time, never stacked.

```tsx
{competitors.map((c) => (
  <PricePositionBadge
    key={c.id}
    competitor={c.name}
    positionPct={c.positionPct}
    evidence={{ matched: c.matched, above: c.above, below: c.below }}
    period="8-week comparison"
  />
))}
```

### Chip

No card: for a dense table column or an inline mention.

```tsx
<PricePositionChip positionPct={12.4} />
<PricePositionChip positionPct={-4.2} />
<PricePositionChip positionPct={0.3} />
<PricePositionChip positionPct={null} />
```

### PricePositionBadge props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `competitor` (required) | `string` |  | Who the comparison is against; named in the card and the label. |
| `positionPct` (required) | `number \| null` |  | Median gap across comparable lines, ours vs theirs, in percent. null when too few lines matched. |
| `evidence` (required) | `{ matched, above, below }` |  | How many lines the position rests on and which way they split. |
| `period` | `string` |  | e.g. "8-week comparison". |
| `channel` | `string` |  | e.g. "Direct retail" or "Marketplace sellers". |
| `action` | `ReactNode` |  | A link or button under the evidence. |
| `className` | `string` |  | Extra classes on the chip. |

### PricePositionChip props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `positionPct` (required) | `number \| null` |  | As above. |
| `className` | `string` |  | Extra classes. |

### Notes

- Opening one card closes any other (one-open), so a row of badges never stacks cards.
- The arrow carries the direction for readers who cannot separate the two tones.
- Reads the favourable and exposed colour tokens installed by market-side.

## Price scoreboard

The four numbers at the top of a pricing page: the range's position against the market with its index and trend, then how many lines are priced above, in line and below. The counts can filter the list underneath.

Docs: https://ui.twelfth.ai/price-scoreboard · Category: Pricing · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/price-scoreboard.json
```

```tsx
import { PriceScoreboard, type PriceVerdict } from "@/components/ui/price-scoreboard"
```

### Example

```tsx
const [selected, setSelected] = useState<PriceVerdict | null>(null)

<PriceScoreboard
  positionPct={-8.6}
  trend={weeklyPosition}
  counts={{ above: 146, in_line: 251, below: 635 }}
  unitShares={{ above: 18, in_line: 27, below: 55 }}
  selected={selected}
  onSelect={(v) => setSelected((s) => (s === v ? null : v))}
/>
```

### As filters

Click a count to filter; click it again to clear. The selected tile is spotlit, not hovered.

```tsx
<PriceScoreboard
  positionPct={-8.6}
  trend={weeklyPosition}
  counts={{ above: 146, in_line: 251, below: 635 }}
  unitShares={{ above: 18, in_line: 27, below: 55 }}
  selected={selected}
  onSelect={setSelected}
/>
```

### Read-only

```tsx
<PriceScoreboard positionPct={0.3} trend={weeklyPosition} counts={{ above: 12, in_line: 88, below: 9 }} />
```

### No position yet

```tsx
<PriceScoreboard positionPct={null} counts={{ above: 0, in_line: 4, below: 1 }} />
```

### PriceScoreboard props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `positionPct` (required) | `number \| null` |  | The range's overall position vs market, in percent. |
| `counts` (required) | `{ above, in_line, below }` |  | Lines in each verdict. |
| `trend` | `PriceTrendPoint[]` | `[]` | Weekly position for the sparkline beside the index. |
| `unitShares` | `{ above?, in_line?, below? }` |  | Each verdict's share of recent units, in percent. |
| `selected` | `PriceVerdict \| null` | `null` | The verdict filtering the list. |
| `onSelect` | `(verdict) => void` |  | Makes the counts filters. |
| `info` | `ReactNode` |  | Beside the position, e.g. an (i) explaining the bands. |

### Notes

- Lays out on its own container width (@container), so it fits a narrow panel as well as a page.

## Price trend

Weekly price position against the market as a small line with a dashed parity line. Hover, or focus and use the arrow keys, to read any week's position, index and change.

Docs: https://ui.twelfth.ai/price-trend · Category: Pricing · Updated: 2026-11-02

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/price-trend.json
```

```tsx
import { PriceTrend } from "@/components/ui/price-trend"
```

### Example

```tsx
<PriceTrend
  points={weeklyPosition} // [{ week: "2026-07-27", positionPct: 4 }, …]
  label="vs JB Hi-Fi"
  against="JB Hi-Fi"
  width={120}
  height={32}
  smooth={true}
/>
```

### Drifting above

```tsx
<PriceTrend points={points} label="vs market" />
<PriceTrend points={points} label="vs market" width={48} height={16} />
```

### Crossing parity

```tsx
<PriceTrend points={points} label="vs market" />
<PriceTrend points={points} label="vs market" width={48} height={16} />
```

### Gaps in the data

```tsx
<PriceTrend points={points} label="vs market" />
<PriceTrend points={points} label="vs market" width={48} height={16} />
```

### PriceTrend props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `points` (required) | `{ week: string; positionPct: number \| null }[]` |  | Weekly position, oldest first. A null week breaks the line. |
| `label` (required) | `string` |  | What the line is, for the card and screen readers. |
| `against` | `string` | `"the market"` | Who the comparison is against, in the explainer. |
| `width` | `number` | `96` | Pixels. |
| `height` | `number` | `28` | Pixels. |
| `inverse` | `boolean` | `false` | On a dark tile: the line takes the tile's text colour. |
| `smooth` | `boolean` | `true` | Gently rounds joins without overshooting the observed values. |

### Notes

- The y axis is symmetric around zero, so parity is always the middle and above is always up.
- Opens instantly and shares one-open with the price badges.

## Price band

A price band as a picture: how far from the market a line can sit, across shelf prices on a log scale, before it reads as priced above or below market. Tiers step the band; dollar limits narrow it. Hover for the band at any price.

Docs: https://ui.twelfth.ai/price-band · Category: Pricing · Updated: 2026-11-02

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/price-band.json
```

```tsx
import { PriceBandChart, PriceBandCounts, PriceBandMini } from "@/components/ui/price-band"
```

### Example

```tsx
<PriceBandChart
  band={{
    abovePct: 10,
    belowPct: 10,
    tiers: [{ fromPrice: 500, abovePct: 5, belowPct: 5 }],
  }}
  counts={{ below: 635, inLine: 251, above: 146 }}
  countsNote="Your lines now:"
/>
```

### Price tiers

Each tier starts at its shelf price: the band steps there, and the price is marked on the axis.

```tsx
<PriceBandChart
  band={{
    abovePct: 15,
    belowPct: 12,
    tiers: [
      { fromPrice: 100, abovePct: 10, belowPct: 8 },
      { fromPrice: 1000, abovePct: 5, belowPct: 4 },
    ],
  }}
/>
```

### Dollar limits

A side capped in dollars narrows as the price grows: 10% of a $2,000 line is more than $40.

```tsx
<PriceBandChart band={{ abovePct: 10, belowPct: 8, aboveAmount: 40, belowAmount: 25 }} />
```

### In a table row

The mini picture beside the band's own words, with where the lines sit today.

```tsx
<div className="flex items-center gap-2">
  <PriceBandMini band={band} />
  <div>
    <span className="font-mono">{band.words}</span>
    <PriceBandCounts counts={{ below: 96, inLine: 301, above: 58 }} className="mt-0.5 flex" />
  </div>
</div>
```

### PriceBandChart props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `band` (required) | `PriceBandShape` |  | { abovePct, belowPct, aboveAmount?, belowAmount?, tiers?: [{ fromPrice, … }] }: in line from belowPct under to abovePct over market; an amount caps that side in dollars; a tier takes over from its shelf price. |
| `counts` | `{ below, inLine, above } \| null` |  | Lines in each zone today, shown under the plot while nothing is hovered. |
| `countsNote` | `string` |  | Before the counts, e.g. "Your lines now:". |
| `labels` | `Partial<PriceBandLabels>` |  | The chart's own words: above, below, inLine, market, axis and the screen-reader description. |
| `className` | `string` |  | Extra classes on the figure. |

### PriceBandMini props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `band` (required) | `PriceBandShape` |  | As above. No axis or labels; hidden from screen readers, so put the band's words beside it. |
| `className` | `string` |  | Extra classes. |

### PriceBandCounts props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `counts` (required) | `{ below, inLine, above }` |  | Lines in each zone. |
| `note` | `string` |  | Before the counts. |
| `className` | `string` |  | Extra classes. |

### Notes

- Prints no wording for the band itself ("±10% under $500"): put the band's words beside it, from wherever your bands are defined, so they read one way everywhere.
- Shelf price runs on a log scale, so a $5 line and a $5,000 line both get room; the gap scale is symmetric around the market line.
- Reads the favourable and exposed colour tokens installed by market-side.

## Tax basis

Whether a price includes tax, as a badge the price sits inside: filled when the tax is in the figure, hollow when it is left out. Hover explains the tax, its rate and the price on the other basis. Use it on a figure, alone in a column header, or show a price on both bases at once so an ex-tax figure can be checked against the shelf.

Docs: https://ui.twelfth.ai/tax-basis · Category: Pricing · Updated: 2026-11-02

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/tax-basis.json
```

```tsx
import { PriceFigure, TaxBasisMark } from "@/components/ui/tax-basis"
```

### Example

```tsx
<PriceFigure
  value={136.35}
  basis="exclusive"
  taxBasis="both"
  taxLabel="GST"
  taxRatePct={10}
/>
```

### Inc and ex, any tax label

Short single-word labels print as they are; longer ones print as "tax" and keep the full label in the title and spoken text.

```tsx
<TaxBasisMark basis="inclusive" taxLabel="VAT" />  // INC VAT, filled
<TaxBasisMark basis="exclusive" taxLabel="GST" />  // EX GST, hollow
<TaxBasisMark basis="exclusive" taxLabel="Sales tax" /> // EX TAX
```

### show, both, hide

both needs a rate. Without one it shows the one figure; without a basis it shows no mark at all.

```tsx
<PriceFigure value={136.35} basis="exclusive" />                             // $136.35 EX GST
<PriceFigure value={136.35} basis="exclusive" taxBasis="both" taxRatePct={10} /> // … $149.99 INC GST
<PriceFigure value={136.35} basis="exclusive" taxBasis="hide" />             // $136.35
<PriceFigure value={136.35} basis="exclusive" taxBasis="both" />             // no rate: one figure
<PriceFigure value={136.35} basis={null} taxBasis="both" taxRatePct={10} />  // no basis: no mark
```

### Named once, in a header

A column on one basis names it in the header and hides it in the cells.

```tsx
<th>Our price <TaxBasisMark basis="exclusive" taxLabel="GST" /></th>
<td><PriceFigure value={21.35} basis="exclusive" taxBasis="hide" /></td>
```

### Both, stacked, in a list

```tsx
<PriceFigure value={22.72} basis="inclusive" taxBasis="both" taxLabel="VAT" taxRatePct={20} locale="en-GB" currency="GBP" stacked />
```

### PriceFigure props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `number \| null` |  | The price, on basis. null prints —. |
| `basis` (required) | `"inclusive" \| "exclusive" \| null` |  | The basis value is on. null when the source did not say: no mark, nothing converted. |
| `taxBasis` | `"show" \| "hide" \| "both"` | `"show"` | The figure with its mark, the figure alone, or the price on both bases. |
| `taxLabel` | `string` | `"GST"` | GST, VAT, GST/HST, Sales tax… |
| `taxRatePct` | `number \| null` | `null` | The rate behind both, in percent. Outside 0–30 counts as unknown. |
| `short` | `boolean` | `false` | Badges print inc / ex without the label. |
| `explain` | `"focus" \| "hover" \| false` | `"focus"` | How the badge explains the tax. |
| `note` | `ReactNode` |  | A further line for the hover, e.g. how it was converted. |
| `stacked` | `boolean` | `false` | With both, the second figure goes underneath. |
| `format` | `(value: number) => string` |  | Formats each figure; defaults to formatMoney. |
| `locale / currency` | `string` | `"en-AU" / "AUD"` | For the default format. |

### TaxBasisMark props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `basis` (required) | `"inclusive" \| "exclusive"` |  | Filled for inclusive, hollow for exclusive. |
| `taxLabel` | `string` | `"GST"` | The tax's name. |
| `short` | `boolean` | `false` | inc / ex only, where width is tight. |
| `children` | `ReactNode` |  | The figure the badge holds; omit for the basis alone. |
| `taxRatePct / other / note` | `number \| string \| ReactNode` |  | Rate, other-basis price and a further line for the hover. |
| `explain` | `"focus" \| "hover" \| false` | `"focus"` | Tooltip on hover and focus, hover only (inside a link), or none. |

### otherBasisPrice / taxBasisWords props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `otherBasisPrice(value, basis, taxRatePct)` | `number \| null` |  | The price on the other basis; null without a basis or a plausible rate. |
| `taxBasisWords(basis, taxLabel)` | `string` |  | "including GST" / "excluding GST". |

### Notes

- Screen readers hear "including GST" / "excluding GST". Hover or focus explains the tax in full (GST: Goods and Services Tax), with the rate and the other-basis price when known. Use explain="hover" inside a link or button, explain={false} inside a tooltip.
- The badge takes its colours from the text around it, so it reads on a card, in a header and in an inverted tooltip without a variant.
- Never guesses: no basis means no badge, and no rate means no second figure and no rate in the hover.

## Micro trend

One or more lines over time, small enough for a table cell. Forecasts read as forecasts: dashed, with a P10–P90 band and a marker where actuals stop. Hover to glide the crosshair and read every series.

Docs: https://ui.twelfth.ai/micro-trend · Category: Charts · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/micro-trend.json
```

```tsx
import { MicroTrend } from "@/components/ui/micro-trend"
```

### Example

```tsx
<MicroTrend
  label="Unit sales and forecast, 12 weeks"
  points={weeks} // [{ at: "2026-08-03", values: { actual: 420, forecast: null, p10: null, p90: null } }, …]
  series={[
    { key: "actual", label: "Units sold", color: "#0d9488", weight: 1.8 },
    { key: "forecast", label: "Forecast", color: "#64748b", dashed: true },
  ]}
  bands={[{ lower: "p10", upper: "p90", label: "P10–P90" }]}
  marker={{ at: thisWeek, label: "This week" }}
  width={160}
  height={44}
  formatValue={(n) => n.toLocaleString("en-AU")}
  footer={<>
    <ChangeIndicator label="vs fcst" value={6} higherIs="better" format={(n) => `${n}%`} />
    <MicroStat label="MAPE" value="11%" hint="Forecast error, last 8 weeks" />
  </>}
/>
```

### This year vs last

```tsx
<MicroTrend
  label="Unit sales, this year against last"
  points={weeks}
  series={[
    { key: "ty", label: "This year", color: "#0d9488", weight: 1.8 },
    { key: "ly", label: "Last year", color: "#a8a29e" },
  ]}
  footer={<ChangeIndicator label="vs LY" value={6.2} higherIs="better" format={(n) => `${n}%`} />}
/>
```

### Stock vs reorder point

Actual stock, then projected at the forecast rate of sale, against the reorder point.

```tsx
<MicroTrend
  label="Stock on hand, actual then projected"
  points={weeks}
  series={[
    { key: "soh", label: "Stock on hand", weight: 1.8 },
    { key: "projected", label: "Projected", dashed: true },
  ]}
  reference={{ value: 600, label: "reorder point", tone: "exposed" }}
  marker={{ at: thisWeek, label: "This week" }}
  footer={<MicroStat label="reorder in" value="2 wks" />}
/>
```

### Sell-through vs plan

```tsx
<MicroTrend
  label="Sell-through against plan"
  points={weeks}
  series={[{ key: "actual", label: "Sell-through" }, { key: "plan", label: "Plan", dashed: true }]}
  formatValue={(n) => `${n}%`}
/>
```

### In a table

Right-aligned in a cell, the way a range review reads.

```tsx
<td className="text-right">
  <MicroTrend align="end" label={line.name} points={line.weeks} series={series} bands={bands} />
</td>
```

### Not enough data

```tsx
<MicroTrend label="New line" points={[oneWeek]} series={series} /> // renders "collecting"
```

### MicroTrend props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `points` (required) | `{ at, values: Record<string, number \| null> }[]` |  | Samples in any order; sorted by time. A null value breaks that series' line. |
| `series` (required) | `{ key, label, color?, weight?, dashed? }[]` |  | Which value keys to draw and how. |
| `label` (required) | `string` |  | What the chart shows, for screen readers. |
| `bands` | `{ lower, upper, label, color? }[]` | `[]` | Shaded ranges between two keys, e.g. P10 to P90. |
| `marker` | `{ at, label }` |  | A vertical line, e.g. where actuals stop. |
| `reference` | `{ value, label?, tone? }` |  | A dashed horizontal line: a target, a reorder point. |
| `domain` | `[number, number]` |  | The value axis. Defaults to the data's range. |
| `zeroBased` | `boolean` | `false` | Start the default axis at zero. |
| `range` | `[Date \| number, Date \| number]` |  | The time axis. Defaults to first to last sample. |
| `gapAfter` | `number (ms)` |  | Break a line where two samples are further apart than this. |
| `formatValue` | `(value, series) => string` |  | Values in the tooltip. |
| `formatAt` | `(date) => string` |  | The tooltip's heading. |
| `readout` | `(point) => ReactNode` |  | Extra tooltip lines for the hovered sample. |
| `footer` | `ReactNode` |  | Under the chart, e.g. a ChangeIndicator and a MicroStat. |
| `onExpand` | `() => void` |  | Makes the chart a button that opens a larger view. |
| `width / height` | `number` | `120 / 34` | Pixels. |
| `align` | `"start" \| "end"` | `"end"` | Footer alignment; end for a numeric column. |

### Notes

- Lines draw in on mount; reduced motion skips it. The crosshair and dots glide between samples.
- Arrow keys, Home and End step through samples when the chart has focus.
- Uses your tooltip component; the stock shadcn one inverts colours, which the readout is written to suit.

## Micro stat

The figures under or beside a chart. ChangeIndicator is a movement with its verdict, toned by whether up is good or bad. MicroStat is a labelled figure. Both explain themselves in a tooltip.

Docs: https://ui.twelfth.ai/micro-stat · Category: Charts · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/micro-stat.json
```

```tsx
import { ChangeIndicator, MicroStat } from "@/components/ui/micro-stat"
```

### Example

```tsx
<ChangeIndicator
  label="vs LY"
  value={6.2}
  higherIs="better"
  format={(n) => `${n}%`}
  flatWithin={1}
  strongAt={8}
  hint="How the change was measured"
/>
```

### A rise is good news

```tsx
<ChangeIndicator label="vs LY" value={6.2} higherIs="better" format={(n) => `${n}%`} />
```

### A rise is bad news

```tsx
<ChangeIndicator label="MAPE" value={7} higherIs="worse" format={(n) => `${n} pts`} strongAt={5} />
```

### Labelled figures

```tsx
<MicroStat label="OTB" value="$42k" hint="Open-to-buy left this month" />
```

### ChangeIndicator props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `number \| null` |  | Signed change; null renders nothing. |
| `higherIs` (required) | `"better" \| "worse"` |  | Whether a rise is good news. |
| `label` | `string` |  | What moved, e.g. "vs LY". |
| `format` | `(magnitude) => string` |  | Formats the size of the change. |
| `flatWithin` | `number` | `0` | Changes this small read "flat". |
| `strongAt` | `number` |  | A worsening at least this large gets a filled chip. |
| `hint` | `ReactNode` |  | Explains how it was measured, in a tooltip. |

### MicroStat props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  | e.g. "OTB". |
| `value` (required) | `ReactNode` |  | e.g. "$42k". |
| `hint` | `ReactNode` |  | Tooltip sentence. |

## Weeks of cover

How many weeks the stock on hand lasts at the current rate of sale, on a track against the band the buyer wants it in: short is a stockout risk, over is cash on a shelf.

Docs: https://ui.twelfth.ai/weeks-of-cover · Category: Stock · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/weeks-of-cover.json
```

```tsx
import { WeeksOfCover } from "@/components/ui/weeks-of-cover"
```

### Example

```tsx
<WeeksOfCover
  weeks={18}
  min={4}
  max={12}
  scale={24}
/>
```

### Every state

```tsx
<WeeksOfCover weeks={1.5} min={4} max={12} scale={24} /> // 2.5 wks short
<WeeksOfCover weeks={6} min={4} max={12} scale={24} />   // In band
<WeeksOfCover weeks={18} min={4} max={12} scale={24} />  // 6 wks over
<WeeksOfCover weeks={null} min={4} max={12} />           // No net sales
```

### Aged stock line

No lower bound: anything past 26 weeks is aged cash.

```tsx
<WeeksOfCover weeks={41} max={26} scale={52} />
```

### Dense, for a table cell

```tsx
<WeeksOfCover weeks={34} min={4} max={12} scale={24} showState={false} />
```

### WeeksOfCover props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `weeks` (required) | `number \| null` |  | Saleable units ÷ weekly rate of sale. null when nothing sold. |
| `max` (required) | `number` |  | Top of the healthy band. |
| `min` | `number` | `0` | Bottom of the band, e.g. lead time plus safety stock. |
| `scale` | `number` | `max × 1.5` | Weeks the track spans; beyond it the figure reads ›. |
| `showState` | `boolean` | `true` | Say "6 wks over" beside the figure. |

### Notes

- role="meter" with the value, band and state in its label.

## Market side

The tone every catman figure shares: favourable, exposed, level, unknown. Named after the reader's question (is this going my way) rather than the arithmetic, plus the arrow that carries it without colour and the two colour tokens behind it.

Docs: https://ui.twelfth.ai/market-side · Category: Foundations · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/market-side.json
```

```tsx
import { MarketArrow, marketSideFromPosition, marketToneClass } from "@/components/ui/market-side"
```

### Example

```tsx
<span className={cn("rounded px-2 py-1", marketToneClass.exposed)}>
  <MarketArrow side="exposed" pointing="ours" />
  …
</span>
```

### All four tones

```tsx
marketToneClass.favourable // bg-favourable text-favourable-foreground
marketToneClass.exposed    // bg-exposed text-exposed-foreground
```

### MarketArrow props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` (required) | `MarketSide` |  | Which tone. |
| `pointing` | `"theirs" \| "ours"` | `"theirs"` | A gap points their way; an index points ours. |

### marketSideFromPosition props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `positionPct` (required) | `number \| null` |  | Above market is exposed, below favourable, within half a percent level. |

### Notes

- Installs --favourable, --favourable-foreground, --exposed, --exposed-foreground with light and dark values.

## Retail format

Money, units, percent, price index, position words and weeks, formatted the way a category manager reads them. en-AU and AUD by default.

Docs: https://ui.twelfth.ai/retail-format · Category: Foundations · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/retail-format.json
```

```tsx
import { formatIndex, formatMarketPosition, formatMoney, formatPercent, formatUnits, formatWeeks } from "@/lib/retail-format"
```

### Example

```tsx
formatMoney(1240, { locale: "en-AU", currency: "AUD" })
formatPercent(12.4)        // "12%"
formatIndex(12.4)          // "112.4"
formatMarketPosition(12.4) // "12% above market"
```

### functions props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `formatMoney(value, { locale?, currency?, whole? })` | `string` |  | $1,234.50; whole drops the cents. |
| `formatUnits(value, { locale? })` | `string` |  | 12,840 |
| `formatPercent(value, { locale?, digits? })` | `string` |  | Already in percent units: 12.4 → 12%. |
| `formatIndex(positionPct)` | `string` |  | Ours ÷ theirs × 100: 12.4 → 112.4. |
| `formatPosition / formatMarketPosition` | `string` |  | 12% above / 12% above market / In line with market. |
| `formatWeeks(weeks)` | `string` |  | 6.3 wks, 1 wk. |

## One open

Keeps one hover card open at a time. Sweep across a row of badges and the card follows the pointer instead of stacking. Used by price-position and price-trend; use it for your own hover cards.

Docs: https://ui.twelfth.ai/one-open · Category: Foundations · Updated: 2026-10-30

### Install

```bash
npx shadcn@latest add https://ui.twelfth.ai/r/one-open.json
```

```tsx
import { useOneOpen } from "@/lib/one-open"
```

### A row of hover cards

Move quickly from one to the next.

```tsx
const [open, setOpen] = useOneOpen()

<Popover open={open} onOpenChange={setOpen}>
  <PopoverTrigger openOnHover>…</PopoverTrigger>
  <PopoverContent>…</PopoverContent>
</Popover>
```

### useOneOpen() props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `returns` | `[open: boolean, onOpenChange: (open) => void]` |  | Pass straight to a controlled Popover or Tooltip. |
