{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "title": "Spinner",
  "author": "Noppakorn Kaewsalabnil (https://www.pungrumpy.com)",
  "description": "A wireframe atom that assembles itself and then turns in space: three orbits arrive one per beat, then the whole thing turns as one. Three sizes, in the current text color.",
  "dependencies": [
    "class-variance-authority"
  ],
  "registryDependencies": [
    "theme"
  ],
  "files": [
    {
      "path": "components/spinner.tsx",
      "content": "import { cva } from \"class-variance-authority\";\n\nimport { cn } from \"@/lib/utils\";\n\n/** Size and stroke travel together. The stroke is set in px and every shape\n *  draws it with `non-scaling-stroke`, so it is a true hairline at any size;\n *  `lg` matches the 1.5px the drawing was authored at. `stroke-width` is\n *  inherited, so setting it on the box reaches every circle below it. */\nexport const spinnerVariants = cva(\"relative\", {\n  defaultVariants: { size: \"md\" },\n  variants: {\n    size: {\n      lg: \"size-24 [stroke-width:1.5px]\",\n      md: \"size-12 [stroke-width:1.25px]\",\n      sm: \"size-8 [stroke-width:1px]\",\n    },\n  },\n});\n\n/** The orbits are rings in space, not ellipses: each is drawn as a full circle\n *  and tilted out of the screen by 63.187 degrees, whose cosine is 16.6 / 36.8,\n *  the drawing's own ry over its rx. Standing still they project to exactly the\n *  mark this started from; turning, they behave like one rigid object.\n *\n *  Each fades in a beat after the one before it, once. Under reduced motion the\n *  turn goes and a slow fade takes its place on three phases: a spinner still\n *  has to say that something is happening, and opacity is the one channel the\n *  preference leaves open. The classes are written out rather than built from\n *  the numbers because Tailwind reads this file as text. */\nconst ORBITS = [\n  {\n    angle: 30,\n    entrance:\n      \"animate-[spinner-orbit-in_0.3s_ease-out_0.1s_both] motion-reduce:animate-[spinner-orbit-pulse_2.4s_ease-in-out_infinite]\",\n    tilt: \"[transform:rotate(30deg)_rotateX(63.187deg)]\",\n  },\n  {\n    angle: 90,\n    entrance:\n      \"animate-[spinner-orbit-in_0.3s_ease-out_0.2s_both] motion-reduce:animate-[spinner-orbit-pulse_2.4s_ease-in-out_-0.8s_infinite]\",\n    tilt: \"[transform:rotate(90deg)_rotateX(63.187deg)]\",\n  },\n  {\n    angle: -30,\n    entrance:\n      \"animate-[spinner-orbit-in_0.3s_ease-out_0.3s_both] motion-reduce:animate-[spinner-orbit-pulse_2.4s_ease-in-out_-1.6s_infinite]\",\n    tilt: \"[transform:rotate(-30deg)_rotateX(63.187deg)]\",\n  },\n] as const;\n\n/** One rotation, on one element, of a stack that keeps its depth: the orbits\n *  hold their own tilts inside it, so the whole assembly turns the way a solid\n *  thing turns. It is also the only property animated while the spinner is up,\n *  which is what keeps the turn on the compositor and off the main thread. */\nconst ATOM_TURN =\n  \"absolute inset-0 [transform-style:preserve-3d] animate-[spinner-atom-turn_8s_linear_infinite] motion-reduce:animate-none\";\n\ninterface SpinnerProps {\n  readonly className?: string;\n  /** Announced by assistive tech. Pass `null` where visible text already says\n   *  the same thing, which leaves the spinner decorative. */\n  readonly label?: string | null;\n  readonly size?: \"sm\" | \"md\" | \"lg\";\n}\n\nexport const Spinner = ({ className, label = \"Loading\", size = \"md\" }: SpinnerProps) => {\n  const decorative = label === null;\n\n  return (\n    <span\n      aria-hidden={decorative || undefined}\n      className={cn(\"inline-flex shrink-0 items-center justify-center text-gray-700\", className)}\n      role={decorative ? undefined : \"status\"}\n    >\n      <span aria-hidden=\"true\" className={spinnerVariants({ size })}>\n        <svg className=\"absolute inset-0 size-full\" viewBox=\"0 0 76 76\">\n          <circle\n            cx=\"38\"\n            cy=\"38\"\n            fill=\"none\"\n            r=\"36.85\"\n            stroke=\"currentColor\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n          <circle\n            cx=\"38\"\n            cy=\"38\"\n            fill=\"none\"\n            r=\"2.3\"\n            stroke=\"currentColor\"\n            vectorEffect=\"non-scaling-stroke\"\n          />\n        </svg>\n        <span className={ATOM_TURN}>\n          {ORBITS.map((orbit) => (\n            <span\n              className={cn(\"absolute inset-0 opacity-35\", orbit.tilt, orbit.entrance)}\n              key={orbit.angle}\n            >\n              <svg className=\"size-full\" viewBox=\"0 0 76 76\">\n                <circle\n                  cx=\"38\"\n                  cy=\"38\"\n                  fill=\"none\"\n                  r=\"36.8\"\n                  stroke=\"currentColor\"\n                  vectorEffect=\"non-scaling-stroke\"\n                />\n              </svg>\n            </span>\n          ))}\n        </span>\n      </span>\n      {decorative ? null : <span className=\"sr-only\">{label}</span>}\n    </span>\n  );\n};\n",
      "type": "registry:ui"
    }
  ],
  "css": {
    "@keyframes spinner-atom-turn": {
      "to": {
        "transform": "rotateY(360deg)"
      }
    },
    "@keyframes spinner-orbit-in": {
      "from": {
        "opacity": "0"
      },
      "to": {
        "opacity": "0.35"
      }
    },
    "@keyframes spinner-orbit-pulse": {
      "0%, 100%": {
        "opacity": "0.35"
      },
      "50%": {
        "opacity": "0.08"
      }
    }
  },
  "type": "registry:ui"
}