{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "type": "registry:ui",
  "title": "Spinner",
  "description": "Orbiting light drawn in three layers (a crisp arc, a tight glow and a wide bloom) in five sizes and any text colour, announced as loading.",
  "dependencies": [
    "react-aria-components@^1.17.0"
  ],
  "registryDependencies": [
    "cx"
  ],
  "files": [
    {
      "path": "components/base/spinner/spinner.tsx",
      "content": "\"use client\";\n\nimport type { CSSProperties } from \"react\";\nimport { ProgressBar as AriaProgressBar } from \"react-aria-components\";\nimport { cx } from \"@/utils/cx\";\n\n/**\n * Spinner — an orbiting light for work of unknown length.\n *\n *   <Spinner />                                  // 20px, accent\n *   <Spinner size=\"sm\" className=\"text-white\" /> // inside a primary button\n *\n * The light is drawn the way Composer Loader draws its band: the same arc\n * at three scales, a crisp line with a rounded head, a tight glow and a wide\n * bloom, each on its own blur. The soft layers carry shorter arcs and turn a\n * few degrees behind, so the sharp head always leads and the glow gathers\n * just behind it like the tail of a comet. A faint track completes the\n * circle. Everything is drawn in `currentColor`, so a text colour sets it.\n *\n * Announced as an indeterminate progress bar labelled \"Loading\" (override\n * with `aria-label`). Pass `aria-hidden` where the surrounding control\n * already says it is busy, e.g. a pending button. Reduced motion slows the\n * orbit rather than stopping it: the motion is the message.\n */\n\nexport type SpinnerSize = \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\";\n\n/** Box size and line width in px. */\nconst SIZES: Record<SpinnerSize, { box: number; line: number }> = {\n  xs: { box: 12, line: 1.5 },\n  sm: { box: 16, line: 2 },\n  md: { box: 20, line: 2 },\n  lg: { box: 24, line: 2.5 },\n  xl: { box: 32, line: 3 },\n};\n\n/** One scale of the light: a conic arc masked to a ring, blurred by its wrapper. */\nfunction Arc({\n  box,\n  line,\n  width,\n  blur,\n  opacity,\n  arc,\n  lag,\n}: {\n  box: number;\n  line: number;\n  /** Ring thickness, px. */\n  width: number;\n  blur: number;\n  opacity: number;\n  /** Arc length in degrees; the head sits at 12 o'clock before the turn. */\n  arc: number;\n  /** Degrees this layer trails the crisp head. */\n  lag: number;\n}) {\n  const center = box / 2 - line / 2;\n  const inner = center - width / 2;\n  const outer = center + width / 2;\n  // The wrapper is grown to hold the ring plus its blur, so nothing clips.\n  const bleed = Math.max(0, outer + blur * 2 - box / 2);\n  const ring = `radial-gradient(circle, transparent ${inner}px, #000 ${inner + 0.5}px, #000 ${outer - 0.5}px, transparent ${outer}px)`;\n  const style: CSSProperties = {\n    inset: -bleed,\n    filter: blur > 0 ? `blur(${blur}px)` : undefined,\n    opacity,\n    rotate: `${-lag}deg`,\n  };\n  return (\n    <span aria-hidden className=\"absolute\" style={style}>\n      <span\n        className=\"absolute inset-0\"\n        style={{\n          background: `conic-gradient(transparent ${360 - arc}deg, currentColor 360deg)`,\n          maskImage: ring,\n          WebkitMaskImage: ring,\n        }}\n      />\n    </span>\n  );\n}\n\nexport interface SpinnerProps {\n  /** 12, 16, 20 (default), 24, or 32px. */\n  size?: SpinnerSize;\n  /** Draw the faint full circle under the light. Default true. */\n  track?: boolean;\n  /** Accessible name. Default \"Loading\". */\n  \"aria-label\"?: string;\n  /** Hide it from assistive technology when a parent already announces the wait. */\n  \"aria-hidden\"?: boolean;\n  /** Colour comes from the text colour, e.g. `text-white`. Default accent. */\n  className?: string;\n}\n\nexport function SpinnerGlyph({ size = \"md\", track = true, className }: Omit<SpinnerProps, \"aria-label\" | \"aria-hidden\">) {\n  const { box, line } = SIZES[size];\n  return (\n    <span\n      aria-hidden\n      className={cx(\"relative inline-flex shrink-0 text-accent-500\", className)}\n      style={{ width: box, height: box }}\n    >\n      {track ? (\n        <span className=\"absolute inset-0 rounded-full border-current opacity-15\" style={{ borderWidth: line }} />\n      ) : null}\n      <span className=\"absolute inset-0 animate-spin [animation-duration:900ms] motion-reduce:[animation-duration:2400ms]\">\n        <Arc box={box} line={line} width={line * 4} blur={line * 1.6} opacity={0.32} arc={110} lag={22} />\n        <Arc box={box} line={line} width={line * 2} blur={line * 0.6} opacity={0.6} arc={200} lag={9} />\n        <Arc box={box} line={line} width={line} blur={0} opacity={1} arc={290} lag={0} />\n        {/* The crisp line's rounded head, at the arc's leading end. */}\n        <span\n          className=\"absolute left-1/2 top-0 rounded-full bg-current\"\n          style={{ width: line, height: line, translate: \"-50% 0\" }}\n        />\n      </span>\n    </span>\n  );\n}\n\nexport function Spinner({ size = \"md\", track = true, \"aria-label\": ariaLabel = \"Loading\", \"aria-hidden\": ariaHidden, className }: SpinnerProps) {\n  if (ariaHidden) return <SpinnerGlyph size={size} track={track} className={className} />;\n  return (\n    <AriaProgressBar isIndeterminate aria-label={ariaLabel} className=\"inline-flex shrink-0\">\n      <SpinnerGlyph size={size} track={track} className={className} />\n    </AriaProgressBar>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/base/spinner/spinner.tsx"
    }
  ],
  "meta": {
    "docsUrl": "https://www.boardui.com/components/spinner",
    "example": "import { Spinner } from \"@/components/base/spinner/spinner\";\n\nexport function Example() {\n  return (\n    <div className=\"flex items-end gap-6\">\n      <Spinner size=\"xs\" />\n      <Spinner size=\"sm\" />\n      <Spinner size=\"md\" />\n      <Spinner size=\"lg\" />\n      <Spinner size=\"xl\" />\n    </div>\n  );\n}",
    "snippets": [
      {
        "label": "color",
        "code": "<Spinner size=\"lg\" />                                // accent\n<Spinner size=\"lg\" className=\"text-text-secondary\" />  // muted\n<Spinner size=\"lg\" className=\"text-white\" />           // on a primary surface\n<Spinner size=\"lg\" track={false} />                    // the light alone"
      },
      {
        "label": "button",
        "code": "import { Button } from \"@/components/base/buttons/button\";\nimport { SpinnerGlyph } from \"@/components/base/spinner/spinner\";\n\n// Button's icon slot passes a size class; the glyph keeps its own box.\nfunction ButtonSpinner({ className }: { className?: string }) {\n  return <SpinnerGlyph size=\"sm\" className={cx(className, \"text-current\")} />;\n}\n\n// Busy, not disabled: the button keeps its colour, so the light stays legible.\n<Button leadingIcon={pending ? ButtonSpinner : undefined} aria-busy={pending} onClick={() => !pending && publish()}>\n  {pending ? \"Publishing\" : \"Publish changes\"}\n</Button>"
      }
    ]
  }
}
