{
  "name": "spinner",
  "type": "registry:ui",
  "title": "Spinner",
  "description": "Indeterminate loading status for saving, refreshing and bounded asynchronous work.",
  "registryDependencies": [
    "@siteplane/base"
  ],
  "dependencies": [
    "lucide-react@1.18.0"
  ],
  "files": [
    {
      "path": "src/components/ui/spinner.tsx",
      "type": "registry:ui",
      "content": "import { Loader2Icon } from \"lucide-react\";\nimport type React from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type SpinnerVariant =\n  | \"default\"\n  | \"ring\"\n  | \"dots\"\n  | \"bars\"\n  | \"spokes\"\n  | \"pulse\"\n  | \"comet\";\n\ntype SpinnerProps = React.ComponentProps<\"div\"> & {\n  variant?: SpinnerVariant;\n};\n\nexport function Spinner({\n  className,\n  variant = \"default\",\n  ...props\n}: SpinnerProps): React.ReactElement {\n  // Default bleibt der Lucide-Loader (SVG) - so funktionieren Buttons & bestehende\n  // Verwendungen unveraendert. comet/ring/dots/... sind optionale Varianten.\n  if (variant === \"default\") {\n    return (\n      <Loader2Icon\n        aria-label=\"Loading\"\n        className={cn(\"siteplane-spinner-motion animate-spin\", className)}\n        data-slot=\"spinner\"\n        role=\"status\"\n        {...(props as React.ComponentProps<typeof Loader2Icon>)}\n      />\n    );\n  }\n\n  if (variant === \"comet\") {\n    // Material-Style: lineare Rotation + weich (ease-in-out) wachsender/schrumpfender\n    // Bogen - der Schwanz zieht versetzt nach.\n    return (\n      <svg\n        aria-label=\"Loading\"\n        className={cn(\n          \"siteplane-spinner-motion size-4 animate-[siteplane-spinner-rotate_2s_linear_infinite]\",\n          className,\n        )}\n        fill=\"none\"\n        data-slot=\"spinner\"\n        role=\"status\"\n        viewBox=\"0 0 50 50\"\n        {...(props as React.SVGProps<SVGSVGElement>)}\n      >\n        <circle\n          className=\"animate-[siteplane-spinner-dash_1.5s_ease-in-out_infinite]\"\n          cx=\"25\"\n          cy=\"25\"\n          fill=\"none\"\n          r=\"20\"\n          stroke=\"currentColor\"\n          strokeLinecap=\"round\"\n          strokeWidth=\"4\"\n        />\n      </svg>\n    );\n  }\n\n  const shared = {\n    \"aria-label\": \"Loading\",\n    \"data-slot\": \"spinner\",\n    role: \"status\" as const,\n    ...props,\n  };\n\n  if (variant === \"ring\") {\n    return (\n      <div\n        className={cn(\n          \"siteplane-spinner-motion inline-block size-4 animate-spin rounded-full border-2 border-current/25 border-t-current\",\n          className,\n        )}\n        {...shared}\n      />\n    );\n  }\n\n  if (variant === \"pulse\") {\n    return (\n      <div\n        className={cn(\n          \"siteplane-spinner-motion relative inline-grid size-4 place-items-center\",\n          className,\n        )}\n        {...shared}\n      >\n        <span className=\"absolute inline-flex size-full animate-ping rounded-full bg-current opacity-60\" />\n        <span className=\"relative inline-flex size-1/2 rounded-full bg-current\" />\n      </div>\n    );\n  }\n\n  if (variant === \"dots\") {\n    return (\n      <div\n        className={cn(\n          \"siteplane-spinner-motion inline-flex items-center gap-1\",\n          className,\n        )}\n        {...shared}\n      >\n        {[0, 1, 2].map((i) => (\n          <span\n            className=\"size-2 animate-[siteplane-spinner-dot_1.2s_ease-in-out_infinite] rounded-full bg-current\"\n            key={i}\n            style={{ animationDelay: `${i * 0.16}s` }}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  if (variant === \"bars\") {\n    return (\n      <div\n        className={cn(\n          \"siteplane-spinner-motion inline-flex h-4 items-center gap-[3px]\",\n          className,\n        )}\n        {...shared}\n      >\n        {[0, 1, 2, 3].map((i) => (\n          <span\n            className=\"h-full w-[3px] origin-center animate-[siteplane-spinner-bars_1s_ease-in-out_infinite] rounded-full bg-current\"\n            key={i}\n            style={{ animationDelay: `${i * 0.12}s` }}\n          />\n        ))}\n      </div>\n    );\n  }\n\n  // spokes - klassischer iOS/Apple-Spinner: radiale Striche, die nacheinander faden\n  return (\n    <div\n      className={cn(\n        \"siteplane-spinner-motion relative inline-block size-4\",\n        className,\n      )}\n      {...shared}\n    >\n      {Array.from({ length: 8 }, (_, i) => (\n        <span\n          className=\"absolute top-0 left-1/2 h-[42%] w-[2px] -translate-x-1/2 animate-[siteplane-spinner-spoke_0.8s_linear_infinite] rounded-full bg-current [transform-origin:50%_119%]\"\n          key={i}\n          style={{\n            animationDelay: `${(i - 8) * 0.1}s`,\n            rotate: `${i * 45}deg`,\n          }}\n        />\n      ))}\n    </div>\n  );\n}\n"
    }
  ],
  "categories": [
    "feedback"
  ],
  "docs": "# Spinner\n\n> Siteplane UI release `0.1.1`.\n\n## Public Purpose\n\nUse Spinner for an indeterminate operation whose completion percentage is unknown, such as saving, refreshing or loading a bounded panel.\n\n## Import\n\n`import { Spinner } from \"@/components/ui/spinner\";`\n\n`import { Button } from \"@/components/ui/button\";`\n\n## Public API\n\n- `Spinner` accepts standard element props and `variant=\"default\" | \"ring\" | \"dots\" | \"bars\" | \"spokes\" | \"pulse\" | \"comet\"`.\n- Size and color inherit from the surrounding context unless an existing utility explicitly changes them.\n- Button loading should use the Button `loading` contract rather than manually placing a separate Spinner next to the label.\n\n## Public Motion\n\n- Each variant owns its loading animation; do not stack another spin, pulse or duration class on top.\n- Both the provider override and the operating-system reduced-motion preference stop root and child animations through the shared `siteplane-spinner-motion` contract. The static status indicator and its semantics remain visible.\n\n## Public Accessibility\n\n- A standalone Spinner uses `role=\"status\"` and needs a context-specific accessible label when `Loading` is not descriptive enough.\n- Keep nearby visible loading text available to assistive technology.\n- Use Progress when a real percentage or stage is known.\n\n## Public Agent Guidance\n\n- Prefer the default variant unless another documented form better matches the available space.\n- Do not invent a new loader animation in product code.\n- Avoid showing several independent spinners for one operation.\n\n## Public Links\n\n- [Documentation and preview](https://ui.siteplane.io/docs/components/feedback/spinner)\n\n- [Registry source](https://ui.siteplane.io/r/spinner.json)\n",
  "meta": {
    "siteplane:agentContractVersion": 1,
    "siteplane:docs": "https://ui.siteplane.io/docs/components/feedback/spinner",
    "siteplane:releaseVersion": "0.1.1",
    "siteplane:sourceOwned": true
  },
  "devDependencies": []
}
