{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "title": "Spinner",
  "description": "The wait, written the way a score writes it: periods, a round, a metronome, a fermata, a breath, a rising chord, or the word itself being inked.",
  "registryDependencies": [
    "https://thedirectors.agency/ui/r/0nlytype.json"
  ],
  "files": [
    {
      "path": "registry/0nlytype/ui/spinner.tsx",
      "content": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ntype SpinnerProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  /**\n   * How the wait is written. dots: three periods breathing in turn. round: three voices chasing\n   * round a ring, as the voices of a round do. metronome: a pendulum keeping a slow beat. fermata:\n   * the hold, drawn, held and lifted. breath: a dot that opens into a ring and closes again.\n   * arpeggio: four notes rising, struck in turn and let ring. word: the label, inked letter by letter.\n   */\n  variant?: \"dots\" | \"round\" | \"metronome\" | \"fermata\" | \"breath\" | \"arpeggio\" | \"word\"\n  /** m takes the size of the text around it; l stands alone, at the section-heading size. */\n  size?: \"m\" | \"l\"\n  /**\n   * Leave it out when the words beside it say what's happening (\"Saving\"), as a busy button's do:\n   * the spinner then stays silent. Given, it's a polite status a screen reader hears once.\n   * `word` writes it (default \"Loading\") and is always read.\n   */\n  label?: string\n}\n\nconst marks = { dots: 3, round: 3, metronome: 1, fermata: 1, breath: 1, arpeggio: 4 }\n\n/** Something the person started is under way. Sized in em, so it sits in a line of text at that text's size. */\nfunction Spinner({ variant = \"dots\", size = \"m\", label, className, style, ...props }: SpinnerProps) {\n  const word = variant === \"word\" ? (label ?? \"Loading\") : undefined\n  const said = word ?? label\n  return (\n    <span\n      data-slot=\"spinner\"\n      data-variant={variant}\n      data-size={size === \"m\" ? undefined : size}\n      className={cn(\"ot-dots\", className)}\n      style={word ? ({ \"--n\": Array.from(word).length, ...style } as React.CSSProperties) : style}\n      {...(said ? { role: \"status\", \"aria-live\": \"polite\" as const } : { \"aria-hidden\": true })}\n      {...props}\n    >\n      <span className=\"ot-dots-art\" aria-hidden=\"true\">\n        {word\n          ? Array.from(word).map((c, i) => (\n              <span key={i} style={{ \"--i\": i } as React.CSSProperties}>\n                {c}\n              </span>\n            ))\n          : Array.from({ length: marks[variant as keyof typeof marks] }, (_, i) => <i key={i} />)}\n      </span>\n      {said ? <span className=\"ot-sr\">{said}</span> : null}\n    </span>\n  )\n}\n\nexport { Spinner, type SpinnerProps }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/0nlytype/styles/spinner.css",
      "content": "@layer components {\n  /* ── ot-dots ── the wait, written the way a score or a page writes it: periods, a round, a pendulum,\n     a fermata, a breath, a rising chord or a word being inked. The one thing that moves by itself, and only\n     while something the person started is running, so every loop is slow and eased like breathing.\n     Everything is in em: it sits in a line of text at that text's size, and the art stands on the baseline\n     (a box as tall as a capital) so it reads as one more glyph. Strokes stay hairlines at any size; dots grow. */\n  @property --ot-dots-d { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n  @property --ot-dots-w { syntax: \"<length>\"; inherits: false; initial-value: 0px; }\n  @property --ot-dots-a { syntax: \"<angle>\"; inherits: false; initial-value: 0deg; }\n  @property --ot-dots-b { syntax: \"<angle>\"; inherits: false; initial-value: 0deg; }\n  .ot-dots { position: relative; display: inline-block; white-space: nowrap; }\n  .ot-dots[data-size=\"l\"] { font-size: var(--ot-f); line-height: var(--ot-f-lh); }\n  .ot-dots-art { position: relative; display: inline-block; vertical-align: baseline; inline-size: 1em; block-size: 0.7em; }\n  .ot-dots-art > i { position: absolute; font-style: normal; }\n\n  /* Dots: three periods on the baseline, each lifting as it brightens, in turn. Still, they trail off. */\n  .ot-dots[data-variant=\"dots\"] .ot-dots-art { inline-size: 0.94em; }\n  .ot-dots[data-variant=\"dots\"] i { inset-block-end: 0; inline-size: 0.18em; block-size: 0.18em; border-radius: 50%; background: currentColor; animation: ot-dots-lift calc(var(--ot-andante) * 2.5) var(--ot-breath) infinite both; }\n  .ot-dots[data-variant=\"dots\"] i:nth-child(1) { inset-inline-start: 0.04em; }\n  .ot-dots[data-variant=\"dots\"] i:nth-child(2) { inset-inline-start: 0.38em; opacity: 0.55; animation-delay: var(--ot-allegro); }\n  .ot-dots[data-variant=\"dots\"] i:nth-child(3) { inset-inline-start: 0.72em; opacity: 0.25; animation-delay: calc(var(--ot-allegro) * 2); }\n  @keyframes ot-dots-lift { 0%, 100% { opacity: 0.22; translate: 0 0; } 40% { opacity: 1; translate: 0 -0.2em; } }\n\n  /* Round: three voices on one hairline ring, each entering a beat after the last, as a round's voices do.\n     The easing slows them at the top and the foot, so they gather into one dot there and spread out between. */\n  .ot-dots[data-variant=\"round\"] .ot-dots-art { inline-size: 0.7em; }\n  .ot-dots[data-variant=\"round\"] .ot-dots-art::before { content: \"\"; position: absolute; inset: 0; border: var(--ot-hairline) solid; border-radius: 50%; opacity: 0.28; }\n  .ot-dots[data-variant=\"round\"] i { inset: 0; rotate: 60deg; animation: ot-dots-round calc(var(--ot-adagio) * 2) var(--ot-breath) infinite both; }\n  .ot-dots[data-variant=\"round\"] i::before { content: \"\"; position: absolute; inset-block-start: -0.08em; inset-inline-start: calc(50% - 0.08em); inline-size: 0.16em; block-size: 0.16em; border-radius: 50%; background: currentColor; }\n  .ot-dots[data-variant=\"round\"] i:nth-child(2) { rotate: 30deg; opacity: 0.6; animation-delay: var(--ot-allegro); }\n  .ot-dots[data-variant=\"round\"] i:nth-child(3) { rotate: 0deg; opacity: 0.3; animation-delay: calc(var(--ot-allegro) * 2); }\n  @keyframes ot-dots-round { 0% { rotate: 0deg; } 50% { rotate: 180deg; } 100% { rotate: 360deg; } }\n\n  /* Metronome: a stroke rod standing on a hairline foot, a dot for its weight, swinging a slow beat. */\n  .ot-dots[data-variant=\"metronome\"] .ot-dots-art { inline-size: 0.76em; }\n  .ot-dots[data-variant=\"metronome\"] .ot-dots-art::before { content: \"\"; position: absolute; inset-inline: 0.1em; inset-block-end: 0; block-size: var(--ot-hairline); background: currentColor; opacity: 0.4; }\n  .ot-dots[data-variant=\"metronome\"] i { inset-block-end: 0; inset-inline-start: calc(50% - var(--ot-stroke) / 2); inline-size: var(--ot-stroke); block-size: 0.72em; background: currentColor; transform-origin: 50% 100%; rotate: -22deg; animation: ot-dots-swing calc(var(--ot-moderato) * 3) var(--ot-breath) infinite alternate both; }\n  .ot-dots[data-variant=\"metronome\"] i::before { content: \"\"; position: absolute; inset-block-start: 0.2em; inset-inline-start: 50%; inline-size: 0.2em; block-size: 0.2em; margin-inline-start: -0.1em; border-radius: 50%; background: currentColor; }\n  @keyframes ot-dots-swing { from { rotate: -22deg; } to { rotate: 22deg; } }\n\n  /* Fermata: the hold. The arc (the top half of a stroke ellipse, shown through a conic mask whose edges are\n     registered angles) draws in the reading direction, the dot lands under it, the pause is held, then the arc\n     lifts away the way it was drawn and the dot fades. Still, it's simply the sign: wait. */\n  .ot-dots[data-variant=\"fermata\"] .ot-dots-art { inline-size: 1em; }\n  .ot-dots[data-variant=\"fermata\"] .ot-dots-art:is([dir=\"rtl\"], [dir=\"rtl\"] *) { scale: -1 1; }\n  .ot-dots[data-variant=\"fermata\"] .ot-dots-art::before { content: \"\"; position: absolute; inset-inline: 0; inset-block-start: 0.02em; block-size: 1.2em; border: var(--ot-stroke) solid; border-radius: 50%; mask: conic-gradient(from 270deg, transparent var(--ot-dots-a), #000 0 var(--ot-dots-b), transparent 0); --ot-dots-a: 0deg; --ot-dots-b: 180deg; animation: ot-dots-arc calc(var(--ot-andante) * 5) infinite both; }\n  .ot-dots[data-variant=\"fermata\"] i { inset-block-end: 0; inset-inline-start: calc(50% - 0.085em); inline-size: 0.17em; block-size: 0.17em; border-radius: 50%; background: currentColor; animation: ot-dots-hold calc(var(--ot-andante) * 5) infinite both; }\n  @keyframes ot-dots-arc { 0% { --ot-dots-a: 0deg; --ot-dots-b: 0deg; animation-timing-function: var(--ot-exhale); } 30% { --ot-dots-a: 0deg; --ot-dots-b: 180deg; } 78% { --ot-dots-a: 0deg; --ot-dots-b: 180deg; animation-timing-function: var(--ot-breath); } 100% { --ot-dots-a: 180deg; --ot-dots-b: 180deg; } }\n  @keyframes ot-dots-hold { 0%, 22% { scale: 0; opacity: 1; animation-timing-function: var(--ot-spiccato); } 36% { scale: 1; } 80% { scale: 1; opacity: 1; animation-timing-function: var(--ot-breath); } 100% { scale: 1; opacity: 0; } }\n\n  /* Breath: a dot breathes in and opens into a hairline ring, holds, and breathes out, closing into a dot again.\n     The size and the ink are two eased loops: the ink drains early and returns late, so it's a ring that breathes\n     and only a dot at the bottom of the breath, as the switch's full stop fills from its rim. Still, a small ring. */\n  .ot-dots[data-variant=\"breath\"] .ot-dots-art { inline-size: 0.7em; }\n  .ot-dots[data-variant=\"breath\"] i { --ot-dots-d: 0.44em; --ot-dots-w: var(--ot-hairline); inset: 0; margin: auto; inline-size: var(--ot-dots-d); block-size: var(--ot-dots-d); box-sizing: border-box; border: var(--ot-dots-w) solid; border-radius: 50%; animation: ot-dots-breath calc(var(--ot-adagio) * 2) var(--ot-breath) infinite both, ot-dots-drain calc(var(--ot-adagio) * 2) var(--ot-breath) infinite both; }\n  @keyframes ot-dots-breath { 0%, 100% { --ot-dots-d: 0.22em; } 42%, 58% { --ot-dots-d: 0.7em; } }\n  @keyframes ot-dots-drain { 0%, 100% { --ot-dots-w: 0.11em; } 20%, 80% { --ot-dots-w: var(--ot-hairline); } }\n\n  /* Arpeggio: four notes of a chord rising (a third, a third, a fourth), struck in turn, each landing with\n     a small rebound, and let ring; the first struck fades first. Still, the chord rises in strength. */\n  .ot-dots[data-variant=\"arpeggio\"] .ot-dots-art { inline-size: 1em; }\n  .ot-dots[data-variant=\"arpeggio\"] i { inline-size: 0.16em; block-size: 0.16em; border-radius: 50%; background: currentColor; animation: ot-dots-strike calc(var(--ot-andante) * 4) infinite both; }\n  .ot-dots[data-variant=\"arpeggio\"] i:nth-child(1) { inset-inline-start: 0; inset-block-end: 0; opacity: 0.4; }\n  .ot-dots[data-variant=\"arpeggio\"] i:nth-child(2) { inset-inline-start: 0.28em; inset-block-end: 0.16em; opacity: 0.6; animation-delay: calc(var(--ot-arpeggio) * 4); }\n  .ot-dots[data-variant=\"arpeggio\"] i:nth-child(3) { inset-inline-start: 0.56em; inset-block-end: 0.32em; opacity: 0.8; animation-delay: calc(var(--ot-arpeggio) * 8); }\n  .ot-dots[data-variant=\"arpeggio\"] i:nth-child(4) { inset-inline-start: 0.84em; inset-block-end: 0.54em; animation-delay: calc(var(--ot-arpeggio) * 12); }\n  @keyframes ot-dots-strike { 0% { opacity: 0.2; scale: 0.6; animation-timing-function: var(--ot-spiccato); } 12% { opacity: 1; scale: 1; } 50% { opacity: 1; scale: 1; animation-timing-function: var(--ot-breath); } 85%, 100% { opacity: 0.2; scale: 0.6; } }\n\n  /* Word: the label itself, in the voice, pencilled; the ink runs through it letter by letter, stays a\n     moment, and leaves in the same order. Still, the first letters are inked: it's being written. */\n  .ot-dots[data-variant=\"word\"] .ot-dots-art { inline-size: auto; block-size: auto; }\n  .ot-dots[data-variant=\"word\"] .ot-dots-art > span { --rest: var(--ot-pencil); --inked: var(--ot-ink); color: var(--rest); animation: ot-dots-ink calc(var(--n, 7) * var(--ot-arpeggio) * 2.5 + var(--ot-adagio)) var(--ot-breath) calc(var(--i) * var(--ot-arpeggio) * 2.5) infinite both; }\n  .ot-dots[data-variant=\"word\"] .ot-dots-art > span:nth-child(-n + 3) { color: var(--inked); }\n  @keyframes ot-dots-ink { 0% { color: var(--rest); } 14%, 55% { color: var(--inked); } 85%, 100% { color: var(--rest); } }\n\n  /* Reduced motion: nothing travels. Every loop stops on the frame above that still reads as unfinished, and\n     the whole mark breathes in ink instead, dimming and returning, so it still reads as busy. The tempo tokens\n     collapse to 1ms here, so this one breath is written out: two adagios. data-force=\"reduced\" pins it for the docs. */\n  @media (prefers-reduced-motion: reduce) {\n    .ot-dots[data-variant] .ot-dots-art::before, .ot-dots[data-variant] .ot-dots-art :is(i, span), .ot-dots[data-variant] .ot-dots-art i::before { animation: none; }\n    .ot-dots-art { animation: ot-dots-dim 2800ms var(--ot-breath) infinite alternate both; }\n  }\n  .ot-dots[data-variant][data-force~=\"reduced\"] .ot-dots-art::before, .ot-dots[data-variant][data-force~=\"reduced\"] .ot-dots-art :is(i, span), .ot-dots[data-variant][data-force~=\"reduced\"] .ot-dots-art i::before { animation: none; }\n  .ot-dots[data-force~=\"reduced\"] .ot-dots-art { animation: ot-dots-dim 2800ms var(--ot-breath) infinite alternate both; }\n  @keyframes ot-dots-dim { from { opacity: 1; } to { opacity: 0.5; } }\n\n  @media (forced-colors: active) {\n    .ot-dots-art { forced-color-adjust: none; } /* keeps the dots drawn, in the forced colour of the words around them */\n    .ot-dots[data-variant=\"word\"] .ot-dots-art > span { --rest: GrayText; --inked: CanvasText; }\n  }\n}\n",
      "type": "registry:file",
      "target": "styles/0nlytype/spinner.css"
    }
  ],
  "css": {
    "@import \"../styles/0nlytype/spinner.css\"": {}
  },
  "meta": {
    "movement": "VII",
    "underneath": "native",
    "contract": "ot-dots"
  },
  "type": "registry:ui"
}
