{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "type": "registry:ui",
  "title": "Spinner",
  "description": "Arc, dots and pixel-grid spinners drawn on the icon grid, so they swap for icons.",
  "categories": [
    "async"
  ],
  "dependencies": [],
  "registryDependencies": [
    "https://www.stealth.pm/r/tokens.json",
    "https://www.stealth.pm/r/cn.json"
  ],
  "files": [
    {
      "path": "components/ui/spinner.tsx",
      "content": "import { cn } from \"@/lib/cn\";\n\n// Three spinners drawn on the icon grid (16 units, 1.5 stroke), so a spinner can\n// stand in for an icon at the same size without anything around it moving.\n// Everything is CSS keyframes: a spinner must keep turning while the main thread\n// is busy, which is exactly when it is on screen.\nconst CSS = `\n@keyframes stealth-spinner-in { from { opacity: 0; transform: scale(0.8); } }\n@keyframes stealth-spinner-turn { to { transform: rotate(360deg); } }\n@keyframes stealth-spinner-arc {\n  0% { stroke-dasharray: 8 92; stroke-dashoffset: 0; }\n  50% { stroke-dasharray: 46 54; stroke-dashoffset: -22; }\n  100% { stroke-dasharray: 8 92; stroke-dashoffset: -100; }\n}\n@keyframes stealth-spinner-dot {\n  0%, 64%, 100% { transform: translateY(0); opacity: 0.32; }\n  30% { transform: translateY(-2.5px); opacity: 1; }\n}\n@keyframes stealth-spinner-cell {\n  0%, 70%, 100% { transform: scale(0.78); opacity: 0.18; }\n  30% { transform: scale(1); opacity: 1; }\n}\n@keyframes stealth-spinner-breathe { 0%, 100% { opacity: 1; } 50% { opacity: 0.35; } }\n\n.stealth-spinner { animation: stealth-spinner-in 180ms cubic-bezier(0.16, 1, 0.3, 1) both; animation-delay: var(--spinner-delay, 0ms); }\n.stealth-spinner [data-part=\"turn\"] { animation: stealth-spinner-turn 1.1s linear infinite; }\n.stealth-spinner [data-part=\"arc\"] { animation: stealth-spinner-arc 1.5s cubic-bezier(0.65, 0, 0.35, 1) infinite; }\n.stealth-spinner [data-part=\"dot\"] { transform-box: fill-box; animation: stealth-spinner-dot 1.1s cubic-bezier(0.45, 0, 0.55, 1) infinite; }\n.stealth-spinner [data-part=\"cell\"] { transform-box: fill-box; transform-origin: center; animation: stealth-spinner-cell 1.2s cubic-bezier(0.45, 0, 0.55, 1) infinite; }\n\n/* Reduced motion: nothing travels or turns. The whole mark breathes slowly instead,\n   so \"still working\" is still said. Specific enough to outrank a global kill switch. */\n@media (prefers-reduced-motion: reduce) {\n  .stealth-spinner [data-part] { animation: none !important; }\n  .stealth-spinner [data-part=\"arc\"] { stroke-dasharray: 30 70; }\n  .stealth-spinner [data-part=\"dot\"] { opacity: 0.8; }\n  .stealth-spinner [data-part=\"cell\"] { opacity: 0.55; transform: scale(0.9); }\n  .stealth-spinner > svg { animation: stealth-spinner-breathe 2.4s ease-in-out infinite !important; }\n}\n`;\n\nconst SIZES = { sm: 14, md: 16, lg: 20 } as const;\n\nexport type SpinnerProps = Omit<React.ComponentProps<\"span\">, \"children\"> & {\n  variant?: \"ring\" | \"dots\" | \"grid\";\n  /** 14, 16 or 20px, the same steps as icons. A number sets pixels directly. */\n  size?: keyof typeof SIZES | number;\n  /** Read by screen readers. Pass an empty string when the parent already says it is busy. */\n  label?: string;\n  /** Milliseconds to wait before fading in, so fast work never flashes a spinner. */\n  delay?: number;\n};\n\nexport function Spinner({ variant = \"ring\", size = \"md\", label = \"Loading\", delay = 0, className, style, ...rest }: SpinnerProps) {\n  const px = typeof size === \"number\" ? size : SIZES[size];\n  const decorative = !label;\n\n  return (\n    <span\n      role={decorative ? undefined : \"status\"}\n      aria-hidden={decorative || undefined}\n      data-variant={variant}\n      data-size={typeof size === \"number\" ? undefined : size}\n      className={cn(\"stealth-spinner relative inline-grid shrink-0 place-items-center align-middle\", className)}\n      style={{ width: px, height: px, ...(delay ? { \"--spinner-delay\": `${delay}ms` } : null), ...style } as React.CSSProperties}\n      {...rest}\n    >\n      <style href=\"stealth-spinner\" precedence=\"default\">\n        {CSS}\n      </style>\n      <svg width={px} height={px} viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden focusable=\"false\" className=\"overflow-visible\">\n        {variant === \"ring\" && <Ring />}\n        {variant === \"dots\" && <Dots />}\n        {variant === \"grid\" && <Grid />}\n      </svg>\n      {!decorative && <span className=\"sr-only\">{label}</span>}\n    </span>\n  );\n}\n\nfunction Ring() {\n  return (\n    <>\n      <circle cx=\"8\" cy=\"8\" r=\"6\" stroke=\"currentColor\" strokeWidth=\"1.5\" opacity=\"0.18\" />\n      {/* The turn and the breathing arc run on different periods, so the loop never looks mechanical. */}\n      <g data-part=\"turn\" style={{ transformOrigin: \"8px 8px\" }}>\n        <circle\n          data-part=\"arc\"\n          cx=\"8\"\n          cy=\"8\"\n          r=\"6\"\n          pathLength={100}\n          stroke=\"currentColor\"\n          strokeWidth=\"1.5\"\n          strokeLinecap=\"round\"\n          strokeDasharray=\"8 92\"\n          transform=\"rotate(-90 8 8)\"\n        />\n      </g>\n    </>\n  );\n}\n\nfunction Dots() {\n  return (\n    <>\n      {[3, 8, 13].map((cx, i) => (\n        <circle key={cx} data-part=\"dot\" cx={cx} cy=\"8\" r=\"1.6\" fill=\"currentColor\" style={{ animationDelay: `${i * 0.14}s` }} />\n      ))}\n    </>\n  );\n}\n\nfunction Grid() {\n  const at = [1.4, 6.2, 11];\n  return (\n    <>\n      {at.flatMap((y, row) =>\n        at.map((x, col) => (\n          // A diagonal wave: each cell lights one step after the cell above-left of it.\n          <rect key={`${row}${col}`} data-part=\"cell\" x={x} y={y} width=\"3.6\" height=\"3.6\" rx=\"0.9\" fill=\"currentColor\" style={{ animationDelay: `${(row + col) * 0.1}s` }} />\n        )),\n      )}\n    </>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "docs": "https://www.stealth.pm/ui/spinner"
}
