{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "title": "Spinner",
  "description": "Says work is running when nobody knows how long it will take: a ring, three dots, eight bars or an orbit, with a pulse instead of movement for people who turned motion off.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/miseui/ui/spinner.tsx",
      "content": "import type * as React from \"react\"\nimport { motion, useReducedMotion } from \"motion/react\"\nimport { cn } from \"@/registry/miseui/lib/utils\"\n\ntype SpinnerVariant = \"ring\" | \"dots\" | \"bars\" | \"orbit\"\ntype SpinnerSize = \"sm\" | \"md\" | \"lg\"\n\nconst SIZE_CLASS: Record<SpinnerSize, string> = { sm: \"size-4\", md: \"size-6\", lg: \"size-8\" }\n\nconst DOT_COUNT = 3\nconst DOT_SECONDS = 1.1\nconst DOT_STAGGER_SECONDS = 0.16\nconst BAR_COUNT = 8\nconst BAR_SECONDS = 0.8\nconst BAR_REST_OPACITY = 0.2\nconst PULSE_SECONDS = 2.4\nconst PULSE_OPACITY = [1, 0.4, 1]\n/** Slower than Tailwind's 1 s turn: a calm ring reads as working, a fast one reads as hurrying. */\nconst RING_SPIN_CLASS = \"animate-spin [animation-duration:900ms]\"\nconst ORBIT_SPIN_CLASS = \"animate-spin [animation-duration:1200ms]\"\n\ninterface ShapeProps {\n  moving: boolean\n}\n\nfunction RingShape({ moving }: ShapeProps) {\n  return (\n    <svg data-slot=\"spinner-ring\" viewBox=\"0 0 24 24\" fill=\"none\" className={cn(\"size-full\", moving && RING_SPIN_CLASS)}>\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"2.5\" opacity=\"0.2\" />\n      <circle cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" strokeDasharray=\"16 63\" />\n    </svg>\n  )\n}\n\nfunction DotsShape({ moving }: ShapeProps) {\n  return (\n    <span className=\"flex size-full items-center justify-between\">\n      {Array.from({ length: DOT_COUNT }, (_, index) => (\n        <motion.span\n          key={index}\n          data-slot=\"spinner-dot\"\n          className=\"aspect-square w-1/4 rounded-full bg-current\"\n          initial={{ opacity: moving ? 0.25 : 1 }}\n          animate={moving ? { opacity: [0.25, 1, 0.25] } : undefined}\n          transition={{ duration: DOT_SECONDS, ease: \"easeInOut\", repeat: Infinity, delay: index * DOT_STAGGER_SECONDS }}\n        />\n      ))}\n    </span>\n  )\n}\n\nfunction BarsShape({ moving }: ShapeProps) {\n  return (\n    <span className=\"relative block size-full\">\n      {Array.from({ length: BAR_COUNT }, (_, index) => {\n        const staticOpacity = 1 - (index / BAR_COUNT) * (1 - BAR_REST_OPACITY)\n        return (\n          <span\n            key={index}\n            className=\"absolute inset-0 flex justify-center\"\n            style={{ transform: `rotate(${(index * 360) / BAR_COUNT}deg)` }}\n          >\n            <motion.span\n              data-slot=\"spinner-bar\"\n              className=\"block h-[28%] w-[10%] rounded-full bg-current\"\n              initial={{ opacity: moving ? BAR_REST_OPACITY : staticOpacity }}\n              animate={moving ? { opacity: [1, BAR_REST_OPACITY] } : undefined}\n              transition={{ duration: BAR_SECONDS, ease: \"linear\", repeat: Infinity, delay: (index * BAR_SECONDS) / BAR_COUNT }}\n            />\n          </span>\n        )\n      })}\n    </span>\n  )\n}\n\nfunction OrbitShape({ moving }: ShapeProps) {\n  return (\n    <span className=\"relative block size-full\">\n      <span className=\"absolute inset-[12%] rounded-full border border-current opacity-25\" />\n      <span data-slot=\"spinner-orbit\" className={cn(\"absolute inset-0 flex items-start justify-center\", moving && ORBIT_SPIN_CLASS)}>\n        <span className=\"mt-[2%] aspect-square w-1/5 rounded-full bg-current\" />\n      </span>\n    </span>\n  )\n}\n\nconst SHAPES: Record<SpinnerVariant, (props: ShapeProps) => React.JSX.Element> = {\n  ring: RingShape,\n  dots: DotsShape,\n  bars: BarsShape,\n  orbit: OrbitShape,\n}\n\ntype SpinnerProps = React.ComponentProps<\"span\"> & {\n  /** `ring` an arc on a track, `dots` three in sequence, `bars` eight around a centre, `orbit` one dot circling. */\n  variant?: SpinnerVariant\n  /** 16, 24 or 32 px. */\n  size?: SpinnerSize\n  /** What is happening, announced politely to assistive tech. */\n  label?: string\n  /** Shows the label next to the mark. */\n  showLabel?: boolean\n}\n\nfunction Spinner({ variant = \"ring\", size = \"md\", label = \"Loading\", showLabel = false, className, ...props }: SpinnerProps) {\n  const reduceMotion = useReducedMotion() ?? false\n  const Shape = SHAPES[variant]\n  return (\n    <span\n      role=\"status\"\n      data-slot=\"spinner\"\n      data-variant={variant}\n      data-size={size}\n      data-reduced={reduceMotion ? \"true\" : undefined}\n      className={cn(\"inline-flex items-center gap-2 text-muted-foreground\", className)}\n      {...props}\n    >\n      {reduceMotion ? (\n        // Opacity only: still says \"working\" to someone who asked the screen not to move.\n        <motion.span\n          aria-hidden=\"true\"\n          className={cn(\"block shrink-0\", SIZE_CLASS[size])}\n          animate={{ opacity: PULSE_OPACITY }}\n          transition={{ duration: PULSE_SECONDS, ease: \"easeInOut\", repeat: Infinity }}\n        >\n          <Shape moving={false} />\n        </motion.span>\n      ) : (\n        <span aria-hidden=\"true\" className={cn(\"block shrink-0\", SIZE_CLASS[size])}>\n          <Shape moving />\n        </span>\n      )}\n      <span className={showLabel ? \"text-[13px]\" : \"sr-only\"}>{label}</span>\n    </span>\n  )\n}\n\nexport { Spinner, type SpinnerProps, type SpinnerVariant, type SpinnerSize }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "feedback"
  ],
  "type": "registry:ui"
}