{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "dither-donut",
	"title": "Dither Donut",
	"type": "registry:ui",
	"description": "Canvas dither chart from Amicro, ported to Svelte 5 and motion-sv.",
	"dependencies": [
		"@lucide/svelte",
		"runed"
	],
	"registryDependencies": [
		"https://amicro-sv.enisdev.com/r/dither-math.json"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { untrack } from 'svelte';\n\timport { prefersReducedMotion } from 'svelte/motion';\n\timport { Users } from '@lucide/svelte';\n\timport { AnimationFrames } from 'runed';\n\timport { smoothstep, hash, hexToRgba } from './dither-math.js';\n\n\texport interface Props {\n\t\ttheme?: 'dark' | 'light';\n\t\tcompact?: boolean;\n\t}\n\n\tlet { theme = 'dark', compact = false }: Props = $props();\n\n\ttype Plan = { name: string; color: string; base: number };\n\tconst PLANS: Plan[] = [\n\t\t{ name: 'Unlimited', color: '#FFFFFF', base: 1240 },\n\t\t{ name: '30-day pass', color: '#E2E8F0', base: 980 },\n\t\t{ name: '10-class pack', color: '#CBD5E1', base: 620 },\n\t\t{ name: 'Drop-in', color: '#94A3B8', base: 410 },\n\t\t{ name: 'Student', color: '#64748B', base: 300 }\n\t];\n\n\ttype Period = { name: string; mult: number };\n\tconst PERIODS: Period[] = [\n\t\t{ name: 'Week', mult: 0.42 },\n\t\t{ name: 'Month', mult: 1 },\n\t\t{ name: 'Quarter', mult: 2.6 },\n\t\t{ name: 'Year', mult: 8.4 }\n\t];\n\n\tfunction drawRoundedWedge(\n\t\tctx: CanvasRenderingContext2D,\n\t\tcx: number,\n\t\tcy: number,\n\t\trIn: number,\n\t\trOut: number,\n\t\taStart: number,\n\t\taEnd: number,\n\t\tcr: number\n\t) {\n\t\tconst sweep = aEnd - aStart;\n\t\tconst maxCr = Math.min(cr, (rOut - rIn) / 2, (sweep * rIn) / 2);\n\t\tif (sweep <= 0.001) return;\n\t\tconst crIn = maxCr;\n\t\tconst crOut = maxCr;\n\n\t\tconst aStartIn = aStart + crIn / rIn;\n\t\tconst aEndIn = aEnd - crIn / rIn;\n\t\tconst aStartOut = aStart + crOut / rOut;\n\t\tconst aEndOut = aEnd - crOut / rOut;\n\n\t\tctx.moveTo(cx + rIn * Math.cos(aStartIn), cy + rIn * Math.sin(aStartIn));\n\t\tctx.arc(cx, cy, rIn, aStartIn, aEndIn);\n\t\tctx.arcTo(\n\t\t\tcx + rIn * Math.cos(aEnd),\n\t\t\tcy + rIn * Math.sin(aEnd),\n\t\t\tcx + rOut * Math.cos(aEnd),\n\t\t\tcy + rOut * Math.sin(aEnd),\n\t\t\tcrIn\n\t\t);\n\t\tctx.arcTo(\n\t\t\tcx + rOut * Math.cos(aEnd),\n\t\t\tcy + rOut * Math.sin(aEnd),\n\t\t\tcx + rOut * Math.cos(aEndOut),\n\t\t\tcy + rOut * Math.sin(aEndOut),\n\t\t\tcrOut\n\t\t);\n\t\tctx.arc(cx, cy, rOut, aEndOut, aStartOut, true);\n\t\tctx.arcTo(\n\t\t\tcx + rOut * Math.cos(aStart),\n\t\t\tcy + rOut * Math.sin(aStart),\n\t\t\tcx + rIn * Math.cos(aStart),\n\t\t\tcy + rIn * Math.sin(aStart),\n\t\t\tcrOut\n\t\t);\n\t\tctx.arcTo(\n\t\t\tcx + rIn * Math.cos(aStart),\n\t\t\tcy + rIn * Math.sin(aStart),\n\t\t\tcx + rIn * Math.cos(aStartIn),\n\t\t\tcy + rIn * Math.sin(aStartIn),\n\t\t\tcrIn\n\t\t);\n\t}\n\n\tlet periodIndex = $state(1); \n\tlet hoverIndex = $state<number | null>(null);\n\tlet canvas = $state<HTMLCanvasElement | null>(null);\n\n\tconst period = $derived(PERIODS[periodIndex]);\n\n\tconst values = $derived(\n\t\tPLANS.map((plan, i) => {\n\t\t\tconst w = 0.78 + 0.4 * (0.5 + 0.5 * Math.sin(i * 1.9 + periodIndex * 1.3));\n\t\t\treturn Math.round(plan.base * period.mult * w);\n\t\t})\n\t);\n\tconst total = $derived(values.reduce((a, b) => a + b, 0));\n\tconst shares = $derived(values.map((v) => v / total));\n\n\tlet time = 0;\n\tlet morphStartTime = 0;\n\tlet fromShares: number[] = [];\n\tlet targetShares: number[] = [];\n\tlet dispShares: number[] = [];\n\n\t$effect(() => {\n\t\tconst next = shares;\n\t\tif (dispShares.length === 0) {\n\t\t\tdispShares = [...next];\n\t\t\tfromShares = [...next];\n\t\t\ttargetShares = [...next];\n\t\t} else {\n\t\t\tfromShares = [...dispShares];\n\t\t\ttargetShares = [...next];\n\t\t\tmorphStartTime = performance.now();\n\t\t}\n\t});\n\n\t$effect(() => {\n\t\tconst el = canvas;\n\t\tif (!el) return;\n\n\t\tconst draw = () => {\n\t\t\ttime += 0.02;\n\t\t\tconst ctx = el.getContext('2d');\n\t\t\tif (!ctx) return;\n\n\t\t\tconst dpr = Math.min(window.devicePixelRatio || 1, 2);\n\t\t\tconst logicalSize = 200;\n\t\t\tconst rect = el.getBoundingClientRect();\n\n\t\t\tif (el.width !== rect.width * dpr || el.height !== rect.height * dpr) {\n\t\t\t\tel.width = rect.width * dpr;\n\t\t\t\tel.height = rect.height * dpr;\n\t\t\t}\n\n\t\t\tctx.save();\n\t\t\tctx.clearRect(0, 0, el.width, el.height);\n\t\t\tctx.scale((rect.width * dpr) / logicalSize, (rect.height * dpr) / logicalSize);\n\n\t\t\tlet t = 0;\n\t\t\tif (morphStartTime > 0) {\n\t\t\t\tt = (performance.now() - morphStartTime) / 500;\n\t\t\t\tif (t > 1) t = 1;\n\t\t\t} else {\n\t\t\t\tt = 1;\n\t\t\t}\n\n\t\t\tconst reducedMotion = untrack(() => prefersReducedMotion.current);\n\t\t\tif (reducedMotion) {\n\t\t\t\tt = 1;\n\t\t\t\ttime = 0;\n\t\t\t}\n\n\t\t\tconst e = 1 - Math.pow(2, -10 * t);\n\t\t\tfor (let i = 0; i < targetShares.length; i++) {\n\t\t\t\tdispShares[i] = fromShares[i] + (targetShares[i] - fromShares[i]) * e;\n\t\t\t}\n\n\t\t\tlet startAngle = -Math.PI / 2;\n\t\t\tconst gap = 0.07;\n\t\t\tconst currentHover = untrack(() => hoverIndex);\n\n\t\t\tfor (let i = 0; i < dispShares.length; i++) {\n\t\t\t\tconst share = dispShares[i];\n\t\t\t\tif (share === 0) continue;\n\n\t\t\t\tconst sweep = share * Math.PI * 2;\n\t\t\t\tconst aStart = startAngle + gap / 2;\n\t\t\t\tlet aEnd = startAngle + sweep - gap / 2;\n\n\t\t\t\tif (aEnd < aStart) aEnd = aStart;\n\n\t\t\t\tctx.save();\n\t\t\t\tconst isHovered = currentHover === i;\n\t\t\t\tconst isAnyHovered = currentHover !== null;\n\n\t\t\t\tif (isHovered) {\n\t\t\t\t\tconst mid = (aStart + aEnd) / 2;\n\t\t\t\t\tctx.translate(Math.cos(mid) * 6, Math.sin(mid) * 6);\n\t\t\t\t}\n\n\t\t\t\tctx.beginPath();\n\t\t\t\tdrawRoundedWedge(ctx, 100, 100, 55, 86, aStart, aEnd, 6);\n\t\t\t\tctx.clip();\n\n\t\t\t\tctx.globalAlpha = isHovered ? 1.0 : isAnyHovered ? 0.3 * 0.72 : 0.72;\n\t\t\t\tctx.fillStyle = PLANS[i].color;\n\n\t\t\t\tif (isHovered) {\n\t\t\t\t\tctx.shadowColor = hexToRgba(PLANS[i].color, 0.55);\n\t\t\t\t\tctx.shadowBlur = 5;\n\t\t\t\t\tctx.shadowOffsetX = 0;\n\t\t\t\t\tctx.shadowOffsetY = 0;\n\t\t\t\t}\n\n\t\t\t\tconst cell = 4.6;\n\t\t\t\tfor (let x = 14; x <= 186; x += cell) {\n\t\t\t\t\tfor (let y = 14; y <= 186; y += cell) {\n\t\t\t\t\t\tconst dx = x - 100;\n\t\t\t\t\t\tconst dy = y - 100;\n\t\t\t\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy);\n\t\t\t\t\t\tif (dist < 55 - cell || dist > 86 + cell) continue;\n\n\t\t\t\t\t\tconst a = Math.atan2(dy, dx);\n\t\t\t\t\t\tlet normalizedA = a - aStart;\n\t\t\t\t\t\twhile (normalizedA < 0) normalizedA += Math.PI * 2;\n\t\t\t\t\t\twhile (normalizedA >= Math.PI * 2) normalizedA -= Math.PI * 2;\n\t\t\t\t\t\tif (normalizedA > aEnd - aStart) continue;\n\n\t\t\t\t\t\tconst fullness = smoothstep(0.62, 1.0, (dist - 55) / (86 - 55));\n\t\t\t\t\t\tconst waveRaw =\n\t\t\t\t\t\t\tMath.sin(dist * 0.1 - time) +\n\t\t\t\t\t\t\tMath.sin(a * 3 + time * 1.5) +\n\t\t\t\t\t\t\tMath.sin(dx * 0.05 + dy * 0.05 + time * 2);\n\t\t\t\t\t\tconst wave = smoothstep(-1.5, 1.5, waveRaw);\n\t\t\t\t\t\tconst jitter = hash(x, y);\n\n\t\t\t\t\t\tconst size =\n\t\t\t\t\t\t\tcell *\n\t\t\t\t\t\t\t((isHovered ? 0.46 : 0.34) + 0.36 * fullness + 0.26 * wave) *\n\t\t\t\t\t\t\t(0.78 + 0.42 * jitter);\n\n\t\t\t\t\t\tctx.fillRect(x - size / 2, y - size / 2, size, size);\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tctx.restore();\n\t\t\t\tstartAngle += sweep;\n\t\t\t}\n\n\t\t\tctx.restore();\n\t\t};\n\n\t\tnew AnimationFrames(draw);\n\t});\n</script>\n\n{#if compact}\n\t<div class=\"relative w-full h-full flex items-center justify-center p-2\">\n\t\t<div class=\"relative w-[130px] h-[130px]\">\n\t\t\t<canvas bind:this={canvas} class=\"w-full h-full block\"></canvas>\n\t\t</div>\n\t</div>\n{:else}\n\t<div\n\t\tclass=\"relative w-full rounded-3xl p-6 transition-colors border {theme === 'dark'\n\t\t\t? 'bg-[#181818] border-white/5 text-white'\n\t\t\t: 'bg-white border-neutral-200 text-black shadow-lg'}\"\n\t>\n\t\t<div class=\"flex items-center justify-between mb-4\">\n\t\t\t<div class=\"flex items-center gap-2\">\n\t\t\t\t<div\n\t\t\t\t\tclass=\"p-2 rounded-xl {theme === 'dark'\n\t\t\t\t\t\t? 'bg-blue-500/10 text-blue-400'\n\t\t\t\t\t\t: 'bg-blue-50 text-blue-600'}\"\n\t\t\t\t>\n\t\t\t\t\t<Users class=\"w-4 h-4\" />\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<h4 class=\"text-sm font-bold\">Plan Distribution</h4>\n\t\t\t\t\t<p class=\"text-[11px] {theme === 'dark' ? 'text-neutral-400' : 'text-neutral-500'}\">\n\t\t\t\t\t\tDithered canvas chart with real-time spring physics\n\t\t\t\t\t</p>\n\t\t\t\t</div>\n\t\t\t</div>\n\n\t\t\t<div\n\t\t\t\tclass=\"flex items-center p-1 rounded-full border text-xs font-medium {theme === 'dark'\n\t\t\t\t\t? 'bg-[#131313] border-white/10'\n\t\t\t\t\t: 'bg-neutral-100 border-neutral-200'}\"\n\t\t\t>\n\t\t\t\t{#each PERIODS as p, idx (p.name)}\n\t\t\t\t\t<button\n\t\t\t\t\t\tonclick={() => (periodIndex = idx)}\n\t\t\t\t\t\tclass=\"px-2.5 py-1 rounded-full transition-all cursor-pointer {periodIndex === idx\n\t\t\t\t\t\t\t? 'bg-blue-600 text-white'\n\t\t\t\t\t\t\t: theme === 'dark'\n\t\t\t\t\t\t\t\t? 'text-neutral-400 hover:text-white'\n\t\t\t\t\t\t\t\t: 'text-neutral-600 hover:text-black'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{p.name}\n\t\t\t\t\t</button>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t</div>\n\n\t\t<div class=\"flex flex-col sm:flex-row items-center gap-6\">\n\t\t\t<div class=\"relative w-[180px] h-[180px] shrink-0\">\n\t\t\t\t<canvas bind:this={canvas} class=\"w-full h-full block\"></canvas>\n\t\t\t</div>\n\n\t\t\t<div class=\"flex-1 w-full space-y-2\">\n\t\t\t\t{#each PLANS as plan, idx (plan.name)}\n\t\t\t\t\t{@const val = values[idx]}\n\t\t\t\t\t{@const pct = Math.round(shares[idx] * 100)}\n\t\t\t\t\t{@const isHovered = hoverIndex === idx}\n\t\t\t\t\t<div\n\t\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\t\tonmouseenter={() => (hoverIndex = idx)}\n\t\t\t\t\t\tonmouseleave={() => (hoverIndex = null)}\n\t\t\t\t\t\tclass=\"flex items-center justify-between p-2 rounded-xl transition-all cursor-pointer border {isHovered\n\t\t\t\t\t\t\t? theme === 'dark'\n\t\t\t\t\t\t\t\t? 'bg-white/10 border-white/20'\n\t\t\t\t\t\t\t\t: 'bg-neutral-100 border-neutral-300'\n\t\t\t\t\t\t\t: 'border-transparent hover:bg-white/5'}\"\n\t\t\t\t\t>\n\t\t\t\t\t\t<div class=\"flex items-center gap-2.5\">\n\t\t\t\t\t\t\t<div class=\"w-2.5 h-2.5 rounded-full\" style=\"background-color: {plan.color}\"></div>\n\t\t\t\t\t\t\t<span class=\"text-xs font-medium\">{plan.name}</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t\t<div class=\"flex items-center gap-3 text-xs\">\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclass=\"font-semibold {theme === 'dark' ? 'text-neutral-300' : 'text-neutral-700'}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{val.toLocaleString()}\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t\t<span\n\t\t\t\t\t\t\t\tclass=\"text-[10px] w-8 text-right font-mono {theme === 'dark'\n\t\t\t\t\t\t\t\t\t? 'text-neutral-500'\n\t\t\t\t\t\t\t\t\t: 'text-neutral-400'}\"\n\t\t\t\t\t\t\t>\n\t\t\t\t\t\t\t\t{pct}%\n\t\t\t\t\t\t\t</span>\n\t\t\t\t\t\t</div>\n\t\t\t\t\t</div>\n\t\t\t\t{/each}\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n",
			"type": "registry:ui",
			"target": "amicro/DitherDonutChart.svelte"
		},
		{
			"content": "This file was ported from Amicro (React) and copied into your project by the\namicro-sv registry: https://amicro-sv.enisdev.com\n\nOriginal: https://github.com/Subhan-code/Amicro--Micro-transitions-\n\nMIT License\n\nCopyright (c) 2026 SYED SUBHAN UDDIN\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n",
			"type": "registry:file",
			"target": "amicro/LICENSE"
		}
	]
}