# Progress

> A track and a fill, where the fill's width is the value — and this file names neither number.

Source: https://docs.astra-void.xyz/facet/components/progress/

```bash
npx facet-rbxts add progress
```

Copies `ui/progress.tsx`, plus `lib/utils.ts`. Needs `@facet-ui/react-variants`,
`@lattice-ui/react-runtime@^0.8.0` and `@lattice-ui/react-progress@^0.8.0`.

```tsx
import { Progress } from "../shared/ui/progress";

<Progress value={72} />
<Progress indeterminate />
```

_Interactive preview: Two determinate bars and the indeterminate sweep. The sweep is motion, not a class._

Renders a `Frame`. Unknown props forward onto it and are type-checked against it, so a prop `Frame` does not accept is a compile error. The primitive owns `Size` on the indicator, which it animates from the value, so values you pass for it are ignored.

## Props

| Prop | Type | Description |
| --- | --- | --- |
| `value` | `number` | How far along. Read against max. |
| `max` | `number` | The end. Defaults to 100. |
| `indeterminate` | `boolean` | Sweep the indicator back and forth instead of mapping value — for work with no known end. |
| `className` | `ClassName` | Threaded into the root recipe's className slot inside the component. A class written at a Vela-compiled call site never reaches it — see Overriding from the call site. |

There is no `Text`, no label and no percentage readout. A progress bar in this registry is two
rectangles; the words above it are a [`Label`](https://docs.astra-void.xyz/facet/components/label.md) you place yourself.

## The primitive draws nothing either

```tsx
<ProgressPrimitive.Root indeterminate={props.indeterminate} max={props.max} value={props.value}>
  <frame className={cn(progressVariants.root({ className: props.className }))} …>
    <ProgressPrimitive.Indicator className={cn(progressVariants.indicator())} />
  </frame>
</ProgressPrimitive.Root>
```

`Progress.Root` renders no instance — it turns `value` and `max` into a ratio and nothing else — so
the track is a plain `<frame>` in the copied file, with the primitive wrapped around it. Same
inversion as [`Avatar`](https://docs.astra-void.xyz/facet/components/avatar.md#the-circle-is-drawn-here-not-by-the-primitive).

## The indicator has no width

```tsx
export const progressVariants = {
  root: fv("h-2 w-full rounded-full bg-secondary overflow-hidden"),
  indicator: fv("rounded-full bg-primary"),
};
```

Two classes on the indicator, and neither is a size. Its `Size` **is** the value: Lattice's motion
owns the property, animates it as the number moves, and sweeps it end to end when `indeterminate`.

> **A width class here would win the first frame and lose every one after**
>
> That is the failure mode worth naming, because it is the one that looks like it works. Declaring
> `w-1/2` on the indicator renders a half-full bar on mount and then gets overwritten as soon as the
> value changes. [Textarea](https://docs.astra-void.xyz/facet/components/textarea.md#the-input-declares-no-height) and
> [Slider](https://docs.astra-void.xyz/facet/components/slider.md#the-track-has-no-flex--and-that-is-the-point) omit a property for
> the same reason.

`overflow-hidden` on the root is what keeps the fill's square end inside the rounded track:
`ClipsDescendants` clips to the rectangle, and the indicator's own `rounded-full` does the corners.

## `indeterminate` is not a value

`<Progress indeterminate />` ignores `value` entirely. Use it for work whose end you cannot measure
— a server round trip, an asset load with no progress signal — rather than passing a fake number
that never advances.
