{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "dither-growth-chart",
  "type": "registry:block",
  "title": "Dither Growth Chart",
  "description": "60fps canvas growth line chart with pixel dither fill matrix.",
  "dependencies": [
    "motion",
    "lucide-react"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "src/components/dither-charts/DitherGrowthChart.tsx",
      "type": "registry:component",
      "target": "components/amicro/DitherGrowthChart.tsx",
      "content": "import React, { useEffect, useRef, useState, useMemo } from 'react';\r\nimport { motion, useSpring, useTransform, useReducedMotion } from 'motion/react';\r\nimport { Users, TrendingUp } from 'lucide-react';\r\nimport { useCanvasSetup } from '../../hooks/useCanvasSetup';\r\n\r\nconst RANGES = [\r\n  { name: '7D', days: 7 },\r\n  { name: '14D', days: 14 },\r\n  { name: '30D', days: 30 },\r\n  { name: '90D', days: 90 },\r\n];\r\n\r\nconst smoothstep = (min: number, max: number, value: number) => {\r\n  const x = Math.max(0, Math.min(1, (value - min) / (max - min)));\r\n  return x * x * (3 - 2 * x);\r\n};\r\n\r\nconst clamp = (val: number, min: number, max: number) => Math.max(min, Math.min(max, val));\r\n\r\nfunction AnimatedNumber({ value }: { value: number }) {\r\n  const prefersReducedMotion = useReducedMotion();\r\n  const spring = useSpring(value, { stiffness: 190, damping: 27, mass: 0.7 });\r\n  const display = useTransform(spring, (current) =>\r\n    '+' + Math.round(current).toLocaleString('en-US')\r\n  );\r\n\r\n  useEffect(() => {\r\n    if (prefersReducedMotion) {\r\n      spring.jump(value);\r\n    } else {\r\n      spring.set(value);\r\n    }\r\n  }, [value, spring, prefersReducedMotion]);\r\n\r\n  return <motion.span className=\"tabular-nums\">{display}</motion.span>;\r\n}\r\n\r\nfunction formatDate(offsetDays: number) {\r\n  const date = new Date(2026, 6, 14);\r\n  date.setDate(date.getDate() - offsetDays);\r\n  return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });\r\n}\r\n\r\ninterface DitherGrowthChartProps {\r\n  theme?: 'dark' | 'light';\r\n  compact?: boolean;\r\n}\r\n\r\nexport function DitherGrowthChart({ theme = 'dark', compact = false }: DitherGrowthChartProps) {\r\n  const [rangeIndex, setRangeIndex] = useState(2); // 30D default\r\n  const { canvasRef, rect, isVisible, reducedMotion } = useCanvasSetup();\r\n  const wrapperRef = useRef<HTMLDivElement>(null);\r\n\r\n  const range = RANGES[rangeIndex];\r\n\r\n  const [scrubIndex, setScrubIndex] = useState<number | null>(null);\r\n  const targetX = useSpring(0, { stiffness: 650, damping: 42, mass: 0.5 });\r\n  const targetY = useSpring(0, { stiffness: 650, damping: 42, mass: 0.5 });\r\n\r\n  const { data, dates, total, maxVal } = useMemo(() => {\r\n    const days = range.days;\r\n    const newData = [];\r\n    const newDates = [];\r\n    let tot = 0;\r\n    let mv = 3;\r\n    for (let i = 0; i < days; i++) {\r\n      const t = i / (days - 1);\r\n      const base = 9 + t * 23;\r\n      const wave = 6 * Math.sin(i * 0.7 + 1) + 3 * Math.sin(i * 1.9);\r\n      const val = Math.max(3, Math.round(base + wave));\r\n      newData.push(val);\r\n      tot += val;\r\n      if (val > mv) mv = val;\r\n      newDates.push(formatDate(days - 1 - i));\r\n    }\r\n    return { data: newData, dates: newDates, total: tot, maxVal: mv };\r\n  }, [rangeIndex]);\r\n\r\n  const timeRef = useRef(0);\r\n  const requestRef = useRef<number>();\r\n  const pointerPosRef = useRef({ x: -100, y: -100 });\r\n  const pointerActiveRef = useRef(false);\r\n\r\n  const fromDataRef = useRef([...data]);\r\n  const fromMaxRef = useRef(maxVal);\r\n  const targetDataRef = useRef([...data]);\r\n  const targetMaxRef = useRef(maxVal);\r\n  const morphStartTimeRef = useRef(0);\r\n\r\n  useEffect(() => {\r\n    fromDataRef.current = targetDataRef.current.map((_, i) => targetDataRef.current[i]);\r\n    fromMaxRef.current = targetMaxRef.current;\r\n\r\n    targetDataRef.current = [...data];\r\n    targetMaxRef.current = maxVal;\r\n\r\n    if (fromDataRef.current.length !== targetDataRef.current.length) {\r\n       const len = targetDataRef.current.length;\r\n       const old = fromDataRef.current;\r\n       fromDataRef.current = Array(len).fill(0).map((_, i) => {\r\n         const t = i / (len - 1);\r\n         const oldIdx = Math.round(t * (old.length - 1));\r\n         return old[oldIdx];\r\n       });\r\n    }\r\n\r\n    morphStartTimeRef.current = performance.now();\r\n  }, [data, maxVal]);\r\n\r\n  useEffect(() => {\r\n    const draw = () => {\r\n      if (!isVisible.current) {\r\n        requestRef.current = requestAnimationFrame(draw);\r\n        return;\r\n      }\r\n\r\n      const canvas = canvasRef.current;\r\n      if (!canvas) return;\r\n      const ctx = canvas.getContext('2d');\r\n      if (!ctx) return;\r\n\r\n      const { width: w, height: h } = rect.current;\r\n      if (w === 0 || h === 0) {\r\n        requestRef.current = requestAnimationFrame(draw);\r\n        return;\r\n      }\r\n\r\n      timeRef.current += reducedMotion ? 0 : 0.03;\r\n\r\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\r\n      const cell = Math.max(3, Math.round(w / 180));\r\n\r\n      ctx.save();\r\n      ctx.clearRect(0, 0, canvas.width, canvas.height);\r\n      ctx.scale(dpr, dpr);\r\n\r\n      let prog = 0;\r\n      if (reducedMotion) {\r\n        prog = 1;\r\n      } else if (morphStartTimeRef.current > 0) {\r\n        prog = (performance.now() - morphStartTimeRef.current) / 460;\r\n        if (prog > 1) prog = 1;\r\n      } else {\r\n        prog = 1;\r\n      }\r\n\r\n      const curMax = fromMaxRef.current + (targetMaxRef.current - fromMaxRef.current) * prog;\r\n      const curData = targetDataRef.current.map((v, i) => fromDataRef.current[i] + (v - fromDataRef.current[i]) * prog);\r\n\r\n      const px = pointerPosRef.current.x;\r\n      const py = pointerPosRef.current.y;\r\n      const isActive = pointerActiveRef.current;\r\n      const t2 = timeRef.current;\r\n\r\n      for (let x = 0; x < w; x += cell) {\r\n        const t = x / w;\r\n        const exactIdx = t * (curData.length - 1);\r\n        const i0 = Math.floor(exactIdx);\r\n        const i1 = Math.min(i0 + 1, curData.length - 1);\r\n        const frac = exactIdx - i0;\r\n        const val = curData[i0] + (curData[i1] - curData[i0]) * frac;\r\n\r\n        const headroom = 0.16 * h;\r\n        const plotH = h - headroom;\r\n        const curveY = h - plotH * (val / curMax);\r\n\r\n        for (let y = h; y >= 0; y -= cell) {\r\n          ctx.fillStyle = theme === 'dark' ? 'rgba(255, 255, 255, 0.03)' : 'rgba(0, 0, 0, 0.03)';\r\n          ctx.fillRect(x + 1, y + 1, cell - 1, cell - 1);\r\n\r\n          if (y < curveY) continue;\r\n\r\n          const dx = x - px;\r\n          const dy = y - py;\r\n          const dist = Math.sqrt(dx*dx + dy*dy);\r\n\r\n          let glow = 0;\r\n          if (isActive && !reducedMotion) {\r\n             const rad = h * 0.35;\r\n             glow = 1 - smoothstep(0, rad, dist);\r\n          }\r\n\r\n          const shimmer = reducedMotion ? 0 : Math.sin(y * 0.1 - t2 * 2) * 0.07;\r\n\r\n          ctx.fillStyle = '#FFFFFF';\r\n          const sz = cell * (0.7 + shimmer + glow * 0.3);\r\n          const alpha = 0.6 + glow * 0.4;\r\n          ctx.globalAlpha = alpha;\r\n\r\n          const offset = (cell - sz) / 2;\r\n          ctx.fillRect(x + offset, y + offset, sz, sz);\r\n          ctx.globalAlpha = 1;\r\n        }\r\n      }\r\n\r\n      ctx.restore();\r\n      requestRef.current = requestAnimationFrame(draw);\r\n    };\r\n\r\n    requestRef.current = requestAnimationFrame(draw);\r\n    return () => { if (requestRef.current) cancelAnimationFrame(requestRef.current); };\r\n  }, [theme, reducedMotion]);\r\n\r\n  const handlePointer = (e: React.MouseEvent | React.PointerEvent) => {\r\n    const wrapper = wrapperRef.current;\r\n    if (!wrapper) return;\r\n    const r = wrapper.getBoundingClientRect();\r\n    const x = e.clientX - r.left;\r\n    const y = e.clientY - r.top;\r\n\r\n    pointerPosRef.current = { x, y };\r\n    pointerActiveRef.current = true;\r\n\r\n    const { width: w, height: h } = rect.current;\r\n\r\n    const t = clamp(x / w, 0, 1);\r\n    const idx = Math.round(t * (data.length - 1));\r\n    setScrubIndex(idx);\r\n\r\n    const actualT = data.length > 1 ? idx / (data.length - 1) : 0.5;\r\n    targetX.set(actualT * w);\r\n\r\n    const val = data[idx];\r\n    const headroom = 0.16 * h;\r\n    const plotH = h - headroom;\r\n    const curveY = h - plotH * (val / maxVal);\r\n    targetY.set(curveY);\r\n  };\r\n\r\n  const handlePointerLeave = () => {\r\n    pointerActiveRef.current = false;\r\n    setScrubIndex(null);\r\n  };\r\n\r\n  const xPos = useTransform(targetX, x => `${x}px`);\r\n  const yPos = useTransform(targetY, y => `${y}px`);\r\n\r\n  if (compact) {\r\n    return (\r\n      <div className=\"relative w-full h-full flex items-center justify-center p-2\">\r\n        <div className=\"relative w-full h-[120px]\">\r\n          <canvas ref={canvasRef} className=\"w-full h-full block\" />\r\n        </div>\r\n      </div>\r\n    );\r\n  }\r\n\r\n  const ticks = [maxVal, Math.round(maxVal * 0.66), Math.round(maxVal * 0.33), 0];\r\n  const dateLabels = [\r\n    dates[0],\r\n    dates[Math.floor(dates.length * 0.25)],\r\n    dates[Math.floor(dates.length * 0.5)],\r\n    dates[Math.floor(dates.length * 0.75)],\r\n    dates[dates.length - 1],\r\n  ];\r\n\r\n  return (\r\n    <div className={`relative w-full rounded-3xl p-6 transition-colors border ${\r\n      theme === 'dark' ? 'bg-[#181818] border-white/5 text-white' : 'bg-white border-neutral-200 text-black shadow-lg'\r\n    }`}>\r\n      {/* Header */}\r\n      <div className=\"flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6\">\r\n        <div className=\"flex items-center gap-3\">\r\n          <div className={`p-2.5 rounded-2xl ${theme === 'dark' ? 'bg-blue-500/10 text-blue-400' : 'bg-blue-50 text-blue-600'}`}>\r\n            <Users className=\"w-5 h-5\" />\r\n          </div>\r\n          <div>\r\n            <div className=\"flex items-baseline gap-2\">\r\n              <span className=\"text-2xl font-bold tracking-tight\">\r\n                <AnimatedNumber value={total} />\r\n              </span>\r\n              <span className=\"text-xs font-semibold text-emerald-500 flex items-center gap-0.5\">\r\n                <TrendingUp className=\"w-3.5 h-3.5\" /> +14%\r\n              </span>\r\n            </div>\r\n            <p className={`text-xs ${theme === 'dark' ? 'text-neutral-400' : 'text-neutral-500'}`}>\r\n              Member growth over selected interval\r\n            </p>\r\n          </div>\r\n        </div>\r\n\r\n        {/* Range Filters */}\r\n        <div className={`flex items-center p-1 rounded-full border text-xs font-medium ${\r\n          theme === 'dark' ? 'bg-[#131313] border-white/10' : 'bg-neutral-100 border-neutral-200'\r\n        }`}>\r\n          {RANGES.map((r, idx) => (\r\n            <button\r\n              key={r.name}\r\n              onClick={() => setRangeIndex(idx)}\r\n              className={`px-3 py-1 rounded-full transition-all cursor-pointer ${\r\n                rangeIndex === idx\r\n                  ? 'bg-blue-600 text-white shadow-sm'\r\n                  : (theme === 'dark' ? 'text-neutral-400 hover:text-white' : 'text-neutral-600 hover:text-black')\r\n              }`}\r\n            >\r\n              {r.name}\r\n            </button>\r\n          ))}\r\n        </div>\r\n      </div>\r\n\r\n      {/* Main Canvas Chart Stage */}\r\n      <div className=\"flex gap-3 items-start\">\r\n        {/* Y Axis Ticks */}\r\n        <div className=\"relative w-7 h-[180px] shrink-0\">\r\n          {ticks.map((t, i) => (\r\n            <span\r\n              key={i}\r\n              className={`absolute right-0 text-[10px] font-mono ${theme === 'dark' ? 'text-neutral-500' : 'text-neutral-400'}`}\r\n              style={{ top: `${(i / 3) * 82 + 8}%`, transform: 'translateY(-50%)' }}\r\n            >\r\n              {t}\r\n            </span>\r\n          ))}\r\n        </div>\r\n\r\n        {/* Chart Canvas Container */}\r\n        <div className=\"flex-1 min-w-0 flex flex-col\">\r\n          <div\r\n            ref={wrapperRef}\r\n            className=\"relative h-[180px] touch-none cursor-crosshair overflow-hidden rounded-xl\"\r\n            onPointerMove={handlePointer}\r\n            onPointerLeave={handlePointerLeave}\r\n          >\r\n            {/* Grid overlay */}\r\n            <div className=\"absolute inset-0 border-t border-b border-dashed border-white/10 pointer-events-none\" />\r\n            <canvas ref={canvasRef} className=\"w-full h-full block\" />\r\n\r\n            {/* Interactive Scrubber Line & Tooltip */}\r\n            {scrubIndex !== null && (\r\n              <>\r\n                <motion.div\r\n                  className=\"absolute top-0 bottom-0 w-px bg-blue-500/80 pointer-events-none z-10\"\r\n                  style={{ left: xPos }}\r\n                />\r\n                <motion.div\r\n                  className=\"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\"\r\n                  style={{ left: xPos, top: yPos }}\r\n                />\r\n                <motion.div\r\n                  className={`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 ${\r\n                    theme === 'dark' ? 'bg-[#131313] text-white border-white/20' : 'bg-black text-white border-black'\r\n                  }`}\r\n                  style={{ left: xPos, top: yPos }}\r\n                >\r\n                  <div className=\"text-[10px] text-neutral-400 uppercase\">{dates[scrubIndex]}</div>\r\n                  <div>+{data[scrubIndex]} members</div>\r\n                </motion.div>\r\n              </>\r\n            )}\r\n          </div>\r\n\r\n          {/* X Axis Dates */}\r\n          <div className=\"flex justify-between items-center mt-2 px-1 text-[10px] font-mono opacity-60\">\r\n            {dateLabels.map((lbl, idx) => (\r\n              <span key={idx}>{lbl}</span>\r\n            ))}\r\n          </div>\r\n        </div>\r\n      </div>\r\n    </div>\r\n  );\r\n}\r\n"
    }
  ]
}