{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "dither-growth",
	"title": "Dither Growth",
	"type": "registry:ui",
	"description": "Canvas dither chart from Amicro, ported to Svelte 5 and motion-sv.",
	"dependencies": [
		"@lucide/svelte",
		"motion-sv",
		"runed"
	],
	"registryDependencies": [
		"https://amicro-sv.enisdev.com/r/dither-animated-value.json",
		"https://amicro-sv.enisdev.com/r/dither-math.json"
	],
	"files": [
		{
			"content": "<script lang=\"ts\">\n\timport { motion, useSpring, useTransform } from 'motion-sv';\n\timport { untrack } from 'svelte';\n\timport { prefersReducedMotion } from 'svelte/motion';\n\timport { Users, TrendingUp } from '@lucide/svelte';\n\timport { AnimationFrames } from 'runed';\n\timport DitherAnimatedValue from './DitherAnimatedValue.svelte';\n\timport { smoothstep, clamp } 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\tconst RANGES = [\n\t\t{ name: '7D', days: 7 },\n\t\t{ name: '14D', days: 14 },\n\t\t{ name: '30D', days: 30 },\n\t\t{ name: '90D', days: 90 }\n\t];\n\n\tfunction formatDate(offsetDays: number) {\n\t\tconst date = new Date(2026, 6, 14);\n\t\tdate.setDate(date.getDate() - offsetDays);\n\t\treturn date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });\n\t}\n\n\tlet rangeIndex = $state(2); \n\tlet canvas = $state<HTMLCanvasElement | null>(null);\n\tlet wrapper = $state<HTMLDivElement | null>(null);\n\tlet scrubIndex = $state<number | null>(null);\n\n\tconst targetX = useSpring(0, { stiffness: 650, damping: 42, mass: 0.5 });\n\tconst targetY = useSpring(0, { stiffness: 650, damping: 42, mass: 0.5 });\n\tconst xPos = useTransform(targetX, (x) => `${x}px`);\n\tconst yPos = useTransform(targetY, (y) => `${y}px`);\n\n\tconst range = $derived(RANGES[rangeIndex]);\n\n\tconst series = $derived.by(() => {\n\t\tconst days = range.days;\n\t\tconst newData: number[] = [];\n\t\tconst newDates: string[] = [];\n\t\tlet tot = 0;\n\t\tlet mv = 3;\n\t\tfor (let i = 0; i < days; i++) {\n\t\t\tconst t = i / (days - 1);\n\t\t\tconst base = 9 + t * 23;\n\t\t\tconst wave = 6 * Math.sin(i * 0.7 + 1) + 3 * Math.sin(i * 1.9);\n\t\t\tconst val = Math.max(3, Math.round(base + wave));\n\t\t\tnewData.push(val);\n\t\t\ttot += val;\n\t\t\tif (val > mv) mv = val;\n\t\t\tnewDates.push(formatDate(days - 1 - i));\n\t\t}\n\t\treturn { data: newData, dates: newDates, total: tot, maxVal: mv };\n\t});\n\n\tconst data = $derived(series.data);\n\tconst dates = $derived(series.dates);\n\tconst total = $derived(series.total);\n\tconst maxVal = $derived(series.maxVal);\n\n\tlet time = 0;\n\tlet pointerPos = { x: -100, y: -100 };\n\tlet pointerActive = false;\n\n\tlet fromData: number[] = [...untrack(() => data)];\n\tlet fromMax = untrack(() => maxVal);\n\tlet targetData: number[] = [...untrack(() => data)];\n\tlet targetMax = untrack(() => maxVal);\n\tlet morphStartTime = 0;\n\n\t$effect(() => {\n\t\tconst nextData = data;\n\t\tconst nextMax = maxVal;\n\n\t\tfromData = targetData.map((_, i) => targetData[i]);\n\t\tfromMax = targetMax;\n\n\t\ttargetData = [...nextData];\n\t\ttargetMax = nextMax;\n\n\t\tif (fromData.length !== targetData.length) {\n\t\t\tconst len = targetData.length;\n\t\t\tconst old = fromData;\n\t\t\tfromData = Array(len)\n\t\t\t\t.fill(0)\n\t\t\t\t.map((_, i) => {\n\t\t\t\t\tconst t = i / (len - 1);\n\t\t\t\t\tconst oldIdx = Math.round(t * (old.length - 1));\n\t\t\t\t\treturn old[oldIdx];\n\t\t\t\t});\n\t\t}\n\n\t\tmorphStartTime = performance.now();\n\t});\n\n\t$effect(() => {\n\t\tconst currentTheme = theme;\n\t\tconst el = canvas;\n\t\tif (!el) return;\n\n\t\tconst draw = () => {\n\t\t\ttime += 0.03;\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 rect = el.getBoundingClientRect();\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\tctx.imageSmoothingEnabled = false;\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(dpr, dpr);\n\n\t\t\tconst w = rect.width;\n\t\t\tconst h = rect.height;\n\t\t\tconst cell = Math.max(3, Math.round(w / 180));\n\n\t\t\tlet prog = 0;\n\t\t\tif (morphStartTime > 0) {\n\t\t\t\tprog = (performance.now() - morphStartTime) / 460;\n\t\t\t\tif (prog > 1) prog = 1;\n\t\t\t} else {\n\t\t\t\tprog = 1;\n\t\t\t}\n\n\t\t\tconst reducedMotion = untrack(() => prefersReducedMotion.current);\n\t\t\tif (reducedMotion) {\n\t\t\t\tprog = 1;\n\t\t\t\ttime = 0;\n\t\t\t}\n\n\t\t\tconst curMax = fromMax + (targetMax - fromMax) * prog;\n\t\t\tconst curData = targetData.map((v, i) => fromData[i] + (v - fromData[i]) * prog);\n\n\t\t\tconst px = pointerPos.x;\n\t\t\tconst py = pointerPos.y;\n\t\t\tconst isActive = pointerActive;\n\n\t\t\tfor (let x = 0; x < w; x += cell) {\n\t\t\t\tconst t = x / w;\n\t\t\t\tconst exactIdx = t * (curData.length - 1);\n\t\t\t\tconst i0 = Math.floor(exactIdx);\n\t\t\t\tconst i1 = Math.min(i0 + 1, curData.length - 1);\n\t\t\t\tconst frac = exactIdx - i0;\n\t\t\t\tconst val = curData[i0] + (curData[i1] - curData[i0]) * frac;\n\n\t\t\t\tconst headroom = 0.16 * h;\n\t\t\t\tconst plotH = h - headroom;\n\t\t\t\tconst curveY = h - plotH * (val / curMax);\n\n\t\t\t\tfor (let y = h; y >= 0; y -= cell) {\n\t\t\t\t\tctx.fillStyle =\n\t\t\t\t\t\tcurrentTheme === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)';\n\t\t\t\t\tctx.fillRect(x + 1, y + 1, cell - 1, cell - 1);\n\n\t\t\t\t\tif (y < curveY) continue;\n\n\t\t\t\t\tconst dx = x - px;\n\t\t\t\t\tconst dy = y - py;\n\t\t\t\t\tconst dist = Math.sqrt(dx * dx + dy * dy);\n\n\t\t\t\t\tlet glow = 0;\n\t\t\t\t\tif (isActive && !reducedMotion) {\n\t\t\t\t\t\tconst rad = h * 0.35;\n\t\t\t\t\t\tglow = 1 - smoothstep(0, rad, dist);\n\t\t\t\t\t}\n\n\t\t\t\t\tconst shimmer = reducedMotion ? 0 : Math.sin(y * 0.1 - time * 2) * 0.07;\n\n\t\t\t\t\tctx.fillStyle = '#FFFFFF';\n\t\t\t\t\tconst sz = cell * (0.7 + shimmer + glow * 0.3);\n\t\t\t\t\tconst alpha = 0.6 + glow * 0.4;\n\t\t\t\t\tctx.globalAlpha = alpha;\n\n\t\t\t\t\tconst offset = (cell - sz) / 2;\n\t\t\t\t\tctx.fillRect(x + offset, y + offset, sz, sz);\n\t\t\t\t\tctx.globalAlpha = 1;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tctx.restore();\n\t\t};\n\n\t\tnew AnimationFrames(draw);\n\t});\n\n\tfunction handlePointer(event: PointerEvent) {\n\t\tif (!wrapper) return;\n\t\tconst rect = wrapper.getBoundingClientRect();\n\t\tconst x = event.clientX - rect.left;\n\t\tconst y = event.clientY - rect.top;\n\n\t\tpointerPos = { x, y };\n\t\tpointerActive = true;\n\n\t\tconst w = rect.width;\n\t\tconst h = rect.height;\n\n\t\tconst t = clamp(x / w, 0, 1);\n\t\tconst idx = Math.round(t * (data.length - 1));\n\t\tscrubIndex = idx;\n\n\t\tconst actualT = data.length > 1 ? idx / (data.length - 1) : 0.5;\n\t\ttargetX.set(actualT * w);\n\n\t\tconst val = data[idx];\n\t\tconst headroom = 0.16 * h;\n\t\tconst plotH = h - headroom;\n\t\tconst curveY = h - plotH * (val / maxVal);\n\t\ttargetY.set(curveY);\n\t}\n\n\tfunction handlePointerLeave() {\n\t\tpointerActive = false;\n\t\tscrubIndex = null;\n\t}\n\n\tconst ticks = $derived([maxVal, Math.round(maxVal * 0.66), Math.round(maxVal * 0.33), 0]);\n\tconst dateLabels = $derived([\n\t\tdates[0],\n\t\tdates[Math.floor(dates.length * 0.25)],\n\t\tdates[Math.floor(dates.length * 0.5)],\n\t\tdates[Math.floor(dates.length * 0.75)],\n\t\tdates[dates.length - 1]\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-full h-[120px]\">\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\n\t\t\tclass=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6\"\n\t\t>\n\t\t\t<div class=\"flex items-center gap-3\">\n\t\t\t\t<div\n\t\t\t\t\tclass=\"p-2.5 rounded-2xl {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-5 h-5\" />\n\t\t\t\t</div>\n\t\t\t\t<div>\n\t\t\t\t\t<div class=\"flex items-baseline gap-2\">\n\t\t\t\t\t\t<span class=\"text-2xl font-bold tracking-tight\">\n\t\t\t\t\t\t\t<DitherAnimatedValue\n\t\t\t\t\t\t\t\tvalue={total}\n\t\t\t\t\t\t\t\tformat={(current) => '+' + Math.round(current).toLocaleString('en-US')}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t</span>\n\t\t\t\t\t\t<span class=\"text-xs font-semibold text-emerald-500 flex items-center gap-0.5\">\n\t\t\t\t\t\t\t<TrendingUp class=\"w-3.5 h-3.5\" /> +14%\n\t\t\t\t\t\t</span>\n\t\t\t\t\t</div>\n\t\t\t\t\t<p class=\"text-xs {theme === 'dark' ? 'text-neutral-400' : 'text-neutral-500'}\">\n\t\t\t\t\t\tMember growth over selected interval\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 RANGES as r, idx (r.name)}\n\t\t\t\t\t<button\n\t\t\t\t\t\tonclick={() => (rangeIndex = idx)}\n\t\t\t\t\t\tclass=\"px-3 py-1 rounded-full transition-all cursor-pointer {rangeIndex === idx\n\t\t\t\t\t\t\t? 'bg-blue-600 text-white shadow-sm'\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{r.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 gap-3 items-start\">\n\t\t\t<div class=\"relative w-7 h-[180px] shrink-0\">\n\t\t\t\t{#each ticks as t, i (i)}\n\t\t\t\t\t<span\n\t\t\t\t\t\tclass=\"absolute right-0 text-[10px] font-mono {theme === 'dark'\n\t\t\t\t\t\t\t? 'text-neutral-500'\n\t\t\t\t\t\t\t: 'text-neutral-400'}\"\n\t\t\t\t\t\tstyle=\"top: {(i / 3) * 82 + 8}%; transform: translateY(-50%)\"\n\t\t\t\t\t>\n\t\t\t\t\t\t{t}\n\t\t\t\t\t</span>\n\t\t\t\t{/each}\n\t\t\t</div>\n\n\t\t\t<div class=\"flex-1 min-w-0 flex flex-col\">\n\t\t\t\t<div\n\t\t\t\t\tbind:this={wrapper}\n\t\t\t\t\trole=\"presentation\"\n\t\t\t\t\tclass=\"relative h-[180px] touch-none cursor-crosshair overflow-hidden rounded-xl\"\n\t\t\t\t\tonpointermove={handlePointer}\n\t\t\t\t\tonpointerleave={handlePointerLeave}\n\t\t\t\t>\n\t\t\t\t\t<div\n\t\t\t\t\t\tclass=\"absolute inset-0 border-t border-b border-dashed border-white/10 pointer-events-none\"\n\t\t\t\t\t></div>\n\t\t\t\t\t<canvas bind:this={canvas} class=\"w-full h-full block\"></canvas>\n\n\t\t\t\t\t{#if scrubIndex !== null}\n\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\tclass=\"absolute top-0 bottom-0 w-px bg-blue-500/80 pointer-events-none z-10\"\n\t\t\t\t\t\t\tstyle={{ left: xPos }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\tclass=\"absolute w-3 h-3 -ml-[6px] -mt-[6px] rounded-full bg-blue-500 border-2 border-white shadow-lg pointer-events-none z-20\"\n\t\t\t\t\t\t\tstyle={{ left: xPos, top: yPos }}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t<motion.div\n\t\t\t\t\t\t\tclass=\"absolute -translate-x-1/2 -translate-y-full mb-3 px-2.5 py-1 rounded-lg text-xs font-semibold shadow-xl border pointer-events-none z-30 {theme ===\n\t\t\t\t\t\t\t'dark'\n\t\t\t\t\t\t\t\t? 'bg-[#131313] text-white border-white/20'\n\t\t\t\t\t\t\t\t: 'bg-black text-white border-black'}\"\n\t\t\t\t\t\t\tstyle={{ left: xPos, top: yPos }}\n\t\t\t\t\t\t>\n\t\t\t\t\t\t\t<div class=\"text-[10px] text-neutral-400 uppercase\">{dates[scrubIndex]}</div>\n\t\t\t\t\t\t\t<div>+{data[scrubIndex]} members</div>\n\t\t\t\t\t\t</motion.div>\n\t\t\t\t\t{/if}\n\t\t\t\t</div>\n\n\t\t\t\t<div class=\"flex justify-between items-center mt-2 px-1 text-[10px] font-mono opacity-60\">\n\t\t\t\t\t{#each dateLabels as lbl, idx (idx)}\n\t\t\t\t\t\t<span>{lbl}</span>\n\t\t\t\t\t{/each}\n\t\t\t\t</div>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n{/if}\n",
			"type": "registry:ui",
			"target": "amicro/DitherGrowthChart.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"
		}
	]
}