{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "liquid-metal",
  "title": "Liquid Metal",
  "author": "WebberUI",
  "description": "反光液態鉻／金屬流動表面",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/webber/liquid-metal.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion } from \"motion/react\";\nimport { cn } from \"@/lib/utils\";\n\n/** 預設鉻金屬三段色階：暗藍黑 → 板岩灰 → 近白高光 */\nconst DEFAULT_COLORS: [string, string, string] = [\n  \"#0b1220\",\n  \"#64748b\",\n  \"#e2e8f0\",\n];\n\n/** 高光反射的顏色（鉻的鏡面幾乎純白，帶一點冷色） */\nconst DEFAULT_SPECULAR = \"#f8fafc\";\n\nconst TAU = Math.PI * 2;\n\ntype RGB = [number, number, number];\n\n/** 解析 #rgb / #rrggbb 成 0~255 的三元組；失敗時回退中性灰 */\nfunction parseHex(hex: string): RGB {\n  let h = hex.trim().replace(\"#\", \"\");\n  if (h.length === 3) {\n    h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  }\n  const n = Number.parseInt(h, 16);\n  if (h.length !== 6 || Number.isNaN(n)) return [128, 128, 128];\n  return [(n >> 16) & 255, (n >> 8) & 255, n & 255];\n}\n\ninterface LiquidMetalProps {\n  /** 三段金屬色階：[陰影, 中間調, 高光]，預設鉻銀配色 */\n  colors?: [string, string, string];\n  /** 鏡面反射高光色，預設冷白 */\n  specular?: string;\n  /** 流動速度倍率 */\n  speed?: number;\n  /** 空間頻率——越大表面紋理越密 */\n  scale?: number;\n  /** 域扭曲強度——越大液態感越強、流得越亂 */\n  distortion?: number;\n  /** 反射條紋密度（鏡面亮帶數量） */\n  reflections?: number;\n  /** 滑鼠在表面拖出漣漪（僅滑鼠指標生效，觸控不作用） */\n  interactive?: boolean;\n  /** 內部繪製緩衝寬度（px）；越小越省效能，CSS 會平滑放大 */\n  resolution?: number;\n  /** 前景內容，疊在金屬表面之上；佈局（如置中）寫在 className */\n  children?: React.ReactNode;\n  className?: string;\n}\n\nexport function LiquidMetal({\n  colors = DEFAULT_COLORS,\n  specular = DEFAULT_SPECULAR,\n  speed = 1,\n  scale = 3,\n  distortion = 0.6,\n  reflections = 3,\n  interactive = false,\n  resolution = 192,\n  children,\n  className,\n}: LiquidMetalProps) {\n  const reducedMotion = useReducedMotion();\n  const canvasRef = React.useRef<HTMLCanvasElement | null>(null);\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n  // 指標位置正規化為 0~1，離開時 active 關閉，漣漪隨即淡出\n  const pointerRef = React.useRef({ x: 0.5, y: 0.5, active: false });\n\n  // 把會變動的 props 放進 ref，動畫迴圈讀最新值而不必重啟\n  const optsRef = React.useRef({\n    colors,\n    specular,\n    speed,\n    scale,\n    distortion,\n    reflections,\n    interactive,\n  });\n  optsRef.current = {\n    colors,\n    specular,\n    speed,\n    scale,\n    distortion,\n    reflections,\n    interactive,\n  };\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    const container = containerRef.current;\n    if (!canvas || !container) return;\n    const ctx = canvas.getContext(\"2d\");\n    if (!ctx) return;\n\n    let raf = 0;\n    let imageData: ImageData | null = null;\n    let bw = 0;\n    let bh = 0;\n\n    // 依容器長寬比決定緩衝尺寸，避免金屬紋理被拉伸變形\n    function resize() {\n      if (!canvas || !container || !ctx) return;\n      const rect = container.getBoundingClientRect();\n      const aspect =\n        rect.width > 0 && rect.height > 0 ? rect.height / rect.width : 0.6;\n      const w = Math.max(32, Math.round(resolution));\n      const h = Math.max(24, Math.round(w * Math.min(2, Math.max(0.3, aspect))));\n      if (w === bw && h === bh) return;\n      bw = w;\n      bh = h;\n      canvas.width = w;\n      canvas.height = h;\n      imageData = ctx.createImageData(w, h);\n    }\n\n    /** 三段色階插值：t=0 陰影、t=0.5 中間調、t=1 高光 */\n    function ramp(t: number, shadow: RGB, mid: RGB, hi: RGB, out: RGB) {\n      if (t < 0.5) {\n        const k = t * 2;\n        out[0] = shadow[0] + (mid[0] - shadow[0]) * k;\n        out[1] = shadow[1] + (mid[1] - shadow[1]) * k;\n        out[2] = shadow[2] + (mid[2] - shadow[2]) * k;\n      } else {\n        const k = (t - 0.5) * 2;\n        out[0] = mid[0] + (hi[0] - mid[0]) * k;\n        out[1] = mid[1] + (hi[1] - mid[1]) * k;\n        out[2] = mid[2] + (hi[2] - mid[2]) * k;\n      }\n    }\n\n    const tmp: RGB = [0, 0, 0];\n\n    function render(time: number) {\n      if (!ctx || !imageData) return;\n      const {\n        colors: cols,\n        specular: spec,\n        speed: sp,\n        scale: sc,\n        distortion: dist,\n        reflections: refl,\n        interactive: inter,\n      } = optsRef.current;\n\n      const shadow = parseHex(cols[0] ?? DEFAULT_COLORS[0]);\n      const mid = parseHex(cols[1] ?? DEFAULT_COLORS[1]);\n      const hi = parseHex(cols[2] ?? DEFAULT_COLORS[2]);\n      const specRGB = parseHex(spec);\n\n      const t = time * 0.001 * sp;\n      const data = imageData.data;\n      const pointer = pointerRef.current;\n      const pAmp = inter && pointer.active ? 1 : 0;\n\n      let idx = 0;\n      for (let y = 0; y < bh; y++) {\n        const fy = y / bh;\n        const sy = fy * sc;\n        for (let x = 0; x < bw; x++) {\n          const fx = x / bw;\n          const sx = fx * sc;\n\n          // 域扭曲：座標本身被正弦推移，產生液態流動的錯位感\n          const wx = sx + dist * Math.sin(sy * 1.7 + t * 0.9);\n          const wy = sy + dist * Math.cos(sx * 1.5 - t * 1.1);\n\n          // 多層正弦疊加成連續的金屬起伏場，範圍約 -1~1\n          let v =\n            Math.sin(wx * 1.3 + t) +\n            Math.sin(wy * 1.7 - t * 0.8) +\n            Math.sin((wx + wy) * 1.1 + t * 0.5);\n          v /= 3;\n\n          // 指標漣漪：以游標為中心的環狀波，隨距離高斯衰減\n          if (pAmp > 0) {\n            const dx = fx - pointer.x;\n            const dy = fy - pointer.y;\n            const d2 = dx * dx + dy * dy;\n            const fall = Math.exp(-d2 * 26);\n            v += Math.sin(Math.sqrt(d2) * 34 - t * 6) * fall * 0.6 * pAmp;\n          }\n\n          // 基底金屬明暗：把場值映到三段色階\n          ramp(0.5 + 0.5 * v, shadow, mid, hi, tmp);\n\n          // 鏡面條紋：對場值取高頻正弦再銳化，做出鉻的亮帶反射\n          const band = 0.5 + 0.5 * Math.sin(v * TAU * refl + t * 1.3);\n          const specK = band * band * band * band; // pow4，讓亮帶更集中\n\n          data[idx] = Math.min(255, tmp[0] + specRGB[0] * specK);\n          data[idx + 1] = Math.min(255, tmp[1] + specRGB[1] * specK);\n          data[idx + 2] = Math.min(255, tmp[2] + specRGB[2] * specK);\n          data[idx + 3] = 255;\n          idx += 4;\n        }\n      }\n\n      ctx.putImageData(imageData, 0, 0);\n    }\n\n    resize();\n\n    // reduced-motion：只畫一張靜態金屬表面，不啟動迴圈\n    if (reducedMotion) {\n      render(0);\n      return;\n    }\n\n    const loop = (time: number) => {\n      render(time);\n      raf = requestAnimationFrame(loop);\n    };\n    raf = requestAnimationFrame(loop);\n\n    const ro =\n      typeof ResizeObserver !== \"undefined\"\n        ? new ResizeObserver(() => resize())\n        : null;\n    ro?.observe(container);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro?.disconnect();\n    };\n  }, [reducedMotion, resolution]);\n\n  function handlePointerMove(e: React.PointerEvent<HTMLDivElement>) {\n    if (e.pointerType !== \"mouse\") return;\n    const rect = e.currentTarget.getBoundingClientRect();\n    pointerRef.current.x = (e.clientX - rect.left) / rect.width;\n    pointerRef.current.y = (e.clientY - rect.top) / rect.height;\n    pointerRef.current.active = true;\n  }\n\n  function handlePointerLeave() {\n    pointerRef.current.active = false;\n  }\n\n  return (\n    <div\n      ref={containerRef}\n      onPointerMove={interactive ? handlePointerMove : undefined}\n      onPointerLeave={interactive ? handlePointerLeave : undefined}\n      className={cn(\n        \"relative isolate overflow-hidden bg-neutral-950\",\n        className,\n      )}\n    >\n      <canvas\n        ref={canvasRef}\n        aria-hidden\n        className=\"pointer-events-none absolute inset-0 -z-10 h-full w-full\"\n      />\n      {children}\n    </div>\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "background"
  ],
  "type": "registry:ui"
}