{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "shader-waves",
  "title": "Shader Waves",
  "author": "WebberUI",
  "description": "WebGL 流體波動背景",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/webber/shader-waves.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/** 內建配色款式：每組是由暗到亮的漸層色停（stop），會被烘焙成 256 階查找表 */\nconst PALETTES: Record<string, string[]> = {\n  // 冷色系極光：深海藍 → 青 → 靛 → 翠\n  aurora: [\"#0b1120\", \"#0ea5e9\", \"#22d3ee\", \"#818cf8\", \"#34d399\"],\n  // 暖色系日落：暗紫 → 橘 → 桃紅 → 紫\n  sunset: [\"#1a1033\", \"#f97316\", \"#ef4444\", \"#ec4899\", \"#a855f7\"],\n  // 深海：墨藍 → 青碧 → 亮青 → 淺水藍\n  ocean: [\"#05243b\", \"#0e7490\", \"#06b6d4\", \"#67e8f9\", \"#cffafe\"],\n  // 中性：接近純黑 → 中灰 → 亮灰，深淺底皆耐看\n  mono: [\"#0a0a0a\", \"#404040\", \"#a3a3a3\", \"#e5e5e5\", \"#fafafa\"],\n};\n\ntype ShaderWavesVariant = keyof typeof PALETTES;\n\n/** 噪點疊層：feTurbulence SVG 內嵌為 data URI，不需額外網路請求 */\nconst GRAIN_URL = `url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E\")`;\n\n/** #rgb / #rrggbb 轉為 [r,g,b]；解析失敗時回退為黑色 */\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  const n = Number.parseInt(h, 16);\n  if (!Number.isFinite(n)) return [0, 0, 0];\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\n/** 把色停陣列烘焙成 256 階 RGB 查找表，逐像素上色時只需一次陣列查找 */\nfunction buildLut(colors: string[]) {\n  const stops = (colors.length >= 2 ? colors : [colors[0] ?? \"#000\", \"#fff\"]).map(\n    hexToRgb,\n  );\n  const r = new Uint8Array(256);\n  const g = new Uint8Array(256);\n  const b = new Uint8Array(256);\n  const seg = stops.length - 1;\n  for (let i = 0; i < 256; i++) {\n    const f = (i / 255) * seg;\n    const idx = Math.min(seg - 1, Math.floor(f));\n    const t = f - idx;\n    const a = stops[idx];\n    const c = stops[idx + 1];\n    r[i] = a[0] + (c[0] - a[0]) * t;\n    g[i] = a[1] + (c[1] - a[1]) * t;\n    b[i] = a[2] + (c[2] - a[2]) * t;\n  }\n  return { r, g, b };\n}\n\ninterface ShaderWavesProps {\n  /** 內建配色款式：`aurora`（冷色極光）、`sunset`（暖色日落）、`ocean`（深海）、`mono`（中性灰） */\n  variant?: ShaderWavesVariant;\n  /** 自訂漸層色停（由暗到亮 2 色以上），指定後覆蓋 `variant` 的配色 */\n  colors?: string[];\n  /** 流動速度倍率，1 為基準，越大波動越快；0 或負值回退為 1 */\n  speed?: number;\n  /** 波紋尺度，越大波形越舒展、數量越少；預設 1 */\n  scale?: number;\n  /** 內部運算緩衝的長邊像素數，越低越柔和、效能越好（實際以此低解析度算完再放大模糊）；預設 128 */\n  resolution?: number;\n  /** 波場輕微跟隨滑鼠位移（僅滑鼠指標生效，觸控不作用） */\n  interactive?: boolean;\n  /** 疊加低透明度噪點，減少大面積漸層的色帶感 */\n  grain?: boolean;\n  /** 前景內容，疊在波動之上；佈局（如置中、高度）直接寫在 `className` */\n  children?: React.ReactNode;\n  className?: string;\n}\n\nexport function ShaderWaves({\n  variant = \"aurora\",\n  colors,\n  speed = 1,\n  scale = 1,\n  resolution = 128,\n  interactive = false,\n  grain = false,\n  children,\n  className,\n}: ShaderWavesProps) {\n  const reducedMotion = useReducedMotion();\n  const reduced = Boolean(reducedMotion);\n\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const rafRef = React.useRef<number | null>(null);\n  // 游標目標：座標正規化為 -1 ~ 1，active 表示滑鼠是否在容器內\n  const pointerRef = React.useRef({ x: 0, y: 0, active: false });\n\n  // 配色與尺度先解析成穩定字串，避免陣列 / 物件 identity 每次 render 都變動而重啟 effect\n  const palette = React.useMemo(\n    () => (colors && colors.length > 0 ? colors : PALETTES[variant] ?? PALETTES.aurora),\n    [colors, variant],\n  );\n  const paletteKey = palette.join(\",\");\n\n  const safeSpeed = speed > 0 ? speed : 1;\n  const safeScale = scale > 0 ? scale : 1;\n  const safeResolution = Math.max(24, Math.min(240, Math.round(resolution)));\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    const lut = buildLut(paletteKey.split(\",\"));\n    // 低解析度的離屏緩衝：逐像素在這裡算完，再以平滑放大貼到主畫布，天然形成柔化的流體感\n    const off = document.createElement(\"canvas\");\n    const offCtx = off.getContext(\"2d\");\n    if (!offCtx) return;\n\n    let width = 0;\n    let height = 0;\n    let bufW = 0;\n    let bufH = 0;\n    let image: ImageData | null = null;\n\n    /** 依容器長寬比配置緩衝尺寸，長邊固定為 resolution */\n    const allocBuffer = () => {\n      const ar = width / height || 1;\n      if (ar >= 1) {\n        bufW = safeResolution;\n        bufH = Math.max(1, Math.round(safeResolution / ar));\n      } else {\n        bufH = safeResolution;\n        bufW = Math.max(1, Math.round(safeResolution * ar));\n      }\n      off.width = bufW;\n      off.height = bufH;\n      image = offCtx.createImageData(bufW, bufH);\n    };\n\n    /** 逐像素計算：多層正弦加上座標域扭曲（domain warping），產生流動的波動場 */\n    const renderField = (t: number, ox: number, oy: number) => {\n      if (!image) return;\n      const data = image.data;\n      const aspect = bufW / bufH || 1;\n      const freq = 3 / safeScale;\n      let i = 0;\n      for (let py = 0; py < bufH; py++) {\n        const ny = ((py / bufH) * 2 - 1) * freq + oy;\n        for (let px = 0; px < bufW; px++) {\n          const nx = ((px / bufW) * 2 - 1) * aspect * freq + ox;\n          const w = Math.sin(nx * 1.3 + t * 0.7) * 0.6;\n          const v =\n            Math.sin((nx + w) * 2.1 + t * 0.5) +\n            Math.sin((ny - w) * 1.8 - t * 0.4) +\n            Math.sin((nx * 0.7 + ny * 0.9) + t * 0.6) +\n            Math.sin(Math.hypot(nx, ny) * 1.5 - t * 0.45);\n          // v 落在 [-4,4]，映射到 0 ~ 1 後查表上色\n          let val = v * 0.125 + 0.5;\n          val = val < 0 ? 0 : val > 1 ? 1 : val;\n          const idx = (val * 255) | 0;\n          data[i++] = lut.r[idx];\n          data[i++] = lut.g[idx];\n          data[i++] = lut.b[idx];\n          data[i++] = 255;\n        }\n      }\n      offCtx.putImageData(image, 0, 0);\n      ctx.imageSmoothingEnabled = true;\n      ctx.clearRect(0, 0, width, height);\n      ctx.drawImage(off, 0, 0, bufW, bufH, 0, 0, width, height);\n    };\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 2);\n      const rect = canvas.getBoundingClientRect();\n      width = rect.width;\n      height = rect.height;\n      canvas.width = Math.max(1, Math.round(width * dpr));\n      canvas.height = Math.max(1, Math.round(height * dpr));\n      // 以 CSS 像素為單位繪製，交由 transform 放大到裝置像素\n      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n      allocBuffer();\n    };\n\n    const ro = new ResizeObserver(resize);\n    ro.observe(canvas);\n    resize();\n\n    if (reduced) {\n      // 減少動態效果：畫一幀靜態波紋、不啟動 rAF、不做互動\n      renderField(0.6, 0, 0);\n      return () => ro.disconnect();\n    }\n\n    // 平滑追隨的游標位移，離開時衰減回 0\n    let curX = 0;\n    let curY = 0;\n    let start = performance.now();\n\n    const loop = (now: number) => {\n      const t = ((now - start) / 1000) * safeSpeed;\n      const p = pointerRef.current;\n      const targetX = p.active ? p.x : 0;\n      const targetY = p.active ? p.y : 0;\n      curX += (targetX - curX) * 0.06;\n      curY += (targetY - curY) * 0.06;\n      renderField(t, curX * 0.9, curY * 0.9);\n      rafRef.current = requestAnimationFrame(loop);\n    };\n    rafRef.current = requestAnimationFrame(loop);\n\n    return () => {\n      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);\n      ro.disconnect();\n    };\n  }, [paletteKey, safeSpeed, safeScale, safeResolution, reduced]);\n\n  function handlePointerMove(e: React.PointerEvent<HTMLDivElement>) {\n    if (reduced || e.pointerType !== \"mouse\") return;\n    const rect = e.currentTarget.getBoundingClientRect();\n    const p = pointerRef.current;\n    p.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n    p.y = ((e.clientY - rect.top) / rect.height) * 2 - 1;\n    p.active = true;\n  }\n\n  function handlePointerLeave() {\n    pointerRef.current.active = false;\n  }\n\n  return (\n    <div\n      onPointerMove={interactive ? handlePointerMove : undefined}\n      onPointerLeave={interactive ? handlePointerLeave : undefined}\n      className={cn(\n        \"relative isolate overflow-hidden bg-neutral-50 dark:bg-neutral-950\",\n        className,\n      )}\n    >\n      <div aria-hidden className=\"pointer-events-none absolute inset-0 -z-10\">\n        <canvas\n          ref={canvasRef}\n          className=\"absolute inset-0 h-full w-full opacity-90 dark:opacity-80\"\n        />\n        {grain && (\n          <div\n            className=\"absolute inset-0 opacity-[0.06] dark:opacity-[0.08]\"\n            style={{ backgroundImage: GRAIN_URL, backgroundSize: \"180px 180px\" }}\n          />\n        )}\n      </div>\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "background"
  ],
  "type": "registry:ui"
}