{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "textura-settle",
  "title": "Textura Settle",
  "author": "Visant Labs <visantsupply@gmail.com>",
  "description": "Areia rastelada que o cursor espalha e que assenta de volta devagar, com os sulcos reaparecendo.",
  "dependencies": [],
  "registryDependencies": [
    "utils",
    "@visant/css-color"
  ],
  "files": [
    {
      "path": "registry/visant/ui/textura-settle.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { onThemeChange, resolveCssColor } from \"@/lib/css-color\";\n\n/**\n * Textura Settle — areia rastelada, como num jardim de pedras. O ponteiro\n * passa e desfaz o rastelo, espalhando o grão; depois a areia assenta de\n * volta devagar e os sulcos reaparecem. Fundo calmo para página de pausa,\n * meditação ou tela de espera.\n *\n * Original da Visant Labs, família texturas: superfície que reage ao ponteiro,\n * um gesto só.\n *\n * ── As decisões que não se veem ────────────────────────────────────────────\n *\n * 1. **A memória é uma textura pequena.** O quanto cada lugar foi mexido mora\n *    numa grade de 48 linhas na CPU, enviada como textura de um canal com\n *    filtro linear. Pequena de propósito: a interpolação do GPU arredonda o\n *    rastro, e o custo por quadro é um `texSubImage2D` de poucos KB.\n * 2. **Mexer reembaralha o grão, não o borra.** Onde a areia foi mexida o\n *    grão sorteia de novo (outra semente de hash) e o sulco perde amplitude.\n *    Borrar daria mancha; reembaralhar dá areia revirada.\n * 3. **Assentar é decair.** A grade multiplica por um fator a cada segundo e\n *    zera abaixo de um limiar. Quando zera inteira, o laço desliga: areia\n *    parada não pede quadro.\n * 4. **O sulco ondula na horizontal.** Duas senoides baixas entortam as\n *    linhas do rastelo; reta perfeita leria como listra de interface.\n * 5. **Escala pela altura** (sulcos e grade contam linhas na altura), e\n *    com movimento reduzido o rastro aparece sob o ponteiro e some ao sair,\n *    sem animação de assentamento.\n */\n\nconst VERT = `attribute vec2 a_pos;void main(){gl_Position=vec4(a_pos,0.0,1.0);}`;\n\nconst FRAG = `\n#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\nuniform vec2 uRes;\nuniform sampler2D uMexe;\nuniform float uGrao;\nuniform float uSulcos;\nuniform vec3 uLuz;\nuniform vec3 uAreia;\nuniform vec3 uSombra;\n\nfloat hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}\n\nvoid main(){\n  vec2 px=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);\n  vec2 uv=px/uRes;\n  float m=texture2D(uMexe,uv).r;\n  vec2 q=px/uRes.y;\n  vec2 g=floor(px/uGrao);\n  float n=hash(g);\n  float n2=hash(g+floor(m*5.)*17.31);\n  float onda=q.y*uSulcos+.6*sin(q.x*1.7+.7)+.25*sin(q.x*4.3+q.y*2.1);\n  float sulco=.5+.5*sin(onda*6.2832);\n  float mexido=smoothstep(.02,.6,m);\n  float grao=mix(n,n2,mexido);\n  float tom=.5+(sulco-.5)*.6*(1.-mexido)+(grao-.5)*(.3+.55*mexido);\n  vec3 col=mix(uSombra,uAreia,smoothstep(.05,.55,tom));\n  col=mix(col,uLuz,smoothstep(.55,1.,tom));\n  gl_FragColor=vec4(col,1.);\n}\n`;\n\n/** Paradas padrão: crista, areia, fundo do sulco. Tudo token. */\nconst CORES_PADRAO = [\n  \"var(--background)\",\n  \"color-mix(in oklab, var(--brand) 22%, var(--background))\",\n  \"color-mix(in oklab, var(--brand) 60%, var(--background))\",\n];\n\nconst LINHAS = 48;\n\nexport interface TexturaSettleProps extends Omit<React.HTMLAttributes<HTMLDivElement>, \"children\"> {\n  /**\n   * Quantos sulcos de rastelo na altura.\n   * @min 4\n   * @max 40\n   * @step 1\n   */\n  ridges?: number;\n  /**\n   * Tamanho do grão, em px.\n   * @min 1\n   * @max 6\n   * @step 1\n   */\n  grain?: number;\n  /**\n   * Raio do rastro do ponteiro.\n   * @min 0\n   * @max 100\n   * @step 1\n   */\n  radius?: number;\n  /**\n   * Velocidade com que a areia assenta.\n   * @min 0\n   * @max 100\n   * @step 1\n   */\n  settle?: number;\n  /** Paradas: crista, areia e fundo do sulco. Qualquer cor CSS ou token. */\n  colors?: string[];\n}\n\nfunction compilar(gl: WebGLRenderingContext, frag: string): WebGLProgram | null {\n  const shader = (tipo: number, src: string) => {\n    const s = gl.createShader(tipo);\n    if (!s) return null;\n    gl.shaderSource(s, src);\n    gl.compileShader(s);\n    return gl.getShaderParameter(s, gl.COMPILE_STATUS) ? s : null;\n  };\n  const vs = shader(gl.VERTEX_SHADER, VERT);\n  const fs = shader(gl.FRAGMENT_SHADER, frag);\n  const p = gl.createProgram();\n  if (!vs || !fs || !p) return null;\n  gl.attachShader(p, vs);\n  gl.attachShader(p, fs);\n  gl.linkProgram(p);\n  gl.deleteShader(vs);\n  gl.deleteShader(fs);\n  return gl.getProgramParameter(p, gl.LINK_STATUS) ? p : null;\n}\n\nexport function TexturaSettle({\n  ridges = 16,\n  grain = 2,\n  radius = 45,\n  settle = 35,\n  colors,\n  className,\n  ...props\n}: TexturaSettleProps) {\n  const wrapRef = React.useRef<HTMLDivElement>(null);\n  const params = React.useRef({ ridges, grain, radius, settle });\n  params.current = { ridges, grain, radius, settle };\n  const redesenhar = React.useRef<() => void>(() => {});\n  const coresChave = (colors ?? CORES_PADRAO).join(\"|\");\n\n  React.useEffect(() => {\n    const wrap = wrapRef.current;\n    if (!wrap) return;\n    const canvas = document.createElement(\"canvas\");\n    canvas.setAttribute(\"aria-hidden\", \"true\");\n    canvas.className = \"absolute inset-0 block h-full w-full\";\n    wrap.appendChild(canvas);\n    const gl = canvas.getContext(\"webgl\", { antialias: false, alpha: false });\n    const prog = gl && compilar(gl, FRAG);\n    if (!gl || !prog) {\n      canvas.remove();\n      return;\n    }\n\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    gl.useProgram(prog);\n    const aPos = gl.getAttribLocation(prog, \"a_pos\");\n    gl.enableVertexAttribArray(aPos);\n    gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);\n    const u = (n: string) => gl.getUniformLocation(prog, n);\n    const U = {\n      res: u(\"uRes\"),\n      mexe: u(\"uMexe\"),\n      grao: u(\"uGrao\"),\n      sulcos: u(\"uSulcos\"),\n      luz: u(\"uLuz\"),\n      areia: u(\"uAreia\"),\n      sombra: u(\"uSombra\"),\n    };\n\n    const tex = gl.createTexture();\n    gl.bindTexture(gl.TEXTURE_2D, tex);\n    gl.pixelStorei(gl.UNPACK_ALIGNMENT, 1);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);\n    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);\n    gl.uniform1i(U.mexe, 0);\n\n    const mq = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    let dpr = 1;\n    let raf = 0;\n    let ultimo = 0;\n    let lx = -1;\n    let ly = -1;\n    // A memória do rastro: 48 linhas, colunas pela proporção.\n    let gw = 1;\n    let campo = new Float32Array(LINHAS);\n    let bytes = new Uint8Array(LINHAS);\n\n    const enviar = () => {\n      for (let i = 0; i < campo.length; i++) bytes[i] = Math.round(campo[i] * 255);\n      gl.bindTexture(gl.TEXTURE_2D, tex);\n      gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gw, LINHAS, gl.LUMINANCE, gl.UNSIGNED_BYTE, bytes);\n    };\n\n    const cores = () => {\n      const [a, b, c] = coresChave.split(\"|\").map((cor) => resolveCssColor(cor, wrap));\n      gl.uniform3fv(U.luz, a);\n      gl.uniform3fv(U.areia, b ?? a);\n      gl.uniform3fv(U.sombra, c ?? b ?? a);\n    };\n\n    const medir = () => {\n      dpr = Math.min(2, window.devicePixelRatio || 1);\n      canvas.width = Math.max(1, Math.round(wrap.offsetWidth * dpr));\n      canvas.height = Math.max(1, Math.round(wrap.offsetHeight * dpr));\n      gl.viewport(0, 0, canvas.width, canvas.height);\n      const novo = Math.max(1, Math.min(256, Math.round((LINHAS * canvas.width) / canvas.height)));\n      if (novo !== gw || campo.length !== novo * LINHAS) {\n        gw = novo;\n        campo = new Float32Array(gw * LINHAS);\n        bytes = new Uint8Array(gw * LINHAS);\n        gl.bindTexture(gl.TEXTURE_2D, tex);\n        gl.texImage2D(gl.TEXTURE_2D, 0, gl.LUMINANCE, gw, LINHAS, 0, gl.LUMINANCE, gl.UNSIGNED_BYTE, bytes);\n      }\n    };\n\n    const desenhar = () => {\n      const p = params.current;\n      gl.uniform2f(U.res, canvas.width, canvas.height);\n      gl.uniform1f(U.grao, Math.max(1, Math.min(6, p.grain)) * dpr);\n      gl.uniform1f(U.sulcos, Math.max(4, Math.min(40, p.ridges)));\n      gl.drawArrays(gl.TRIANGLES, 0, 3);\n    };\n\n    // O laço só vive enquanto ainda há areia por assentar.\n    const quadro = (agora: number) => {\n      const dt = ultimo ? Math.min(0.05, (agora - ultimo) / 1000) : 1 / 60;\n      ultimo = agora;\n      const fator = Math.pow(0.5 - (params.current.settle / 100) * 0.45, dt);\n      let resta = false;\n      for (let i = 0; i < campo.length; i++) {\n        const v = campo[i] * fator;\n        campo[i] = v < 0.004 ? 0 : v;\n        if (campo[i]) resta = true;\n      }\n      enviar();\n      desenhar();\n      raf = resta ? requestAnimationFrame(quadro) : 0;\n    };\n    const pedirQuadro = () => {\n      if (!raf) {\n        ultimo = 0;\n        raf = requestAnimationFrame(quadro);\n      }\n    };\n\n    redesenhar.current = () => {\n      if (!raf) desenhar();\n    };\n\n    const carimbar = (x: number, y: number) => {\n      const cel = canvas.height / LINHAS;\n      const r = (canvas.height * (0.04 + (params.current.radius / 100) * 0.14)) / cel;\n      const cx = x / cel;\n      const cy = y / cel;\n      const x0 = Math.max(0, Math.floor(cx - r));\n      const x1 = Math.min(gw - 1, Math.ceil(cx + r));\n      const y0 = Math.max(0, Math.floor(cy - r));\n      const y1 = Math.min(LINHAS - 1, Math.ceil(cy + r));\n      for (let j = y0; j <= y1; j++)\n        for (let i = x0; i <= x1; i++) {\n          const d = Math.hypot(i + 0.5 - cx, j + 0.5 - cy) / r;\n          if (d < 1) campo[j * gw + i] = Math.max(campo[j * gw + i], 1 - d * d);\n        }\n    };\n\n    const mover = (e: PointerEvent) => {\n      // Ponteiro em px locais: a vitrine escala o palco com transform.\n      const r = wrap.getBoundingClientRect();\n      const k = r.width ? wrap.offsetWidth / r.width : 1;\n      const x = (e.clientX - r.left) * k * dpr;\n      const y = (e.clientY - r.top) * k * dpr;\n      if (mq.matches) campo.fill(0);\n      // Movimento rápido pula pixels entre eventos: carimba ao longo do trajeto.\n      const passo = canvas.height / LINHAS;\n      const sem = lx < 0 || mq.matches;\n      const n = sem ? 1 : Math.min(24, Math.ceil(Math.hypot(x - lx, y - ly) / passo));\n      for (let i = 1; i <= n; i++) {\n        const t = sem ? 1 : i / n;\n        carimbar(sem ? x : lx + (x - lx) * t, sem ? y : ly + (y - ly) * t);\n      }\n      lx = x;\n      ly = y;\n      if (mq.matches) {\n        enviar();\n        desenhar();\n      } else pedirQuadro();\n    };\n    const sair = () => {\n      lx = ly = -1;\n      if (!mq.matches) return;\n      campo.fill(0);\n      enviar();\n      desenhar();\n    };\n\n    cores();\n    medir();\n    enviar();\n    desenhar();\n\n    const ro = new ResizeObserver(() => {\n      medir();\n      enviar();\n      desenhar();\n    });\n    ro.observe(wrap);\n    const offTema = onThemeChange(() => {\n      cores();\n      desenhar();\n    });\n    wrap.addEventListener(\"pointermove\", mover);\n    wrap.addEventListener(\"pointerleave\", sair);\n\n    return () => {\n      cancelAnimationFrame(raf);\n      ro.disconnect();\n      offTema();\n      wrap.removeEventListener(\"pointermove\", mover);\n      wrap.removeEventListener(\"pointerleave\", sair);\n      redesenhar.current = () => {};\n      gl.deleteTexture(tex);\n      gl.deleteBuffer(buf);\n      gl.deleteProgram(prog);\n      gl.getExtension(\"WEBGL_lose_context\")?.loseContext();\n      canvas.remove();\n    };\n  }, [coresChave]);\n\n  React.useEffect(() => {\n    redesenhar.current();\n  }, [ridges, grain, radius, settle]);\n\n  return (\n    <div\n      ref={wrapRef}\n      role=\"img\"\n      aria-label=\"Areia rastelada que o cursor espalha e que assenta de volta\"\n      className={cn(\"relative block aspect-[16/10] w-full overflow-hidden bg-background\", className)}\n      {...props}\n    />\n  );\n}\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "uso": {
      "quando": "Fundo calmo de areia rastelada que o cursor espalha e volta a assentar, em bem-estar, spa ou editorial",
      "evite": "Correnteza que nunca para: textura-current; malha de pontos leve sem WebGL, spring-dots"
    }
  },
  "categories": [
    "efeitos",
    "textura",
    "webgl",
    "fundo",
    "ponteiro",
    "areia",
    "grao",
    "rastelo"
  ],
  "type": "registry:ui"
}