{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A shared WebGL transition engine with eight shader-driven reveal variants.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport { type ReactNode, useEffect, useRef, useState } from \"react\";\n\nexport type ShaderRevealVariant =\n  | \"noise\"\n  | \"zoom\"\n  | \"circle\"\n  | \"wipe\"\n  | \"luma\"\n  | \"planetary\"\n  | \"stripes\"\n  | \"push\";\n\nexport interface ShaderRevealTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n  variant?: ShaderRevealVariant;\n}\n\nconst DEFAULT_DURATION_MS = 1080;\nconst MIDPOINT = 0.5;\nconst MAX_DPR = 2;\nconst VERTEX_SHADER =\n  \"attribute vec2 a; void main(){ gl_Position = vec4(a, 0.0, 1.0); }\";\nconst FRAGMENT_SHADER = `\nprecision mediump float;\nuniform vec2 uRes;\nuniform float uTime;\nuniform float uProgress;\nuniform float uAlpha;\nuniform float uVariant;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123); }\nfloat noise(vec2 p){\n  vec2 i = floor(p);\n  vec2 f = fract(p);\n  float a = hash(i);\n  float b = hash(i + vec2(1.0, 0.0));\n  float c = hash(i + vec2(0.0, 1.0));\n  float d = hash(i + vec2(1.0, 1.0));\n  vec2 u = f * f * (3.0 - 2.0 * f);\n  return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y;\n}\nfloat parabola(float x, float k){ return pow(4.0 * x * (1.0 - x), k); }\nmat2 rot(float a){ float s = sin(a); float c = cos(a); return mat2(c, -s, s, c); }\n\nvec4 paint(vec2 uv, float mask, vec3 color, float glow){\n  float exitFade = smoothstep(1.0, 0.76, uProgress);\n  float entryFade = smoothstep(0.0, 0.10, uProgress);\n  float alpha = clamp((mask * 0.76 + glow * 0.24) * entryFade * exitFade * uAlpha, 0.0, 0.92);\n  return vec4(color * alpha + vec3(1.0) * glow * 0.16 * uAlpha, alpha);\n}\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  vec2 aspect = vec2(uRes.x / uRes.y, 1.0);\n  vec2 p = (uv - 0.5) * aspect;\n  float progress = smoothstep(0.0, 1.0, uProgress);\n  vec3 pink = vec3(1.0, 0.20, 0.58);\n  vec3 cyan = vec3(0.12, 0.86, 1.0);\n  vec3 gold = vec3(1.0, 0.72, 0.28);\n  vec3 mint = vec3(0.45, 1.0, 0.72);\n\n  float mask = 0.0;\n  float glow = 0.0;\n  vec3 color = mix(pink, cyan, uv.x);\n\n  if (uVariant < 0.5) {\n    // Demo 1 — turbulent noise threshold. Chunky, organic gate.\n    float n1 = noise(uv * vec2(18.0, 12.0) + uTime * 0.42);\n    float n2 = noise(uv * vec2(52.0, 44.0) - uTime * 0.18);\n    float threshold = progress * 1.35 - 0.18;\n    float gate = smoothstep(threshold - 0.18, threshold + 0.18, uv.x * 0.62 + uv.y * 0.28 + n1 * 0.34 + n2 * 0.08);\n    mask = pow(gate * (1.0 - gate) * 4.0, 0.74) + step(0.74, n2) * gate * 0.22;\n    glow = exp(-pow(uv.x + n1 * 0.26 - progress, 2.0) * 26.0);\n    color = mix(vec3(1.0, 0.10, 0.55), vec3(0.18, 1.0, 0.78), n1);\n  } else if (uVariant < 1.5) {\n    // Demo 2 — zoom mix. Large central lens, not a sweep.\n    float yGate = smoothstep(0.0, 1.0, progress * 2.15 + uv.y - 1.08);\n    vec2 z = (uv - 0.5) * mix(1.75, 0.35, yGate) + 0.5;\n    float lens = 1.0 - smoothstep(0.12, 0.78, length((z - 0.5) * aspect));\n    float seam = exp(-pow(yGate - 0.5, 2.0) * 20.0);\n    mask = (lens * 0.72 + seam * 0.42) * parabola(progress, 0.42);\n    glow = seam * 0.9 + lens * 0.25;\n    color = mix(vec3(1.0, 0.80, 0.34), vec3(0.70, 0.92, 1.0), yGate);\n  } else if (uVariant < 2.5) {\n    // Demo 3 — noisy circular reveal.\n    float n = noise(uv * 9.0 + uTime * 0.25);\n    float radius = progress * 1.08 + n * 0.045;\n    float d = length(p) - radius * 0.72;\n    mask = exp(-d * d * 42.0) + smoothstep(0.08, -0.12, d) * 0.18;\n    glow = exp(-d * d * 140.0);\n    color = mix(vec3(1.0, 0.22, 0.58), vec3(1.0, 0.75, 0.32), atan(p.y, p.x) / PI * 0.5 + 0.5);\n  } else if (uVariant < 3.5) {\n    // Demo 4 — displacement wipe. Narrow noisy blade.\n    float n = noise(uv * vec2(12.0, 26.0) + vec2(uTime * 0.26, -uTime * 0.12));\n    float blade = progress * 1.32 - 0.16 + n * 0.11;\n    float edge = blade - uv.x;\n    float front = exp(-edge * edge * 120.0);\n    float fillSide = smoothstep(-0.03, 0.16, edge) * 0.18;\n    mask = front * 0.92 + fillSide;\n    glow = front;\n    color = mix(vec3(0.12, 0.92, 1.0), vec3(1.0, 1.0, 1.0), front * 0.38);\n  } else if (uVariant < 4.5) {\n    // Demo 5 — luminance displacement. Horizontal ribbons pulled vertically.\n    float lum = noise(uv * 5.0 + vec2(0.0, uTime * 0.22));\n    float displacedY = uv.y + progress * (lum - 0.5) * 0.55;\n    float ribbons = sin(displacedY * 58.0 + uTime * 1.25) * 0.5 + 0.5;\n    float fine = sin(displacedY * 132.0 - uTime * 1.8) * 0.5 + 0.5;\n    float window = smoothstep(0.03, 0.5, progress) * smoothstep(1.0, 0.72, progress);\n    mask = (smoothstep(0.58, 0.92, ribbons) * 0.74 + smoothstep(0.76, 0.98, fine) * 0.22) * window;\n    glow = smoothstep(0.84, 1.0, ribbons) * window;\n    color = mix(vec3(1.0, 0.68, 0.22), vec3(1.0, 0.20, 0.62), lum);\n  } else if (uVariant < 5.5) {\n    // Demo 6 — planetary rotated displacement. Spiral vortex.\n    vec2 disp = vec2(noise(uv * 5.5 + uTime * 0.12), noise(uv * 5.5 + 4.2 - uTime * 0.10)) - 0.5;\n    vec2 q = rot(PI * 0.25) * (p + disp * 0.44 * parabola(progress, 0.55));\n    float ang = atan(q.y, q.x);\n    float rr = length(q);\n    float arms = sin(ang * 5.0 + rr * 18.0 - uTime * 1.8);\n    float core = 1.0 - smoothstep(0.0, 0.42, rr);\n    mask = (smoothstep(0.08, 0.92, arms) * 0.72 + core * 0.38) * parabola(progress, 0.58);\n    glow = exp(-abs(arms) * 1.5) * parabola(progress, 0.85);\n    color = mix(vec3(0.10, 0.88, 1.0), vec3(1.0, 0.66, 0.18), rr + core * 0.35);\n  } else if (uVariant < 6.5) {\n    // Demo 7 — divided UV stripe displacement. Barcode shutter.\n    vec2 divided = fract(uv * vec2(42.0, 1.0));\n    float stripeCore = step(0.18, divided.x) * step(divided.x, 0.62);\n    float diagonal = uv.x + uv.y * 0.18 + divided.x * 0.06;\n    float sweep = smoothstep(-0.10, 1.12, progress * 1.32 - diagonal + 0.18);\n    float shutter = stripeCore * sweep * smoothstep(0.04, 0.56, progress) * smoothstep(1.0, 0.78, progress);\n    mask = shutter * 0.92;\n    glow = stripeCore * exp(-pow(progress - uv.x, 2.0) * 30.0) * 0.86;\n    color = mix(vec3(1.0, 0.15, 0.55), vec3(0.12, 0.88, 1.0), divided.x);\n  } else {\n    // Demo 8 — noise push/pull. Vertical flow with directional pressure.\n    float hn = noise(uv * uRes / 115.0 + uTime * 0.12);\n    vec2 centerDir = normalize(vec2(0.5) - uv + 0.001);\n    float pull = centerDir.y * (1.0 + hn * 0.7) * parabola(progress, 0.62);\n    float bands = sin((uv.y + pull * 0.52) * 28.0 + hn * 5.0);\n    float pressure = smoothstep(-0.22, 0.96, bands) * parabola(progress, 0.62);\n    float verticalGlow = exp(-pow(uv.y - (0.5 + pull * 0.22), 2.0) * 18.0);\n    mask = pressure * 0.78 + verticalGlow * 0.22;\n    glow = verticalGlow * pressure;\n    color = mix(vec3(0.48, 1.0, 0.74), vec3(1.0, 0.22, 0.58), hn);\n  }\n\n  gl_FragColor = paint(uv, mask, color, glow);\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\ninterface Controller {\n  destroy: () => void;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n  setVariant: (variant: ShaderRevealVariant) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutQuart = (value: number) => 1 - (1 - value) ** 4;\nconst easeInOutCubic = (value: number) =>\n  value < 0.5 ? 4 * value ** 3 : 1 - (-2 * value + 2) ** 3 / 2;\n\nconst variantToUniform = (variant: ShaderRevealVariant) => {\n  switch (variant) {\n    case \"noise\":\n      return 0;\n    case \"zoom\":\n      return 1;\n    case \"circle\":\n      return 2;\n    case \"wipe\":\n      return 3;\n    case \"luma\":\n      return 4;\n    case \"planetary\":\n      return 5;\n    case \"stripes\":\n      return 6;\n    default:\n      return 7;\n  }\n};\n\nconst compile = (gl: WebGLRenderingContext, type: number, source: string) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n  return shader;\n};\n\nconst resize = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DPR);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n  gl.viewport(0, 0, width, height);\n};\n\nconst createController = (\n  canvas: HTMLCanvasElement,\n  initialVariant: ShaderRevealVariant\n): Controller | null => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    premultipliedAlpha: true,\n  });\n  if (!gl) {\n    return null;\n  }\n  const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n  const program = gl.createProgram();\n  if (!(vertex && fragment && program)) {\n    return null;\n  }\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  const buffer = gl.createBuffer();\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, STRIP, gl.STATIC_DRAW);\n  const position = gl.getAttribLocation(program, \"a\");\n  gl.enableVertexAttribArray(position);\n  gl.vertexAttribPointer(position, 2, gl.FLOAT, false, 0, 0);\n  gl.enable(gl.BLEND);\n  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);\n\n  const uRes = gl.getUniformLocation(program, \"uRes\");\n  const uTime = gl.getUniformLocation(program, \"uTime\");\n  const uProgress = gl.getUniformLocation(program, \"uProgress\");\n  const uAlpha = gl.getUniformLocation(program, \"uAlpha\");\n  const uVariant = gl.getUniformLocation(program, \"uVariant\");\n  const startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\n  let variant = initialVariant;\n  let frame = 0;\n  let destroyed = false;\n\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n    resize(canvas, gl);\n    gl.clearColor(0, 0, 0, 0);\n    gl.clear(gl.COLOR_BUFFER_BIT);\n    gl.uniform2f(uRes, canvas.width, canvas.height);\n    gl.uniform1f(uTime, (performance.now() - startedAt) / 1000);\n    gl.uniform1f(uProgress, progress);\n    gl.uniform1f(uAlpha, alpha);\n    gl.uniform1f(uVariant, variantToUniform(variant));\n    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);\n    frame = requestAnimationFrame(tick);\n  };\n  frame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(frame);\n      gl.clear(gl.COLOR_BUFFER_BIT);\n      if (buffer) {\n        gl.deleteBuffer(buffer);\n      }\n      gl.deleteProgram(program);\n    },\n    setAlpha: (nextAlpha: number) => {\n      alpha = clamp01(nextAlpha);\n    },\n    setProgress: (nextProgress: number) => {\n      progress = clamp01(nextProgress);\n    },\n    setVariant: (nextVariant: ShaderRevealVariant) => {\n      variant = nextVariant;\n    },\n  };\n};\n\nconst play = (\n  controller: Controller,\n  duration: number,\n  onMidpoint: () => void,\n  onComplete: () => void\n): Playback => {\n  let frame = 0;\n  let cancelled = false;\n  let didSwap = false;\n  const startedAt = performance.now();\n  controller.setAlpha(1);\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n    const raw = clamp01((performance.now() - startedAt) / duration);\n    const progress = easeOutQuart(raw);\n    controller.setProgress(progress);\n    if (!(didSwap || progress < MIDPOINT)) {\n      didSwap = true;\n      onMidpoint();\n    }\n    if (raw < 1) {\n      frame = requestAnimationFrame(advance);\n      return;\n    }\n    if (!didSwap) {\n      onMidpoint();\n    }\n    const fadeStartedAt = performance.now();\n    const fade = () => {\n      if (cancelled) {\n        return;\n      }\n      const fadeRaw = clamp01((performance.now() - fadeStartedAt) / 90);\n      controller.setAlpha(1 - easeInOutCubic(fadeRaw));\n      if (fadeRaw < 1) {\n        frame = requestAnimationFrame(fade);\n        return;\n      }\n      controller.setAlpha(0);\n      controller.setProgress(0);\n      onComplete();\n    };\n    frame = requestAnimationFrame(fade);\n  };\n  frame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(frame);\n    },\n  };\n};\n\nconst ShaderRevealTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n  variant = \"noise\",\n}: ShaderRevealTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const controllerRef = useRef<Controller | null>(null);\n  const playbackRef = useRef<Playback | null>(null);\n  const previousKey = useRef(transitionKey);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isActive, setIsActive] = useState(false);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n    const controller = createController(canvas, variant);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, [variant]);\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsActive(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      onRest?.();\n      return;\n    }\n    controller.setVariant(variant);\n    setIsActive(true);\n    playbackRef.current = play(\n      controller,\n      duration,\n      () => setRenderedChildren(children),\n      () => {\n        setIsActive(false);\n        playbackRef.current = null;\n        onRest?.();\n      }\n    );\n  }, [children, duration, onRest, shouldReduceMotion, transitionKey, variant]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-transitioning={isActive ? \"true\" : \"false\"}\n    >\n      <div className=\"relative z-0\">{renderedChildren}</div>\n      <canvas\n        className={cn(\n          \"pointer-events-none absolute inset-0 z-10 h-full w-full transition-opacity duration-75\",\n          isActive && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default ShaderRevealTransition;\n","path":"index.tsx","target":"components/smoothui/shader-reveal-transition/index.tsx","type":"registry:ui"}],"name":"shader-reveal-transition","registryDependencies":[],"title":"Shader Reveal Transition","type":"registry:ui"}