{"$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 behind a cinematic aperture bloom.","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 ApertureBlurTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 760;\nconst MIDPOINT = 0.48;\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(127.1, 311.7))) * 43758.5453); }\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 r = length(p);\n  float a = atan(p.y, p.x);\n\n  float progress = smoothstep(0.0, 1.0, uProgress);\n  float radius = mix(0.05, 0.82, progress);\n  float apertureNoise = sin(a * 8.0 + uTime * 0.9) * 0.018 + sin(a * 17.0 - uTime * 0.6) * 0.006;\n  float d = r - radius - apertureNoise;\n\n  float exitFade = smoothstep(0.94, 0.58, progress);\n  float ring = exp(-d * d * 210.0) * exitFade;\n  float innerGlow = smoothstep(radius + 0.18, radius - 0.02, r) * smoothstep(0.0, 0.34, progress) * exitFade;\n  float outerFeather = smoothstep(radius + 0.22, radius - 0.04, r) * exitFade * 0.55;\n  float grain = hash(floor(uv * uRes / 3.0) + floor(uTime * 18.0)) * 0.045;\n\n  vec3 champagne = vec3(1.0, 0.82, 0.56);\n  vec3 rose = vec3(1.0, 0.34, 0.62);\n  vec3 cyan = vec3(0.62, 0.92, 1.0);\n  vec3 color = mix(champagne, rose, smoothstep(-0.4, 0.4, sin(a * 2.0 + uTime * 0.7)));\n  color = mix(color, cyan, ring * 0.38 + smoothstep(0.74, 1.0, progress) * 0.18);\n\n  float highlight = ring * (0.82 + grain) + innerGlow * 0.18 + outerFeather * 0.08;\n  float alpha = clamp(highlight * uAlpha, 0.0, 0.88);\n  gl_FragColor = vec4(color * alpha + vec3(1.0) * ring * 0.22 * uAlpha, alpha);\n}\n`;\nconst STRIP = new Float32Array([-1, -1, 1, -1, -1, 1, 1, 1]);\n\ninterface Playback {\n  cancel: () => void;\n}\n\ninterface Controller {\n  destroy: () => void;\n  getProgress: () => number;\n  setAlpha: (alpha: number) => void;\n  setProgress: (progress: number) => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\nconst easeOutCubic = (value: number) => 1 - (1 - value) ** 3;\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    getProgress: () => progress,\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 = easeOutCubic(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) / 70);\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 ApertureBlurTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n}: ApertureBlurTransitionProps) => {\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 ApertureBlurTransition;\n","path":"index.tsx","target":"components/smoothui/aperture-blur-transition/index.tsx","type":"registry:ui"}],"name":"aperture-blur-transition","registryDependencies":[],"title":"Aperture Blur Transition","type":"registry:ui"}