{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "stipple",
  "type": "registry:ui",
  "title": "Stipple",
  "description": "An ordered-dither shader field: a drifting noise field quantized through an 8x8 Bayer matrix into chunky retro dot patterning, in risograph sepia duotone with a slowly slipping print register. Original OGL fragment shader, offscreen-paused and reduced-motion-safe; a ghost pointer keeps it moving when untouched.",
  "dependencies": [
    "ogl"
  ],
  "registryDependencies": [
    "https://crucibleui.com/r/ogl-canvas.json",
    "https://crucibleui.com/r/use-visibility-pause.json",
    "https://crucibleui.com/r/use-reduced-motion.json"
  ],
  "files": [
    {
      "path": "registry/default/backgrounds/stipple/stipple.tsx",
      "type": "registry:ui",
      "target": "components/crucible/stipple.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\n\nimport { cn } from \"@/lib/utils\";\nimport { OglCanvas } from \"@/lib/ogl-canvas\";\n\nexport interface StippleProps {\n  /**\n   * Duotone pair `[paper, ink]`: the dark paper tone and the light ink laid\n   * over it. Any hex strings. Defaults to risograph sepia — warm cream ink\n   * on near-black paper.\n   */\n  colors?: [string, string];\n  /** Dither cell size in canvas pixels — bigger is chunkier. Clamped 2–16. Default 4. */\n  pixelSize?: number;\n  /** Quantization steps between the two inks. Clamped 2–8. Default 4. */\n  levels?: number;\n  /** Drift speed multiplier. 1 = default. */\n  speed?: number;\n  /** Contrast / ink coverage of the underlying field. 1 = default. */\n  intensity?: number;\n  /**\n   * Idle autoplay: after ~1.5s with no real pointer, a seeded ghost pointer\n   * bends the waves on a slow drift so it never looks dead in thumbnails and\n   * heroes; the real pointer takes over instantly. Never runs under reduced\n   * motion. @default true\n   */\n  idle?: boolean;\n  /** Freeze the animation, holding the current frame. */\n  paused?: boolean;\n  /** Extra classes for the wrapper. Positioned `absolute inset-0 -z-10`. */\n  className?: string;\n}\n\nconst DEFAULT_COLORS: [string, string] = [\n  \"#181310\", // near-black warm paper\n  \"#e0cba4\", // sepia-cream ink\n];\n\n/** Hex → [r,g,b] in 0–1. Accepts #rgb / #rrggbb. */\nfunction hexToRgb(hex: string): [number, number, number] {\n  let h = hex.replace(\"#\", \"\").trim();\n  if (h.length === 3) {\n    h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];\n  }\n  const int = parseInt(h, 16);\n  if (Number.isNaN(int) || h.length !== 6) return [0.88, 0.8, 0.64];\n  return [((int >> 16) & 255) / 255, ((int >> 8) & 255) / 255, (int & 255) / 255];\n}\n\n/**\n * Ordered-dither fragment shader. A drifting fbm field is sampled once per\n * dither cell, quantized to a small number of levels, and the banding is\n * broken by an 8x8 Bayer threshold matrix — the classic retro print\n * pipeline, with the dither itself as the aesthetic. A second quantize pass\n * drifts half a cell out of register on a very slow cycle, like a print\n * pass slipping. GLSL ES 1.00.\n */\nconst FRAGMENT = /* glsl */ `\nprecision highp float;\n\nvarying vec2 vUv;\n\nuniform float uTime;\nuniform vec2 uResolution;\nuniform vec2 uPointer;\nuniform float uPointerActive; // eased 0..1 pointer (or ghost) presence\n\nuniform vec3 uPaper;\nuniform vec3 uInk;\nuniform float uPixelSize;\nuniform float uLevels;\nuniform float uSpeed;\nuniform float uIntensity;\n\n// --- compact value noise + fbm (original) ---\nfloat hash21(vec2 p) {\n  p = fract(p * vec2(217.13, 373.41));\n  p += dot(p, p + 31.73);\n  return fract(p.x * p.y);\n}\n\nfloat vnoise(vec2 p) {\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash21(i);\n  float b = hash21(i + vec2(1.0, 0.0));\n  float c = hash21(i + vec2(0.0, 1.0));\n  float d = hash21(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nfloat fbm(vec2 p) {\n  float v = 0.0;\n  float amp = 0.5;\n  mat2 m = mat2(1.7, 1.1, -1.1, 1.7);\n  for (int i = 0; i < 4; i++) {\n    v += amp * vnoise(p);\n    p = m * p;\n    amp *= 0.5;\n  }\n  return v;\n}\n\n// 8x8 Bayer threshold, computed recursively rather than tabled -- GLSL ES\n// 1.00 has no constant array initializers. Base 2x2 pattern: 0 2 / 3 1,\n// with the least-significant cell bits carrying the largest weights.\nfloat bayer8(vec2 cell) {\n  vec2 q = floor(mod(cell, 8.0));\n  float v = 0.0;\n  float w = 16.0;\n  for (int i = 0; i < 3; i++) {\n    vec2 h = mod(q, 2.0);\n    v += (2.0 * h.x + 3.0 * h.y - 4.0 * h.x * h.y) * w;\n    q = floor(q * 0.5);\n    w *= 0.25;\n  }\n  return (v + 0.5) / 64.0;\n}\n\n// The continuous field under the dither: two drifting fbm layers, a soft\n// lift where the waves bend toward the pointer, a settling vignette, then\n// contrast around the midtone.\nfloat field(vec2 uv, float aspect, float t) {\n  vec2 p = vec2(uv.x * aspect, uv.y) * 2.6;\n  float f = fbm(p + vec2(t * 0.11, -t * 0.05));\n  f += 0.4 * fbm(p * 2.3 + vec2(-t * 0.04, t * 0.08));\n  f /= 1.4;\n\n  // Pointer lift position scales with presence, settling home when nobody is there.\n  vec2 ptr = mix(vec2(0.5), uPointer, uPointerActive);\n  vec2 pd = vec2((uv.x - ptr.x) * aspect, uv.y - ptr.y);\n  f += exp(-dot(pd, pd) * 9.0) * 0.2;\n\n  vec2 vg = (uv - 0.5) * vec2(aspect, 1.0);\n  f -= 0.22 * dot(vg, vg);\n\n  return 0.5 + (f - 0.5) * (0.9 * uIntensity + 0.35);\n}\n\nfloat quantize(float f, float levels, float b) {\n  return clamp(floor(f * (levels - 1.0) + b) / (levels - 1.0), 0.0, 1.0);\n}\n\nvoid main() {\n  float aspect = uResolution.x / max(uResolution.y, 1.0);\n  float t = uTime * uSpeed;\n  float px = clamp(uPixelSize, 2.0, 16.0);\n\n  // One field sample per dither cell keeps the pattern legibly crafted.\n  vec2 cell = floor(gl_FragCoord.xy / px);\n  vec2 cuv = (cell + 0.5) * px / uResolution;\n\n  float levels = clamp(uLevels, 2.0, 8.0);\n  float b = bayer8(cell);\n\n  float q = quantize(field(cuv, aspect, t), levels, b);\n\n  // Misregistered second pass: the light ink drifts half a cell out of\n  // register on a very slow cycle, like a print pass slipping.\n  vec2 reg = (px * 0.5 / uResolution) * vec2(sin(t * 0.07), cos(t * 0.057));\n  float q2 = quantize(field(cuv + reg, aspect, t), levels, b);\n\n  vec3 col = mix(uPaper, uInk, q);\n  float hi = step(1.0 - 0.5 / (levels - 1.0), q2);\n  col = mix(col, uInk, hi * 0.22);\n\n  gl_FragColor = vec4(col, 1.0);\n}\n`;\n\n/**\n * Stipple — an ordered-dither shader field. A slowly drifting noise field is\n * crushed to a handful of levels and rendered through an 8x8 Bayer matrix at\n * a chunky, controllable cell size: living newsprint in risograph sepia\n * duotone, with a light ink that slips a half-cell out of register on a slow\n * cycle. The waves bend toward the pointer. Built on the `ogl-canvas`\n * harness: GPU-light, offscreen-paused, reduced-motion-safe. Drop it inside\n * any `relative` container; it fills and sits behind content.\n */\nexport function Stipple({\n  colors = DEFAULT_COLORS,\n  pixelSize = 4,\n  levels = 4,\n  speed = 1,\n  intensity = 1,\n  paused = false,\n  idle = true,\n  className,\n}: StippleProps) {\n  const uniforms = React.useMemo(\n    () => ({\n      uPaper: { value: hexToRgb(colors[0]) },\n      uInk: { value: hexToRgb(colors[1]) },\n      uPixelSize: { value: pixelSize },\n      uLevels: { value: levels },\n      uSpeed: { value: speed },\n      uIntensity: { value: intensity },\n    }),\n    [colors, pixelSize, levels, speed, intensity]\n  );\n\n  return (\n    <OglCanvas\n      aria-hidden\n      data-crucible=\"stipple\"\n      // DPR locked to 1 so a dither cell is a true pixel block, with\n      // image-rendering: pixelated keeping the upscale crisp on hidpi.\n      className={cn(\n        \"absolute inset-0 -z-10 [&_canvas]:[image-rendering:pixelated]\",\n        className\n      )}\n      fragment={FRAGMENT}\n      uniforms={uniforms}\n      dpr={1}\n      reducedMotionTime={6}\n      paused={paused}\n      ghost={idle}\n    />\n  );\n}\n"
    }
  ]
}