{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "type": "registry:component",
  "title": "Spinner",
  "description": "Something working that has not finished: an arc, spokes, dots, an orbit, a pulse or a dither cell.",
  "dependencies": [
    "tailwind-variants"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "components/panelui/spinner.tsx",
      "type": "registry:component",
      "target": "@components/panelui/spinner.tsx",
      "content": "/**\n * Spinner — shows that something is working and has not finished.\n *\n * ```tsx\n * <Spinner />\n * <Spinner variant=\"dots\" size=\"lg\" label=\"Sending\" />\n * ```\n *\n * ## Six looks\n *\n * `ring` is an arc running round a faint track, for most waits. `spokes` is\n * twelve lines stepping round, the shape people read as \"loading\" from a\n * desktop. `dots` raises three dots in turn and sits on a line of text.\n * `orbit` sends two dots round one circle, meeting and parting. `pulse`\n * breathes a ring out of a dot, for waiting on somebody else rather than on\n * a computer. `dither` fills a three-by-three cell in ordered-dither order,\n * the collection's own pattern.\n *\n * ## Speed\n *\n * The motion is quick on purpose: a fast spinner makes the same wait feel\n * shorter. With reduced motion asked for it slows to a third of its speed\n * instead of stopping, because a spinner that stands still reads as frozen.\n *\n * ## Colour and size\n *\n * It draws in `currentColor`, so it takes the text colour of whatever it is\n * in. `size` gives three steps; a `size-*` class sets any other.\n */\nimport type { ComponentProps } from 'react';\nimport { tv } from 'tailwind-variants';\n\n/*\n * Every look runs on keyframes of its own, shipped with the component so it\n * needs nothing added to a stylesheet. `--panelui-spinner-pace` stretches all\n * of them at once, which is how reduced motion slows them.\n */\nconst KEYFRAMES =\n  '@keyframes panelui-spinner-turn{to{transform:rotate(360deg)}}' +\n  '@keyframes panelui-spinner-rise{0%,60%,100%{transform:translateY(0);opacity:.35}30%{transform:translateY(-4px);opacity:1}}' +\n  '@keyframes panelui-spinner-orbit{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}' +\n  '@keyframes panelui-spinner-breathe{0%{transform:scale(.35);opacity:.9}100%{transform:scale(1);opacity:0}}' +\n  '@keyframes panelui-spinner-beat{0%,100%{transform:scale(.8)}50%{transform:scale(1)}}' +\n  // One cycle per cell of the dither: it lights when the fill reaches its\n  // rank and goes out when the fill falls back past it, so the cell fills in\n  // ordered-dither order and empties in reverse.\n  Array.from({ length: 9 }, (_, rank) => {\n    const on = 4 + rank * 5;\n    return `@keyframes panelui-spinner-cell-${rank}{0%,${on - 3}%{opacity:.14}${on}%,${100 - on}%{opacity:1}${103 - on}%,100%{opacity:.14}}`;\n  }).join('') +\n  '[data-slot=spinner]{--panelui-spinner-pace:1}' +\n  '@media (prefers-reduced-motion:reduce){[data-slot=spinner]{--panelui-spinner-pace:3}}';\n\nconst spinnerVariants = tv({\n  base: 'inline-block shrink-0 align-middle',\n  variants: {\n    size: {\n      sm: 'size-3.5',\n      md: 'size-4',\n      lg: 'size-6',\n    },\n  },\n  defaultVariants: { size: 'md' },\n});\n\n/** How long one cycle takes at normal speed, in seconds, multiplied by the pace. */\nconst pace = (seconds: number) => `calc(${seconds}s * var(--panelui-spinner-pace))`;\n\n/** Lays the cells of a 3×3 ordered-dither matrix out in the order they light. */\nconst BAYER_3 = [\n  [0, 7, 3],\n  [6, 5, 2],\n  [4, 1, 8],\n] as const;\n\nfunction Ring() {\n  return (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"9\" stroke=\"currentColor\" strokeWidth=\"2.5\" opacity=\"0.2\" />\n      <g style={{ transformOrigin: '12px 12px', animation: `panelui-spinner-turn ${pace(0.7)} linear infinite` }}>\n        <path d=\"M12 3a9 9 0 0 1 9 9\" stroke=\"currentColor\" strokeWidth=\"2.5\" strokeLinecap=\"round\" />\n      </g>\n    </>\n  );\n}\n\nfunction Spokes() {\n  // Twelve spokes fading from the leading one, turned a twelfth at a time, so\n  // the fade appears to run round them without any spoke changing.\n  return (\n    <g style={{ transformOrigin: '12px 12px', animation: `panelui-spinner-turn ${pace(0.9)} steps(12) infinite` }}>\n      {Array.from({ length: 12 }, (_, index) => (\n        <line\n          key={index}\n          x1=\"12\"\n          y1=\"2.5\"\n          x2=\"12\"\n          y2=\"7\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2.25\"\n          strokeLinecap=\"round\"\n          opacity={1 - index * 0.07}\n          transform={`rotate(${-index * 30} 12 12)`}\n        />\n      ))}\n    </g>\n  );\n}\n\nfunction Dots() {\n  return (\n    <>\n      {[5, 12, 19].map((cx, index) => (\n        <circle\n          key={cx}\n          cx={cx}\n          cy=\"13\"\n          r=\"2.4\"\n          fill=\"currentColor\"\n          style={{\n            animation: `panelui-spinner-rise ${pace(0.9)} ease-in-out infinite`,\n            animationDelay: `calc(${index * 0.14}s * var(--panelui-spinner-pace))`,\n          }}\n        />\n      ))}\n    </>\n  );\n}\n\nfunction Orbit() {\n  // Two dots on one circle, each eased round on its own clock: they bunch up\n  // where the curve is slow and pull apart where it is fast.\n  return (\n    <>\n      <circle cx=\"12\" cy=\"12\" r=\"8\" stroke=\"currentColor\" strokeWidth=\"1.5\" opacity=\"0.18\" />\n      {[0, 0.18].map((delay, index) => (\n        <g\n          key={delay}\n          style={{\n            transformOrigin: '12px 12px',\n            animation: `panelui-spinner-orbit ${pace(1.1)} cubic-bezier(0.65, 0, 0.35, 1) infinite`,\n            animationDelay: `calc(${delay}s * var(--panelui-spinner-pace))`,\n          }}\n        >\n          <circle cx=\"12\" cy=\"4\" r={index === 0 ? 2.6 : 1.9} fill=\"currentColor\" />\n        </g>\n      ))}\n    </>\n  );\n}\n\nfunction Pulse() {\n  return (\n    <>\n      {[0, 0.6].map((delay) => (\n        <circle\n          key={delay}\n          cx=\"12\"\n          cy=\"12\"\n          r=\"10\"\n          stroke=\"currentColor\"\n          strokeWidth=\"1.75\"\n          style={{\n            transformOrigin: '12px 12px',\n            animation: `panelui-spinner-breathe ${pace(1.2)} cubic-bezier(0.23, 1, 0.32, 1) infinite`,\n            animationDelay: `calc(${delay}s * var(--panelui-spinner-pace))`,\n          }}\n        />\n      ))}\n      <circle\n        cx=\"12\"\n        cy=\"12\"\n        r=\"3.5\"\n        fill=\"currentColor\"\n        style={{ transformOrigin: '12px 12px', animation: `panelui-spinner-beat ${pace(1.2)} ease-in-out infinite` }}\n      />\n    </>\n  );\n}\n\nfunction Dither() {\n  // Each cell runs its own window of the cycle; see the keyframes above.\n  return (\n    <>\n      {BAYER_3.flatMap((row, y) =>\n        row.map((rank, x) => (\n          <rect\n            key={`${x}-${y}`}\n            x={2 + x * 7}\n            y={2 + y * 7}\n            width=\"6\"\n            height=\"6\"\n            rx=\"1.25\"\n            fill=\"currentColor\"\n            style={{ animation: `panelui-spinner-cell-${rank} ${pace(1.6)} linear infinite` }}\n          />\n        ))\n      )}\n    </>\n  );\n}\n\nconst LOOKS = { ring: Ring, spokes: Spokes, dots: Dots, orbit: Orbit, pulse: Pulse, dither: Dither } as const;\n\nexport type SpinnerVariant = keyof typeof LOOKS;\n\nexport interface SpinnerProps extends Omit<ComponentProps<'svg'>, 'children'> {\n  /** Its look: `ring`, `spokes`, `dots`, `orbit`, `pulse` or `dither`. Default `ring`. */\n  variant?: SpinnerVariant;\n  /** `sm` is 14px, `md` 16px and `lg` 24px. A `size-*` class sets any other. Default `md`. */\n  size?: 'sm' | 'md' | 'lg';\n  /** What a screen reader announces. Default `Loading`. */\n  label?: string;\n  className?: string;\n}\n\n/** A spinner in the colour of the text around it. Every attribute of `<svg>` passes through. */\nexport function Spinner({ variant = 'ring', size, label = 'Loading', className, ...props }: SpinnerProps) {\n  const Look = LOOKS[variant];\n  return (\n    <svg\n      role=\"status\"\n      aria-label={label}\n      data-slot=\"spinner\"\n      data-variant={variant}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      className={spinnerVariants({ size, className })}\n      {...props}\n    >\n      <style>{KEYFRAMES}</style>\n      <Look />\n    </svg>\n  );\n}\nSpinner.displayName = 'Spinner';\n"
    }
  ]
}
