{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"Persistent WebGL shader transition wrapper that swaps content under a soft chromatic blur wash.","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 interface ChromaBlurTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 1040;\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;\n#define PI 3.14159265359\n\nfloat hash(vec2 p){ return fract(sin(dot(p, vec2(41.0, 289.0))) * 45758.5453); }\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}\n\nvoid main(){\n  vec2 uv = gl_FragCoord.xy / uRes;\n  float p = smoothstep(0.0, 1.0, uProgress);\n  float field = uv.x * 0.62 + uv.y * 0.38;\n  float n = noise(uv * 10.0 + vec2(uTime * 0.28, -uTime * 0.18));\n  float wave = sin((uv.y + n * 0.12) * 28.0 + uTime * 2.2) * 0.018;\n  float front = p * 1.42 - 0.2;\n  float d = field + wave - front;\n\n  float curtain = exp(-d * d * 34.0);\n  float softGrain = noise(uv * 34.0 + uTime * 0.18) * 0.055;\n  float chroma = exp(-(d + 0.045) * (d + 0.045) * 52.0) + exp(-(d - 0.045) * (d - 0.045) * 52.0);\n  float exitFade = smoothstep(1.0, 0.74, p);\n  float entryFade = smoothstep(0.0, 0.16, p);\n\n  vec3 brandPink = vec3(1.0, 0.32, 0.64);\n  vec3 champagne = vec3(1.0, 0.82, 0.56);\n  vec3 seafoam = vec3(0.52, 0.90, 0.78);\n  vec3 porcelain = vec3(1.0, 0.97, 0.90);\n  vec3 color = mix(brandPink, champagne, smoothstep(-0.18, 0.18, sin((uv.y + n) * PI * 2.0)));\n  color = mix(color, seafoam, smoothstep(0.2, 1.0, chroma) * 0.34);\n  color = mix(color, porcelain, 0.16);\n\n  float alpha = (curtain * 0.72 + chroma * 0.18 + softGrain * curtain) * entryFade * exitFade * uAlpha;\n  gl_FragColor = vec4(color * alpha, clamp(alpha, 0.0, 0.86));\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}\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 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 = (canvas: HTMLCanvasElement): 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 startedAt = performance.now();\n  let progress = 0;\n  let alpha = 0;\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.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  };\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 ChromaBlurTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n}: ChromaBlurTransitionProps) => {\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);\n    controllerRef.current = controller;\n    return () => {\n      playbackRef.current?.cancel();\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, []);\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    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]);\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 ChromaBlurTransition;\n","path":"index.tsx","target":"components/smoothui/chroma-blur-transition/index.tsx","type":"registry:ui"}],"name":"chroma-blur-transition","registryDependencies":[],"title":"Chroma Blur Transition","type":"registry:ui"}