{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "silk-flow",
  "type": "registry:component",
  "title": "Silk Flow",
  "description": "A silk fabric that ripples slowly, with an anisotropic thread sheen that shifts with the light and follows the cursor.",
  "author": "Marcelo Treviño Velázquez <https://furin.dev>",
  "dependencies": [],
  "categories": [
    "Backgrounds"
  ],
  "docs": "Installed in components/furin/silk-flow/. Docs, live controls and props table: https://furin.dev/components/silk-flow/",
  "files": [
    {
      "path": "registry/silk-flow/SilkFlow.tsx",
      "content": "\"use client\";\nimport { useEffect, useRef } from \"react\";\nimport type { CSSProperties } from \"react\";\nimport \"./SilkFlow.css\";\n\nexport interface SilkFlowProps {\n  /** Color base de la seda (las sombras salen más oscuras). */\n  color?: string;\n  /** Color del brillo de la seda. */\n  highlight?: string;\n  /** Cuántos pliegues hay (más alto = más ondas). */\n  folds?: number;\n  /** Intensidad del brillo anisotrópico (0 a 2). */\n  sheen?: number;\n  /** Velocidad del vaivén (1 = normal). */\n  speed?: number;\n  /** Si es true, la luz sigue al cursor. */\n  interactive?: boolean;\n  /** Clase extra para el contenedor. */\n  className?: string;\n  /** Estilos extra para el contenedor. */\n  style?: CSSProperties;\n}\n\nconst VERT = `#version 300 es\nin vec2 p;\nvoid main() { gl_Position = vec4(p, 0.0, 1.0); }`;\n\nconst FRAG = `#version 300 es\nprecision highp float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform vec2 uPtr;\nuniform vec3 uMid;\nuniform vec3 uHi;\nuniform float uFolds;\nuniform float uSheen;\nout vec4 outColor;\n\nfloat hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); }\nfloat vn(vec2 p) {\n  vec2 i = floor(p), f = fract(p); f = f * f * (3.0 - 2.0 * f);\n  return mix(mix(hash(i), hash(i + vec2(1, 0)), f.x), mix(hash(i + vec2(0, 1)), hash(i + vec2(1, 1)), f.x), f.y);\n}\n// Altura de la tela: pliegues largos que se deforman unos a otros.\nfloat H(vec2 p) {\n  float t = uTime;\n  vec2 q = p + 0.34 * vec2(sin(p.y * 1.4 + t * 0.30), cos(p.x * 1.1 - t * 0.24));\n  float h = 0.0;\n  h += 0.62 * sin(dot(q, vec2(0.84, 0.54)) * uFolds * 2.0 + t * 0.40 + 1.3 * sin(q.y * 1.2 + t * 0.17));\n  h += 0.32 * sin(dot(q, vec2(-0.30, 0.95)) * uFolds * 3.3 - t * 0.31 + 1.0 * sin(q.x * 2.1 - t * 0.2));\n  h += 0.14 * sin(dot(q, vec2(0.96, -0.28)) * uFolds * 5.7 + t * 0.52);\n  return h;\n}\nvoid main() {\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float asp = uRes.x / uRes.y;\n  vec2 p = (uv - 0.5) * vec2(asp, 1.0) * 2.0;\n  float e = 0.004;\n  float h0 = H(p);\n  float hx = (H(p + vec2(e, 0.0)) - H(p - vec2(e, 0.0))) / (2.0 * e);\n  float hy = (H(p + vec2(0.0, e)) - H(p - vec2(0.0, e))) / (2.0 * e);\n  vec3 N = normalize(vec3(-hx * 0.2, -hy * 0.2, 1.0));\n  vec3 V = vec3(0.0, 0.0, 1.0);\n  vec3 L = normalize(vec3(-0.5 + uPtr.x * 0.8, 0.55 + uPtr.y * 0.6, 0.7));\n  vec3 Hh = normalize(L + V);\n  // Dirección del hilo: sigue los pliegues y varía despacio.\n  float ta = 0.6 + 0.5 * sin(p.x * 0.9 + uTime * 0.07) + h0 * 0.28;\n  vec3 T = normalize(vec3(cos(ta), sin(ta), 0.0));\n  T = normalize(T - N * dot(T, N));\n  float th = dot(T, Hh);\n  float sinTH = sqrt(max(0.0, 1.0 - th * th));\n  float nl = dot(N, L);\n  float diff = pow(clamp(nl * 1.15 - 0.12, 0.0, 1.0), 2.2);\n  vec3 col = mix(uMid * 0.07, uMid * 1.15, diff);\n  // Fibras: ruido muy estirado a lo largo del hilo.\n  vec2 tp = vec2(dot(p, T.xy), dot(p, vec2(-T.y, T.x)));\n  float fib = 0.55 * vn(vec2(tp.x * 4.0, tp.y * 260.0)) + 0.45 * vn(vec2(tp.x * 9.0, tp.y * 520.0));\n  float fm = 0.86 + 0.28 * fib;\n  float lit = smoothstep(-0.15, 0.5, nl);\n  float nh = max(dot(N, Hh), 0.0);\n  vec3 hi2 = mix(uHi, uMid * 2.4, 0.6);\n  vec3 spec = uHi * (0.5 * pow(sinTH, 70.0) * lit * fm + 0.5 * pow(nh, 55.0))\n            + hi2 * (0.32 * pow(sinTH, 10.0) * lit * fm + 0.3 * pow(nh, 9.0));\n  col += spec * uSheen * 0.85;\n  col += uMid * pow(1.0 - N.z, 2.2) * 0.22;\n  col *= 1.0 - 0.34 * dot(uv - 0.5, uv - 0.5) * 2.0;\n  col = col / (1.0 + col * 0.45) * 1.3;\n  col = pow(max(col, 0.0), vec3(0.92));\n  col += (hash(gl_FragCoord.xy + uTime) - 0.5) * 0.012;\n  outColor = vec4(col, 1.0);\n}`;\n\n// Convierte cualquier color CSS a [r,g,b] en 0..1 usando un canvas de 1px.\nlet cc: CanvasRenderingContext2D | null = null;\nconst memo = new Map<string, [number, number, number]>();\nfunction rgb(c: string): [number, number, number] {\n  const hit = memo.get(c);\n  if (hit) return hit;\n  cc ??= document.createElement(\"canvas\").getContext(\"2d\", { willReadFrequently: true });\n  if (!cc) return [1, 1, 1];\n  cc.clearRect(0, 0, 1, 1);\n  cc.fillStyle = \"#000\";\n  cc.fillStyle = c;\n  cc.fillRect(0, 0, 1, 1);\n  const d = cc.getImageData(0, 0, 1, 1).data;\n  const v: [number, number, number] = [d[0] / 255, d[1] / 255, d[2] / 255];\n  memo.set(c, v);\n  return v;\n}\n\n/** Tela de seda que ondula despacio, con brillo anisotrópico que cambia con la luz (WebGL). */\nexport function SilkFlow({\n  color = \"#2b1c78\",\n  highlight = \"#ffe3f7\",\n  folds = 1.5,\n  sheen = 1,\n  speed = 1,\n  interactive = true,\n  className = \"\",\n  style,\n}: SilkFlowProps) {\n  const ref = useRef<HTMLDivElement>(null);\n  const opts = useRef({ color, highlight, folds, sheen, speed, interactive });\n  opts.current = { color, highlight, folds, sheen, speed, interactive };\n\n  useEffect(() => {\n    const host = ref.current;\n    if (!host) return;\n    // El canvas se crea aquí (no en el JSX): así un remonte (StrictMode) nunca hereda un contexto ya perdido.\n    const cv = document.createElement(\"canvas\");\n    cv.className = \"furin-silk-flow-canvas\";\n    host.appendChild(cv);\n    const gl = cv.getContext(\"webgl2\", { antialias: false, alpha: false, powerPreference: \"low-power\" });\n    if (!gl) { cv.remove(); return; }\n    const sh = (type: number, src: string) => {\n      const s = gl.createShader(type)!;\n      gl.shaderSource(s, src);\n      gl.compileShader(s);\n      return s;\n    };\n    const prog = gl.createProgram()!;\n    const vs = sh(gl.VERTEX_SHADER, VERT), fs = sh(gl.FRAGMENT_SHADER, FRAG);\n    gl.attachShader(prog, vs);\n    gl.attachShader(prog, fs);\n    gl.linkProgram(prog);\n    if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {\n      console.error(gl.getShaderInfoLog(fs) || gl.getProgramInfoLog(prog));\n      cv.remove();\n      return;\n    }\n    gl.useProgram(prog);\n    const buf = gl.createBuffer();\n    gl.bindBuffer(gl.ARRAY_BUFFER, buf);\n    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);\n    const loc = gl.getAttribLocation(prog, \"p\");\n    gl.enableVertexAttribArray(loc);\n    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);\n    const U = (n: string) => gl.getUniformLocation(prog, n);\n    const uRes = U(\"uRes\"), uTime = U(\"uTime\"), uPtr = U(\"uPtr\");\n    const uMid = U(\"uMid\");\n    const uHi = U(\"uHi\");\n    const uFolds = U(\"uFolds\");\n    const uSheen = U(\"uSheen\");\n\n    const reduce = window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n    const ptr = { x: 0, y: 0, tx: 0, ty: 0 };\n    let time = 12, last = 0, raf = 0, visible = true;\n\n    const resize = () => {\n      const dpr = Math.min(window.devicePixelRatio || 1, 1.5);\n      const w = Math.max(2, Math.round(cv.clientWidth * dpr)), h = Math.max(2, Math.round(cv.clientHeight * dpr));\n      if (cv.width !== w || cv.height !== h) { cv.width = w; cv.height = h; }\n      gl.viewport(0, 0, w, h);\n    };\n    const draw = () => {\n      const o = opts.current;\n      gl.uniform2f(uRes, cv.width, cv.height);\n      gl.uniform1f(uTime, time);\n      gl.uniform2f(uPtr, ptr.x, ptr.y);\n      gl.uniform3f(uMid, ...rgb(o.color));\n      gl.uniform3f(uHi, ...rgb(o.highlight));\n      gl.uniform1f(uFolds, o.folds);\n      gl.uniform1f(uSheen, o.sheen);\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n    const tick = (now: number) => {\n      raf = 0;\n      const dt = Math.min(0.05, (now - (last || now)) / 1000);\n      last = now;\n      time += dt * opts.current.speed;\n      const a = 1 - Math.exp(-dt * 4);\n      ptr.x += (ptr.tx - ptr.x) * a;\n      ptr.y += (ptr.ty - ptr.y) * a;\n      draw();\n      if (visible) raf = requestAnimationFrame(tick); else last = 0;\n    };\n    const wake = () => { if (!raf && visible && !reduce) raf = requestAnimationFrame(tick); };\n    const onMove = (e: PointerEvent) => {\n      if (!opts.current.interactive) return;\n      const r = cv.getBoundingClientRect();\n      ptr.tx = ((e.clientX - r.left) / r.width) * 2 - 1;\n      ptr.ty = 1 - ((e.clientY - r.top) / r.height) * 2;\n    };\n    const onLeave = () => { ptr.tx = 0; ptr.ty = 0; };\n    host.addEventListener(\"pointermove\", onMove);\n    host.addEventListener(\"pointerleave\", onLeave);\n    const ro = new ResizeObserver(() => { resize(); draw(); });\n    ro.observe(cv);\n    const io = new IntersectionObserver(([en]) => { visible = en.isIntersecting; if (visible) wake(); }, { threshold: 0.01 });\n    io.observe(cv);\n    resize();\n    draw();\n    wake();\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      io.disconnect();\n      host.removeEventListener(\"pointermove\", onMove);\n      host.removeEventListener(\"pointerleave\", onLeave);\n      gl.deleteBuffer(buf);\n      gl.deleteProgram(prog);\n      gl.deleteShader(vs);\n      gl.deleteShader(fs);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      cv.remove();\n    };\n  }, []);\n\n  return <div ref={ref} className={`furin-silk-flow ${className}`} style={style} aria-hidden />;\n}\n\nexport default SilkFlow;\n",
      "type": "registry:component",
      "target": "components/furin/silk-flow/SilkFlow.tsx"
    },
    {
      "path": "registry/silk-flow/SilkFlow.css",
      "content": ".furin-silk-flow {\n  display: block;\n  width: 100%;\n  height: 100%;\n  min-height: 160px;\n  background: linear-gradient(135deg,#150f3a,#3a2a8f 50%,#150f3a);\n  touch-action: pan-y;\n}\n.furin-silk-flow-canvas { display: block; width: 100%; height: 100%; }\n",
      "type": "registry:file",
      "target": "components/furin/silk-flow/SilkFlow.css"
    }
  ]
}
