{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "spinner",
  "type": "registry:ui",
  "registryDependencies": [],
  "dependencies": ["motion"],
  "devDependencies": [],
  "files": [
    {
      "path": "spinner.tsx",
      "content": "\"use client\";\n\nimport { motion } from \"motion/react\";\nimport {\n  type ComponentProps,\n  type CSSProperties,\n  forwardRef,\n  type HTMLAttributes,\n  useLayoutEffect,\n} from \"react\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst componentThemeClassName =\n  \"[--ic-background:#ffffff] [--ic-foreground:#111111] [--ic-primary:#111111] [--ic-secondary:#646b75] [--ic-surface-border:#e9edf2] [--ic-border:#e3e7ec] [--ic-card:#ffffff] [--ic-card-foreground:#111111] [--ic-muted:#f5f7fa] [--ic-muted-foreground:#6d7480] [--ic-accent:#f3f5f8] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--ic-accent-foreground:#111111] [--ic-input:#e3e7ec] [--ic-ring:rgba(17,17,17,0.16)] [--ic-destructive:#dc2626] [--ic-paper:#fcfcfd] [--ic-popover-foreground:#111111] [--ic-brand:#0ea5e9] [--ic-brand-soft:#bae6fd] [--ic-shadow-soft:0_18px_38px_-24px_rgba(15,23,42,0.35)] [--ic-chart-1:oklch(0.52_0.19_254)] [--ic-chart-2:oklch(0.74_0.11_232)] [--ic-chart-3:oklch(0.42_0.16_262)] [--ic-chart-4:oklch(0.84_0.07_228)] [--ic-chart-5:oklch(0.62_0.14_240)] [--color-background:var(--ic-background)] [--color-foreground:var(--ic-foreground)] [--color-primary:var(--ic-primary)] [--color-secondary:var(--ic-secondary)] [--color-border:var(--ic-border)] [--color-card:var(--ic-card)] [--color-card-foreground:var(--ic-card-foreground)] [--color-muted:var(--ic-muted)] [--color-muted-foreground:var(--ic-muted-foreground)] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] [--color-input:var(--ic-input)] [--color-ring:var(--ic-ring)] [--color-destructive:var(--ic-destructive)] [--color-paper:var(--ic-paper)] [--color-popover-foreground:var(--ic-popover-foreground)] [--color-brand:var(--ic-brand)] [--color-brand-soft:var(--ic-brand-soft)] [--color-chart-1:var(--ic-chart-1)] [--color-chart-2:var(--ic-chart-2)] [--color-chart-3:var(--ic-chart-3)] [--color-chart-4:var(--ic-chart-4)] [--color-chart-5:var(--ic-chart-5)] dark:[--ic-background:#111111] dark:[--ic-foreground:#f6f3ec] dark:[--ic-primary:#f6f3ec] dark:[--ic-secondary:#cbc6bb] dark:[--ic-surface-border:#2a2a25] dark:[--ic-border:#2b2a25] dark:[--ic-card:#111111] dark:[--ic-card-foreground:#f6f3ec] dark:[--ic-muted:#171716] dark:[--ic-muted-foreground:#9a958a] dark:[--ic-accent:#1a1a18] [--color-accent:var(--ic-accent)] [--color-accent-foreground:var(--ic-accent-foreground)] dark:[--ic-accent-foreground:#f6f3ec] dark:[--ic-input:#2b2a25] dark:[--ic-ring:rgba(246,243,236,0.18)] dark:[--ic-destructive:#f87171] dark:[--ic-paper:#171716] dark:[--ic-popover-foreground:#f6f3ec] dark:[--ic-brand:#38bdf8] dark:[--ic-brand-soft:#0c4a6e] dark:[--ic-shadow-soft:0_20px_44px_-28px_rgba(0,0,0,0.6)] dark:[--ic-chart-1:oklch(0.68_0.17_250)] dark:[--ic-chart-2:oklch(0.82_0.09_225)] dark:[--ic-chart-3:oklch(0.58_0.15_260)] dark:[--ic-chart-4:oklch(0.75_0.12_235)] dark:[--ic-chart-5:oklch(0.88_0.06_220)]\";\n\nconst MotionSpan = motion.create(\"span\");\nconst MATRIX_STYLE_ID = \"iconiq-spinner-matrix-styles\";\nconst MATRIX_GRID = 5;\n\nconst MATRIX_CSS = `\n@keyframes iconiq-spinner-matrix-wave {\n  0%,\n  100% {\n    opacity: 0.16;\n    transform: scale(0.88);\n  }\n\n  50% {\n    opacity: 1;\n    transform: scale(1);\n  }\n}\n\n.iconiq-spinner-matrix-dot {\n  animation: iconiq-spinner-matrix-wave var(--iconiq-matrix-duration, 2.8s) ease-in-out infinite;\n  animation-delay: calc(var(--iconiq-matrix-phase, 0) * var(--iconiq-matrix-duration, 2.8s) * -1);\n  background-color: var(--ic-primary);\n  transform-origin: center;\n}\n`;\n\ntype SpinnerSize = \"sm\" | \"md\" | \"lg\";\ntype SpinnerVariant = \"ring\" | \"dots\" | \"matrix\";\n\nconst spinnerSizeClasses = {\n  sm: {\n    root: \"size-4\",\n    border: \"border\",\n    gap: \"gap-[18%]\",\n  },\n  md: {\n    root: \"size-6\",\n    border: \"border-2\",\n    gap: \"gap-[18%]\",\n  },\n  lg: {\n    root: \"size-8\",\n    border: \"border-2\",\n    gap: \"gap-[18%]\",\n  },\n} as const satisfies Record<\n  SpinnerSize,\n  { root: string; border: string; gap: string }\n>;\n\nconst spinnerMatrixConfig = {\n  sm: {\n    root: \"size-6\",\n    gap: \"gap-[10%]\",\n  },\n  md: {\n    root: \"size-7\",\n    gap: \"gap-[9%]\",\n  },\n  lg: {\n    root: \"size-9\",\n    gap: \"gap-[10%]\",\n  },\n} as const satisfies Record<SpinnerSize, { root: string; gap: string }>;\n\nconst spinnerVariantDuration = {\n  ring: 1,\n  dots: 0.9,\n  matrix: 2.8,\n} as const satisfies Record<SpinnerVariant, number>;\n\nfunction ensureMatrixStyles() {\n  if (typeof document === \"undefined\") {\n    return;\n  }\n\n  let style = document.getElementById(\n    MATRIX_STYLE_ID\n  ) as HTMLStyleElement | null;\n\n  if (!style) {\n    style = document.createElement(\"style\");\n    style.id = MATRIX_STYLE_ID;\n    document.head.appendChild(style);\n  }\n\n  style.textContent = MATRIX_CSS;\n}\n\nfunction getMatrixGridSize() {\n  return MATRIX_GRID;\n}\n\nfunction getMatrixDiagonalPhase(row: number, col: number, gridSize: number) {\n  if (gridSize <= 1) {\n    return 0;\n  }\n\n  return (row + col) / (2 * (gridSize - 1));\n}\n\nfunction buildMatrixCells(gridSize: number) {\n  return Array.from({ length: gridSize * gridSize }, (_, index) => {\n    const row = Math.floor(index / gridSize);\n    const col = index % gridSize;\n\n    return {\n      row,\n      col,\n      index,\n      phase: getMatrixDiagonalPhase(row, col, gridSize),\n    };\n  });\n}\n\nfunction resolveSpinnerA11yProps({\n  decorative = false,\n}: {\n  decorative?: boolean;\n}) {\n  if (decorative) {\n    return { \"aria-hidden\": true as const };\n  }\n\n  return {\n    \"aria-label\": \"Loading\",\n    \"aria-live\": \"polite\" as const,\n    role: \"status\" as const,\n  };\n}\n\ninterface SpinnerProps extends HTMLAttributes<HTMLSpanElement> {\n  decorative?: boolean;\n  size?: SpinnerSize;\n  variant?: SpinnerVariant;\n}\n\nconst Spinner = forwardRef<HTMLSpanElement, SpinnerProps>(function Spinner(\n  { className, decorative = false, size = \"md\", variant = \"ring\", ...props },\n  ref\n) {\n  const sizeClasses = spinnerSizeClasses[size];\n  const variantDuration = spinnerVariantDuration[variant];\n  const a11yProps = resolveSpinnerA11yProps({ decorative });\n\n  useLayoutEffect(() => {\n    if (variant === \"matrix\") {\n      ensureMatrixStyles();\n    }\n  }, [variant]);\n\n  if (variant === \"dots\") {\n    const dotDelayStep = variantDuration * 0.167;\n\n    return (\n      <span\n        className={cn(\n          componentThemeClassName,\n          \"inline-flex items-center justify-center\",\n          sizeClasses.root,\n          sizeClasses.gap,\n          className\n        )}\n        ref={ref}\n        {...a11yProps}\n        {...props}\n      >\n        {[0, 1, 2].map((index) => (\n          <motion.span\n            animate={{ y: [\"0%\", \"-55%\", \"0%\"] }}\n            aria-hidden\n            className=\"aspect-square w-[22%] shrink-0 rounded-full bg-primary\"\n            key={index}\n            transition={{\n              duration: variantDuration,\n              repeat: Number.POSITIVE_INFINITY,\n              ease: \"easeInOut\",\n              delay: index * dotDelayStep,\n            }}\n          />\n        ))}\n      </span>\n    );\n  }\n\n  if (variant === \"matrix\") {\n    const matrixConfig = spinnerMatrixConfig[size];\n    const cells = buildMatrixCells(MATRIX_GRID);\n\n    return (\n      <span\n        className={cn(\n          componentThemeClassName,\n          \"inline-grid shrink-0 grid-cols-5 place-items-center\",\n          matrixConfig.root,\n          matrixConfig.gap,\n          className\n        )}\n        ref={ref}\n        {...a11yProps}\n        {...props}\n      >\n        {cells.map(({ index, phase }) => (\n          <span\n            aria-hidden\n            className=\"iconiq-spinner-matrix-dot aspect-square w-full rounded-full\"\n            key={index}\n            style={\n              {\n                \"--iconiq-matrix-duration\": `${variantDuration}s`,\n                \"--iconiq-matrix-phase\": phase,\n              } as CSSProperties\n            }\n          />\n        ))}\n      </span>\n    );\n  }\n\n  return (\n    <MotionSpan\n      animate={{ rotate: 360 }}\n      className={cn(\n        componentThemeClassName,\n        \"box-border inline-flex shrink-0 rounded-full border-muted border-t-primary\",\n        sizeClasses.root,\n        sizeClasses.border,\n        className\n      )}\n      ref={ref}\n      transition={{\n        duration: variantDuration,\n        repeat: Number.POSITIVE_INFINITY,\n        ease: \"linear\",\n      }}\n      {...a11yProps}\n      {...(props as ComponentProps<typeof MotionSpan>)}\n    />\n  );\n});\n\nSpinner.displayName = \"Spinner\";\n\nexport {\n  buildMatrixCells,\n  getMatrixDiagonalPhase,\n  getMatrixGridSize,\n  resolveSpinnerA11yProps,\n  Spinner,\n  spinnerMatrixConfig,\n  spinnerSizeClasses,\n  spinnerVariantDuration,\n};\nexport default Spinner;\nexport type { SpinnerProps, SpinnerSize, SpinnerVariant };\n",
      "type": "registry:ui"
    }
  ],
  "title": "Spinner",
  "description": "Loading indicator with ring, bouncing dots, or dot-matrix sweep variants. Motion; uses primary and muted theme tokens."
}
