{ "name": "concentric-rings-spinner", "type": "registry:ui", "registryDependencies": [], "dependencies": [], "devDependencies": [], "tailwind": {}, "cssVars": { "light": {}, "dark": {} }, "files": [ { "path": "concentric-rings-spinner.tsx", "content": "'use client';\n\nimport * as React from 'react';\nimport { cn } from '@/lib/utils';\n\ntype RadiusShape = number | { inner: number; outer: number };\n\nexport type ConcentricRingsSpinnerProps = {\n /** Canvas size in px (square) */\n size?: number;\n /** Number of rings */\n rings?: number;\n /** Dots per ring (constant or per-ring taper if you pass a function) */\n dotsPerRing?: number;\n /** Radius for first (inner) ring */\n innerRadius?: number;\n /** Space between rings */\n ringGap?: number;\n /** Dot radius. Number for fixed or {inner, outer} to taper */\n dotRadius?: RadiusShape;\n /** Seconds per revolution for middle ring */\n speed?: number;\n /** Spin direction: 1 (cw) or -1 (ccw) */\n direction?: 1 | -1;\n /** Fade the center (0..1). 0=no fade, 1=strong fade */\n fadeCenter?: number;\n /** Fade the edges (0..1). 0=no fade, 1=strong fade */\n fadeEdge?: number;\n /** Whether to alternately reverse rings (nice parallax) */\n alternate?: boolean;\n /** Optional pulsing opacity */\n pulse?: boolean;\n /** Extra classes */\n className?: string;\n /** Extra classes for each dot */\n dotClassName?: string;\n /** a11y label */\n label?: string;\n};\n\nfunction lerp(a: number, b: number, t: number) {\n return a + (b - a) * t;\n}\n\nexport function ConcentricRingsSpinner({\n size = 200,\n rings = 12,\n dotsPerRing = 36,\n innerRadius = 10,\n ringGap = 8,\n dotRadius = { inner: 2, outer: 4 },\n speed = 2,\n direction = 1,\n fadeCenter = 0.15,\n fadeEdge = 0.35,\n alternate = true,\n pulse = true,\n className,\n dotClassName,\n label = 'Loading…',\n}: ConcentricRingsSpinnerProps) {\n const w = size;\n const h = size;\n const cx = w / 2;\n const cy = h / 2;\n\n const getDotR = (ringIdx: number) => {\n if (typeof dotRadius === 'number') return dotRadius;\n // ringIdx: 0 (inner) -> rings-1 (outer)\n const t = rings <= 1 ? 0 : ringIdx / (rings - 1);\n return lerp(dotRadius.inner, dotRadius.outer, t);\n };\n\n // ring opacity falloff\n const opacityFor = (ringIdx: number) => {\n const t = rings <= 1 ? 0 : ringIdx / (rings - 1);\n const centerFade = lerp(1, 1 - fadeCenter, 1 - Math.abs(0.5 - t) * 2); // higher in middle\n const edgeFade = lerp(1, 1 - fadeEdge, t); // fade outer\n return Math.max(0, Math.min(1, centerFade * edgeFade));\n };\n\n return (\n \n \n\n {Array.from({ length: rings }, (_, r) => {\n const radius = innerRadius + r * ringGap;\n const dots = dotsPerRing;\n const ringSpeed = speed * (0.6 + 0.8 * (r / Math.max(1, rings - 1))); // inner slower, outer faster\n const sign = alternate\n ? r % 2 === 0\n ? direction\n : ((direction * -1) as 1 | -1)\n : direction;\n\n const groupStyle: React.CSSProperties = {\n transformOrigin: '50% 50%',\n animation: `crs-rotate ${ringSpeed}s linear infinite`,\n animationDirection: sign === 1 ? 'normal' : 'reverse',\n };\n\n return (\n \n {Array.from({ length: dots }, (_, i) => {\n const a = (i / dots) * Math.PI * 2;\n const x = cx + Math.cos(a) * radius;\n const y = cy + Math.sin(a) * radius;\n return (\n \n );\n })}\n \n );\n })}\n\n {/* tiny center dot for compositional balance */}\n \n \n );\n}\n", "type": "registry:ui" } ] }