{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "progress",
  "title": "Progress",
  "description": "How far something you started has got: a hairline and an italic percentage, the words themselves inking in, one ring per item, a pair of parentheses closing on the words, or a tally. Done, a full stop lands.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json",
    "https://thedirectors.agency/ui/r/fraction.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/progress.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { Fraction } from \"@/components/ui/fraction\"\n\ntype ProgressProps = Omit<React.ComponentProps<\"div\">, \"children\"> & {\n  /**\n   * hairline (a line that fills toward a ring while an italic percentage ticks), sentence (the words themselves\n   * ink in, in reading order) and count (a fraction of items, and one ring per item, like days on a calendar).\n   * parentheses (the work still to do is the silence inside a pair of parentheses, which closes on the words) and\n   * tally (one stroke per item, gated in fives, as a hand counts). All of them end the same way: when it's done, a full stop lands.\n   */\n  variant?: \"hairline\" | \"sentence\" | \"count\" | \"parentheses\" | \"tally\"\n  /** How much is done, out of max. Leave it out (or pass null) while nobody knows yet: the bar is indeterminate. */\n  value?: number | null\n  /** The total. A count shows one ring per item and a tally one stroke per item, up to 100. */\n  max?: number\n  /** What is being done, in words (\"Uploading 12 files\"), without a full stop. It labels the bar; the sentence variant is made of it. */\n  label?: React.ReactNode\n}\n\n/**\n * How far something the person started has got. Native <progress> underneath, named by a native <label>.\n * One number, --ot-progress-p (0–1), is eased in CSS, so the fill, the numeral and the ink glide together.\n */\nfunction Progress({ variant = \"hairline\", value = null, max = 100, label, className, style, ...props }: ProgressProps) {\n  const id = React.useId()\n  const known = value != null && Number.isFinite(value) && max > 0\n  const done = known ? Math.min(Math.max(value, 0), max) : 0\n  const p = known ? done / max : 0\n  const state = !known ? \"indeterminate\" : done >= max ? \"complete\" : \"loading\"\n  const paren = variant === \"parentheses\"\n  const counted = variant === \"count\" || variant === \"tally\"\n  // ponytail: one mark per item up to 100; past that each mark stands for a share, since a thousand rings is a texture, not a count.\n  const units = counted && max > 0 ? Math.min(Math.round(max), 100) : 0\n  const landed = Math.floor(p * units)\n  const mark = (i: number) => (\n    <i key={i} data-done={i < landed || undefined} data-now={(state === \"loading\" && i === landed) || undefined} style={{ \"--i\": i } as React.CSSProperties} />\n  )\n  return (\n    <div\n      data-slot=\"progress\"\n      data-variant={variant}\n      data-state={state}\n      className={cn(\"ot-progress\", className)}\n      style={{ \"--ot-progress-p\": p, ...style } as React.CSSProperties}\n      {...props}\n    >\n      {paren ? (\n        <>\n          <span className=\"ot-progress-paren\" aria-hidden=\"true\">(</span>\n          <i className=\"ot-progress-rest\" aria-hidden=\"true\" />\n        </>\n      ) : null}\n      {label != null ? (\n        <label data-slot=\"progress-label\" className=\"ot-progress-label\" htmlFor={id}>\n          <span>{label}</span>\n        </label>\n      ) : null}\n      {paren ? (\n        <>\n          <i className=\"ot-progress-rest\" aria-hidden=\"true\" />\n          <span className=\"ot-progress-paren\" aria-hidden=\"true\">)</span>\n        </>\n      ) : null}\n      {variant === \"hairline\" ? (\n        <p data-slot=\"progress-value\" className=\"ot-progress-value\" aria-hidden=\"true\">\n          <small>%</small>\n        </p>\n      ) : null}\n      {counted ? <Fraction data-slot=\"progress-count\" className=\"ot-progress-count\" aria-hidden=\"true\" count={Math.floor(done)} total={max} /> : null}\n      <progress\n        id={id}\n        value={known ? done : undefined}\n        max={max}\n        aria-label={label == null ? \"Progress\" : undefined}\n        aria-valuetext={counted && known ? `${Math.floor(done)} of ${max}` : undefined}\n      />\n      {variant === \"hairline\" ? <span data-slot=\"progress-stop\" className=\"ot-progress-stop\" aria-hidden=\"true\" /> : null}\n      {units > 0 && variant === \"count\" ? (\n        <span data-slot=\"progress-units\" className=\"ot-progress-units\" aria-hidden=\"true\" style={{ \"--n\": units } as React.CSSProperties}>\n          {Array.from({ length: units }, (_, i) => mark(i))}\n        </span>\n      ) : null}\n      {units > 0 && variant === \"tally\" ? (\n        <span data-slot=\"progress-tally\" className=\"ot-progress-tally\" aria-hidden=\"true\" style={{ \"--n\": units } as React.CSSProperties}>\n          {/* Gates of five: four strokes stand, the fifth crosses them. */}\n          {Array.from({ length: Math.ceil(units / 5) }, (_, g) => (\n            <b key={g}>{Array.from({ length: Math.min(5, units - g * 5) }, (_, k) => mark(g * 5 + k))}</b>\n          ))}\n        </span>\n      ) : null}\n    </div>\n  )\n}\n\nexport { Progress, type ProgressProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/progress.css",
      "content": "@layer components {\n  /* ── ot-progress ── how far something you started has got, said five ways with one ending: when it's done, a full stop lands.\n     One number drives every variant: --ot-progress-p (0–1), set by the component and eased here, so the fill, the ticking\n     numeral and the inked words glide together. Under reduced motion the tempo is 1ms, so they simply change. */\n  @property --ot-progress-p { syntax: \"<number>\"; inherits: true; initial-value: 0; }\n  @property --ot-progress-n { syntax: \"<integer>\"; inherits: false; initial-value: 0; }\n  .ot-progress {\n    --ot-progress-p: 0;\n    position: relative; display: grid; inline-size: 100%; grid-template-columns: minmax(0, 1fr) auto; align-items: baseline; column-gap: var(--ot-space-5); row-gap: var(--ot-space-3);\n    transition: --ot-progress-p var(--ot-andante) var(--ot-exhale);\n  }\n  .ot-progress-label { grid-column: 1; color: var(--ot-graphite); }\n\n  /* Hairline. The percentage is set in the italic and ticks through every number on its way: a counter on a registered\n     integer that follows the eased value, rounded down, so it never says 100 before the work is done. */\n  .ot-progress-value {\n    grid-column: 2; justify-self: end; white-space: nowrap;\n    font-family: var(--ot-expression); font-style: italic; font-size: calc(var(--ot-expression-scale) * var(--ot-f)); font-weight: 400; line-height: 0.9; letter-spacing: 0;\n    color: var(--ot-ink); font-variant-numeric: lining-nums tabular-nums;\n  }\n  .ot-progress-value::before { --ot-progress-n: round(down, var(--ot-progress-p) * 100, 1); counter-reset: ot-progress var(--ot-progress-n); content: counter(ot-progress); }\n  .ot-progress-value small { margin-inline-start: 0.08em; font-family: var(--ot-voice); font-size: 0.42em; font-style: normal; font-weight: 400; letter-spacing: 0; color: var(--ot-pencil); }\n  /* The line draws its own track and fill from the eased number, so every engine glides the same way; the native bar is emptied. */\n  .ot-progress > progress {\n    grid-row: 2; grid-column: 1 / -1; justify-self: stretch; align-self: center; display: block;\n    -webkit-appearance: none; appearance: none; inline-size: auto; block-size: var(--ot-dot); margin: 0; margin-inline-end: calc(9px + var(--ot-space-2)); border: 0; color: var(--ot-ink);\n    background:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) 0 50% / calc(var(--ot-progress-p) * 100%) var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-rule), var(--ot-rule)) 0 50% / 100% var(--ot-hairline) no-repeat;\n  }\n  .ot-progress > progress::-webkit-progress-bar { background: transparent; }\n  .ot-progress > progress::-webkit-progress-value { background: transparent; }\n  .ot-progress > progress::-moz-progress-bar { background: transparent; }\n  /* The line runs toward a ring, the way the calendar marks a day to come. Done, the ring fills into a full stop and lands. */\n  .ot-progress-stop {\n    grid-row: 2; grid-column: 2; justify-self: end; align-self: center;\n    inline-size: 9px; block-size: 9px; border-radius: 50%; scale: 0.78;\n    box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-rule-strong);\n    transition: box-shadow var(--ot-moderato) var(--ot-exhale), scale var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-progress[data-state=\"complete\"] .ot-progress-stop { box-shadow: inset 0 0 0 5px var(--ot-ink); scale: 1; transition-delay: var(--ot-moderato); }\n\n  /* Sentence. The words are the bar: they stand in pencil and ink in, in reading order, with a soft wet edge. The gradient\n     is laid along one inline and sliced across its lines, so a sentence that wraps still inks line after line. */\n  .ot-progress[data-variant=\"sentence\"] { display: block; }\n  .ot-progress[data-variant=\"sentence\"] > .ot-progress-label { display: block; font-size: var(--ot-mf); line-height: var(--ot-mf-lh); letter-spacing: var(--ot-mf-tr); font-weight: 300; color: var(--ot-ink); text-wrap: pretty; }\n  .ot-progress[data-variant=\"sentence\"] > .ot-progress-label > span {\n    --ot-progress-soak: 1.4em;\n    background-image: linear-gradient(to right, var(--ot-ink) calc(var(--ot-progress-p) * (100% + var(--ot-progress-soak)) - var(--ot-progress-soak)), var(--ot-rule-strong) calc(var(--ot-progress-p) * (100% + var(--ot-progress-soak))));\n    -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;\n    -webkit-box-decoration-break: slice; box-decoration-break: slice;\n  }\n  .ot-progress[data-variant=\"sentence\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) > .ot-progress-label > span {\n    background-image: linear-gradient(to left, var(--ot-ink) calc(var(--ot-progress-p) * (100% + var(--ot-progress-soak)) - var(--ot-progress-soak)), var(--ot-rule-strong) calc(var(--ot-progress-p) * (100% + var(--ot-progress-soak))));\n  }\n  /* Its full stop is always in the line, unseen, so nothing reflows; done, it drops onto the baseline with one small rebound. */\n  .ot-progress:is([data-variant=\"sentence\"], [data-variant=\"parentheses\"]) > .ot-progress-label::after {\n    content: \".\"; position: relative; inset-block-start: -0.4em; opacity: 0; color: var(--ot-ink);\n    transition: opacity var(--ot-allegro) var(--ot-exhale), inset-block-start var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-progress:is([data-variant=\"sentence\"], [data-variant=\"parentheses\"])[data-state=\"complete\"] > .ot-progress-label::after { inset-block-start: 0; opacity: 1; transition-delay: var(--ot-moderato); }\n\n  /* Count. For things you can count: the fraction's count rolls as each item lands, and below it one ring per item, like days\n     on the calendar. Done is a dot that lands, the one under way is an ink ring, and the rest are pencil rings. The rings share\n     the whole measure, first at the start and last at the end; past one row they fall into columns, a calendar's grid. */\n  .ot-progress-count { grid-column: 2; justify-self: end; }\n  .ot-progress-units { grid-row: 2; grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fit, 9px); justify-content: space-between; gap: var(--ot-space-2) 6px; padding-block-start: var(--ot-space-2); }\n  .ot-progress-units > i {\n    inline-size: 9px; block-size: 9px; border-radius: 50%; scale: 0.78;\n    box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-rule-strong);\n    transition: box-shadow var(--ot-moderato) var(--ot-exhale), scale var(--ot-moderato) var(--ot-spiccato);\n  }\n  .ot-progress-units > i[data-now] { box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-ink); }\n  .ot-progress-units > i[data-done] { box-shadow: inset 0 0 0 5px var(--ot-ink); scale: 1; }\n  /* Parentheses. From 20(25): the words stand in the middle of the measure between a pair of thin parentheses, and the work\n     still to do is the silence inside them. Two rests share the free space by (1 − p), so as the work goes the parentheses\n     close on the words from both ends, and done, they hold them and the full stop lands inside. Flex, so right to left\n     simply mirrors, and the glyphs mirror themselves. */\n  .ot-progress[data-variant=\"parentheses\"] { display: flex; justify-content: center; align-items: baseline; column-gap: 0; font-size: var(--ot-mf); line-height: var(--ot-mf-lh); letter-spacing: var(--ot-mf-tr); }\n  .ot-progress[data-variant=\"parentheses\"] > .ot-progress-label { flex: 0 1 auto; min-inline-size: 0; padding-inline: 0.14em; font-weight: 300; color: var(--ot-ink); text-align: center; text-wrap: balance; }\n  .ot-progress-paren { flex: none; font-weight: 100; font-stretch: 88%; line-height: 1; color: var(--ot-ink); user-select: none; }\n  .ot-progress-rest { flex: calc((1 - var(--ot-progress-p)) / 2) 1 0; min-inline-size: 0; }\n  /* Unknown, the parentheses listen: they draw a little way in and back, slowly, only while it runs. */\n  .ot-progress[data-variant=\"parentheses\"][data-state=\"indeterminate\"] { animation: ot-progress-listen 2800ms var(--ot-breath) infinite alternate both; }\n  @keyframes ot-progress-listen { from { --ot-progress-p: 0; } to { --ot-progress-p: 0.34; } }\n\n  /* Tally. How a hand counts: one stroke per item, and every fifth crosses the four before it, so the count reads in fives\n     at a glance. To come, each stroke is a pencil hairline; as its item lands, ink draws down it (the fifth, across it), and\n     the next one waits in ink. Done, a full stop lands after the last gate. */\n  .ot-progress-tally { grid-row: 2; grid-column: 1 / -1; display: flex; flex-wrap: wrap; align-items: end; gap: var(--ot-space-4) 0.66em; padding-block-start: var(--ot-space-2); font-size: var(--ot-mf); }\n  .ot-progress-tally > b { position: relative; display: flex; gap: 0.2em; block-size: 0.78em; }\n  .ot-progress-tally i {\n    position: relative; inline-size: var(--ot-stroke);\n    background: linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) 50% 0 / var(--ot-hairline) 100% no-repeat;\n  }\n  .ot-progress-tally i::after { content: \"\"; position: absolute; inset: 0; background: var(--ot-ink); clip-path: inset(0 0 100% 0); transition: clip-path var(--ot-moderato) var(--ot-exhale); }\n  .ot-progress-tally i[data-now] { background-image: linear-gradient(var(--ot-ink), var(--ot-ink)); }\n  .ot-progress-tally i[data-done]::after { clip-path: inset(0); }\n  .ot-progress-tally > b > i:nth-child(5) { position: absolute; inset-inline: -0.2em; inset-block-start: calc(50% - var(--ot-stroke) / 2); inline-size: auto; block-size: var(--ot-stroke); rotate: -30deg; background-position: 0 50%; background-size: 100% var(--ot-hairline); }\n  .ot-progress-tally > b > i:nth-child(5)::after { clip-path: inset(0 100% 0 0); }\n  .ot-progress-tally > b > i:nth-child(5)[data-done]::after { clip-path: inset(0); }\n  .ot-progress-tally:is([dir=\"rtl\"], [dir=\"rtl\"] *) > b { scale: -1 1; }\n  .ot-progress-tally::after { content: \"\"; inline-size: var(--ot-dot); block-size: var(--ot-dot); border-radius: 50%; background: var(--ot-ink); scale: 0; transition: scale var(--ot-moderato) var(--ot-spiccato); }\n  .ot-progress[data-state=\"complete\"] .ot-progress-tally::after { scale: 1; transition-delay: var(--ot-moderato); }\n  .ot-progress[data-state=\"indeterminate\"] .ot-progress-tally i { animation: ot-progress-tick 1600ms var(--ot-breath) infinite; animation-delay: calc(var(--i) / var(--n) * 960ms); }\n  @keyframes ot-progress-tick { 0%, 100% { opacity: 0.5; } 25% { opacity: 1; } }\n\n  .ot-progress:is([data-variant=\"sentence\"], [data-variant=\"count\"], [data-variant=\"parentheses\"], [data-variant=\"tally\"]) > progress { position: absolute; inline-size: 1px; block-size: 1px; overflow: hidden; clip-path: inset(50%); }\n\n  /* Indeterminate: nobody knows how far yet, so the number stands back and a stroke reads along, again and again, only\n     while it runs: a segment passes along the line, a band of ink passes through the words, the rings ink in turn. */\n  .ot-progress[data-state=\"indeterminate\"] :is(.ot-progress-value, .ot-progress-count) { visibility: hidden; }\n  .ot-progress[data-state=\"indeterminate\"] > progress {\n    background:\n      linear-gradient(var(--ot-ink), var(--ot-ink)) -60% 50% / 30% var(--ot-stroke) no-repeat,\n      linear-gradient(var(--ot-rule), var(--ot-rule)) 0 50% / 100% var(--ot-hairline) no-repeat;\n    animation: ot-progress-read 1600ms var(--ot-breath) infinite;\n  }\n  .ot-progress[data-variant=\"sentence\"][data-state=\"indeterminate\"] > .ot-progress-label > span {\n    background: linear-gradient(to right, var(--ot-rule-strong) 40%, var(--ot-ink) 50%, var(--ot-rule-strong) 60%) 100% 0 / 300% 100% no-repeat;\n    -webkit-background-clip: text; background-clip: text;\n    animation: ot-progress-read-words 2400ms var(--ot-breath) infinite;\n  }\n  .ot-progress[data-state=\"indeterminate\"] .ot-progress-units > i { animation: ot-progress-wave 1600ms var(--ot-breath) infinite; animation-delay: calc(var(--i) / var(--n) * 960ms); }\n  .ot-progress[data-state=\"indeterminate\"]:is([dir=\"rtl\"], [dir=\"rtl\"] *) :is(progress, .ot-progress-label > span) { animation-direction: reverse; }\n  @keyframes ot-progress-read { to { background-position: 160% 50%, 0 50%; } }\n  @keyframes ot-progress-read-words { to { background-position: 0 0; } }\n  @keyframes ot-progress-wave { 25% { box-shadow: inset 0 0 0 var(--ot-hairline) var(--ot-ink); scale: 1; } }\n\n  /* Right to left the line fills from the right; the rest follows the grid and the flex row. */\n  .ot-progress:is([dir=\"rtl\"], [dir=\"rtl\"] *) > progress { background-position: 100% 50%, 0 50%; }\n\n  @media (prefers-reduced-motion: reduce) {\n    .ot-progress[data-state=\"indeterminate\"] > progress { animation: none; background: linear-gradient(var(--ot-rule-strong), var(--ot-rule-strong)) 0 50% / 100% var(--ot-hairline) no-repeat; }\n    .ot-progress[data-variant=\"sentence\"][data-state=\"indeterminate\"] > .ot-progress-label > span { animation: none; background: none; -webkit-text-fill-color: var(--ot-pencil); }\n    .ot-progress[data-state=\"indeterminate\"] :is(.ot-progress-units > i, .ot-progress-tally i) { animation: none; }\n    .ot-progress[data-variant=\"parentheses\"][data-state=\"indeterminate\"] { animation: none; }\n  }\n  @media (forced-colors: active) {\n    .ot-progress { forced-color-adjust: none; --ot-ink: CanvasText; --ot-graphite: CanvasText; --ot-pencil: GrayText; --ot-rule: GrayText; --ot-rule-strong: GrayText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/progress.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/progress.css\"": {}
  },
  "meta": {
    "movement": "VII",
    "underneath": "native",
    "contract": "ot-progress"
  },
  "type": "registry:ui"
}
