{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "type": "registry:ui",
  "title": "Spinner",
  "description": "An accessible React spinner for Next.js and Tailwind CSS: one line going round an infinity that fills it once the task is done.",
  "author": "FewTone <https://fewtone.com>",
  "dependencies": [
    "class-variance-authority@^0.7.1",
    "motion@^14.0.0"
  ],
  "registryDependencies": [
    "https://ui.fewtone.com/r/tokens.json",
    "https://ui.fewtone.com/r/motion.json"
  ],
  "files": [
    {
      "path": "registry/fewtone/ui/spinner.tsx",
      "type": "registry:ui",
      "content": "\"use client\";\n\nimport { duration, ease } from \"@/lib/motion\";\nimport { cn } from \"@/lib/utils\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { animate, motion, useMotionValue, useReducedMotion, useTransform } from \"motion/react\";\nimport { type ComponentProps, useEffect } from \"react\";\n\n/*\n * A small loading indicator (owner's roadmap, 2026-10-16), redesigned as the infinity the owner chose in the Lab\n * (2026-10-22), ported from it exactly: the same path, line, timings and finish. In the text's colour.\n * - One line travels round and round an infinity at an even pace over a faint track.\n * - `done` shows the task finished: the line stops where it is and grows forward until it fills the whole\n *   infinity, the track fading out under it. Back to loading, it shrinks to a line and travels on.\n * - Motion: the line is a dash along the path (its offset and length), so only the stroke moves. With reduced\n *   motion it doesn't travel: it pulses its opacity instead, and a finish fills at once.\n * - role=\"status\", named \"Loading\" unless you give it another aria-label, and \"Done\" once finished (doneLabel).\n *   Inside a button that already says what's happening (\"Saving…\"), hide it with aria-hidden, or the button's\n *   name would include it.\n * - Sizes are the icon sizes (14, 16 and 20px), so it can stand in for an icon anywhere.\n */\n\n/**\n * The infinity as one continuous stroke (a dash restarts at every subpath, so it couldn't run from one loop into\n * the other): from the centre up into the right loop, round it, back through the centre into the left loop,\n * round it and home\n */\nconst INFINITY =\n  \"M12 12C14 9 16 8 18 8A4 4 0 1 1 18 16C16 16 14 15 12 12C10 9 8 8 6 8A4 4 0 1 0 6 16C8 16 10 15 12 12Z\";\n/** The line's length, in hundredths of the infinity */\nconst LINE = 24;\n/** One second: the old spinner's turn; the line goes round in 1.6 of them */\nconst TURN = duration.slower * 2;\nconst forever = { repeat: Number.POSITIVE_INFINITY } as const;\n\nconst spinnerVariants = cva(\"inline-flex shrink-0\", {\n  variants: {\n    size: {\n      sm: \"size-(--size-icon-sm)\",\n      md: \"size-(--size-icon-md)\",\n      lg: \"size-(--size-icon-lg)\",\n    },\n  },\n  defaultVariants: { size: \"md\" },\n});\n\nexport type SpinnerProps = ComponentProps<\"span\"> &\n  VariantProps<typeof spinnerVariants> & {\n    /** The task is finished: the line fills the whole infinity */\n    done?: boolean;\n    /** Its name once done, for screen readers */\n    doneLabel?: string;\n  };\n\nfunction Spinner({\n  className,\n  size,\n  done = false,\n  doneLabel = \"Done\",\n  \"aria-label\": label = \"Loading\",\n  ...props\n}: SpinnerProps) {\n  const reduce = useReducedMotion() ?? false;\n  // Where the line starts along the infinity, and how long it is, in hundredths\n  const offset = useMotionValue(0);\n  const length = useMotionValue(done ? 100 : LINE);\n  const dasharray = useTransform(length, (line) => `${line} ${Math.max(0, 100 - line)}`);\n\n  useEffect(() => {\n    if (done) {\n      // It stops where it is and fills forward from there\n      const fill = animate(\n        length,\n        100,\n        reduce ? { duration: 0 } : { duration: duration.slower * 1.6, ease: ease.out },\n      );\n      return () => fill.stop();\n    }\n    const shrink = animate(\n      length,\n      LINE,\n      reduce ? { duration: 0 } : { duration: duration.slow, ease: ease.out },\n    );\n    if (reduce) return () => shrink.stop();\n    // Round and round at an even pace, on from wherever it is\n    const from = offset.get();\n    const travel = animate(offset, [from, from - 100], { duration: TURN * 1.6, ease: \"linear\", ...forever });\n    return () => {\n      shrink.stop();\n      travel.stop();\n    };\n  }, [done, reduce, offset, length]);\n\n  return (\n    <span\n      role=\"status\"\n      aria-label={done ? doneLabel : label}\n      data-slot=\"spinner\"\n      data-size={size ?? \"md\"}\n      data-done={done || undefined}\n      className={cn(spinnerVariants({ size }), className)}\n      {...props}\n    >\n      {/* With reduced motion the drawing pulses while loading; finished, it holds still */}\n      <motion.span\n        key={reduce && !done ? \"pulse\" : \"still\"}\n        className=\"inline-flex size-full\"\n        animate={reduce && !done ? { opacity: [1, 0.4, 1] } : { opacity: 1 }}\n        transition={\n          reduce && !done ? { duration: TURN * 1.5, ease: ease.inOut, ...forever } : { duration: 0 }\n        }\n      >\n        <svg viewBox=\"0 0 24 24\" fill=\"none\" aria-hidden className=\"size-full\">\n          <motion.path\n            d={INFINITY}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.2\"\n            strokeLinejoin=\"round\"\n            initial={false}\n            animate={{ opacity: done ? 0 : 0.15 }}\n            transition={reduce ? { duration: 0 } : { duration: duration.slower, ease: ease.out }}\n          />\n          <motion.path\n            data-slot=\"spinner-line\"\n            d={INFINITY}\n            pathLength={100}\n            stroke=\"currentColor\"\n            strokeWidth=\"2.2\"\n            strokeLinecap=\"round\"\n            strokeLinejoin=\"round\"\n            style={{ strokeDasharray: dasharray, strokeDashoffset: offset }}\n          />\n        </svg>\n      </motion.span>\n    </span>\n  );\n}\n\nexport { Spinner, spinnerVariants };\n"
    }
  ],
  "categories": [
    "feedback"
  ],
  "docs": "Usage and examples: https://ui.fewtone.com/components/spinner"
}
