{
  "name": "Spinner",
  "source": "src/components/Spinner.tsx",
  "propsType": "SpinnerProps",
  "description": "Spinner — an indeterminate wait, announced once.\n\nWHY THIS IS NOT A DIV WITH A CSS ANIMATION\n\nThe obvious implementation is inaccessible in a specific and common way: a\nspinning graphic with no text is silence to a screen reader, so the user is\ntold nothing happened. Generated code in this repo's own eval corpus reached\nfor a bare `role=\"status\"` in 48 files, which is the right instinct and only\nhalf the job — the role creates a live region, but an empty one announces\nnothing at all.\n\nSo the default carries a label. It is visually hidden, it lives inside the\nlive region, and it is what makes \"Loading\" reach the user who cannot see the\nring.\n\n`isDecorative` exists because the opposite mistake is just as easy. A spinner\ninside a button whose label already changed to \"Saving…\" would announce\ntwice, and two live regions racing is worse than one. Decorative renders no\nrole at all rather than `aria-hidden` on a live region, which browsers treat\ninconsistently.\n\nMOTION\n\n`prefers-reduced-motion` slows the rotation rather than stopping it. A\nstationary spinner is not a calmer spinner — it is a broken one, and it\nremoves the only signal a sighted user has that the wait is still live.",
  "import": "import { Spinner } from 'ionbase-ui';",
  "status": "stable",
  "summary": "An indeterminate wait, announced once. A ring plus a politely-announced label, sized off the icon ramp.",
  "useWhen": [
    "work is in flight and its duration cannot be measured",
    "a control has started something and must show it is working",
    "a small region is waiting and a skeleton would be more scaffolding than content"
  ],
  "useInstead": [
    {
      "when": "the work has a measurable fraction complete",
      "use": "ProgressBar",
      "why": "a spinner over measurable work discards information the user wants — a spinner and a 90%-complete upload look identical"
    },
    {
      "when": "the work is measurable and has to fit a square or a line of text",
      "use": "ProgressRing",
      "why": "the ring is the spinner's size with the fraction in it; a spinner there discards what the user wants to know"
    },
    {
      "when": "a region is being replaced by content with a known shape",
      "use": "Skeleton",
      "why": "a skeleton holds the layout so it does not jump when content lands; a spinner leaves a hole that then reflows"
    }
  ],
  "variants": {
    "size": {
      "sm": {
        "use": "inside a control, or beside a line of text"
      },
      "md": {
        "use": "the default — a panel or a section"
      },
      "lg": {
        "use": "a whole route or a full-page wait"
      }
    }
  },
  "a11y": {
    "requires": [
      "a `label` naming what is loading — it is announced politely and is the only thing a screen-reader user receives",
      "`isDecorative` when something else already announces the wait, so two live regions do not race"
    ],
    "notes": [
      "the ring is aria-hidden; the label carries the meaning",
      "prefers-reduced-motion slows the rotation rather than stopping it — a stationary spinner is indistinguishable from a hung one"
    ]
  },
  "antiPatterns": [
    {
      "dont": "<div className=\"spinner\" role=\"status\" />",
      "do": "<Spinner label=\"Loading invoices\" />",
      "why": "role=\"status\" creates a live region; an EMPTY live region announces nothing. This exact half-measure appeared in 48 files of this repo's eval corpus"
    },
    {
      "dont": "<Button isDisabled>Saving… <Spinner /></Button>",
      "do": "<Button isDisabled>Saving… <Spinner isDecorative /></Button>",
      "why": "the button label already says it. Two live regions announcing the same wait talk over each other"
    },
    {
      "dont": "a Spinner for an upload that reports bytes transferred",
      "do": "ProgressBar with value",
      "why": "throwing away a known percentage makes a finite wait feel unbounded"
    }
  ],
  "stylesheet": "src/styles/spinner.css",
  "tokens": [
    "--border-primary-strong",
    "--border-subtle",
    "--border-width-thick",
    "--font-family-sans",
    "--icon-size-lg",
    "--icon-size-md",
    "--icon-size-sm",
    "--ion-duration-cycle",
    "--ion-duration-slow",
    "--ion-ease-linear",
    "--ion-spinner-head",
    "--ion-spinner-size",
    "--ion-spinner-track",
    "--radius-full",
    "--spacing-8",
    "--text-secondary",
    "--type-body-sm",
    "--type-body-sm-line-height"
  ],
  "props": {
    "size": {
      "type": "SpinnerSize | undefined",
      "required": false,
      "origin": "own",
      "description": "Matches the icon size ramp: sm, md, lg.",
      "values": [
        "sm",
        "md",
        "lg"
      ]
    },
    "label": {
      "type": "string | undefined",
      "required": false,
      "origin": "own",
      "description": "What is loading. Announced politely; visible only with `isLabelVisible`."
    },
    "isLabelVisible": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own",
      "description": "Render the label as text beside the ring as well as announcing it."
    },
    "isDecorative": {
      "type": "boolean | undefined",
      "required": false,
      "origin": "own",
      "description": "The spinner conveys nothing on its own — something else already says the\nregion is busy. Renders inert: no role, no live region, hidden from AT."
    }
  },
  "propCounts": {
    "own": 4,
    "aria": 0,
    "dom": 277,
    "other": 0
  }
}
