{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "ring-stat", "title": "Ring Stat", "description": "A hairline circle marking a reading with a bright petal instead of a filled bar. Flourish, progress and dual variants.", "dependencies": [ "class-variance-authority" ], "registryDependencies": [ "http://localhost:5173/r/liquid-theme.json", "http://localhost:5173/r/liquid-path.json" ], "files": [ { "path": "src/components/ui/ring-stat.tsx", "content": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\nimport { arcPath, clamp, smoothPath, type Point } from \"@/lib/liquid-path\"\n\n/**\n * One strand of the flourish: a sine wave running *along* an arc of the ring,\n * oscillating radially across it rather than sitting beside it.\n *\n * Two strands half a period apart cross wherever the sine hits zero, which is\n * what produces the row of lens shapes — a double helix seen edge-on, wrapped\n * onto the circle. It only reads as a helix once there are several crossings;\n * one or two just look like a ribbon that lost its way.\n *\n * The envelope collapses the amplitude to nothing at both ends so each strand\n * emerges from the ring line and merges back into it instead of stopping\n * mid-air. It is a *clipped* sine rather than a plain one: a plain sine peaks\n * only at the midpoint, so the outer crossings pinch shut and the braid tapers\n * to mush at both ends. Overdriving then clamping holds the middle crossings\n * at equal depth and spends the falloff on the last half-period at each end.\n */\nfunction strandPath(\n cx: number,\n cy: number,\n r: number,\n startDeg: number,\n endDeg: number,\n amplitude: number,\n turns: number,\n phase: number,\n samples = 160,\n): string {\n const points: Point[] = []\n for (let i = 0; i <= samples; i++) {\n const t = i / samples\n const rad = ((startDeg + (endDeg - startDeg) * t) * Math.PI) / 180\n const envelope = Math.min(1, Math.sin(Math.PI * t) * 1.9)\n const radius = r + amplitude * envelope * Math.sin(t * turns * Math.PI + phase)\n points.push({ x: cx + radius * Math.cos(rad), y: cy + radius * Math.sin(rad) })\n }\n return smoothPath(points, 0.5)\n}\n\nconst ring = cva(\"\", {\n variants: {\n tone: {\n chalk: \"text-white\",\n ink: \"text-ink dark:text-white\",\n current: \"text-current\",\n volt: \"text-volt\",\n flare: \"text-flare\",\n },\n },\n defaultVariants: { tone: \"chalk\" },\n})\n\nexport interface RingStatProps\n extends React.ComponentProps<\"div\">,\n VariantProps {\n value: number\n min?: number\n max?: number\n /**\n * `flourish` is the Average Glucose ring: a hairline circle with strands\n * braided along it where the reading lands. `progress` sweeps a conventional\n * arc. `dual` draws an inner comparison ring.\n */\n variant?: \"flourish\" | \"progress\" | \"dual\"\n size?: number\n thickness?: number\n /** Second value for the `dual` variant. */\n compare?: number\n /** The strand / progress accent color class, e.g. `text-volt`. */\n accentClassName?: string\n\n /* --- flourish shape --- */\n /** How many strands are braided together. Two is the reference. */\n strands?: number\n /** Degrees of ring the braid covers, centred on the value. */\n strandSpan?: number\n /** Half-periods across the span. Each one is a crossing, so 5 gives four lenses. */\n strandTurns?: number\n /** Radial swing as a fraction of the ring radius. */\n strandAmplitude?: number\n\n children?: React.ReactNode\n label?: string\n}\n\nexport function RingStat({\n value,\n min = 0,\n max = 100,\n variant = \"flourish\",\n size = 132,\n thickness = 1.25,\n compare,\n accentClassName = \"text-volt\",\n strands = 2,\n strandSpan = 110,\n strandTurns = 5,\n strandAmplitude = 0.12,\n tone,\n label,\n className,\n children,\n ...props\n}: RingStatProps) {\n const t = clamp((value - min) / (max - min || 1), 0, 1)\n const cx = size / 2\n const cy = size / 2\n const r = size / 2 - thickness * 3\n const at = -90 + t * 360\n const rad = (d: number) => (d * Math.PI) / 180\n\n return (\n \n \n \n\n {variant === \"dual\" && compare != null ? (\n \n ) : null}\n\n {variant === \"progress\" || variant === \"dual\" ? (\n \n ) : null}\n\n {variant === \"flourish\"\n ? Array.from({ length: Math.max(1, strands) }).map((_, i) => {\n // Anti-phase for two strands, evenly braided for more. Half a\n // period is what makes them cross symmetrically and open into\n // lenses; anything else reads as one wandering squiggle.\n const phase = (i * Math.PI * 2) / Math.max(1, strands)\n // Each successive strand covers slightly less arc, so the braid\n // frays rather than ending on a hard vertical.\n const span = strandSpan * (1 - i * 0.06)\n return (\n \n )\n })\n : null}\n\n {variant !== \"flourish\" ? (\n \n ) : null}\n \n
{children}
\n \n )\n}\n", "type": "registry:ui" } ], "type": "registry:ui" }