{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A persistent WebGL prism sweep transition wrapper for route and state changes.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport { useReducedMotion } from \"motion/react\";\nimport {\n  type ReactNode,\n  type RefObject,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\nexport type PrismSweepDirection = \"left\" | \"right\" | \"up\" | \"down\";\nexport type PrismSweepTone = \"prism\" | \"chroma\" | \"aperture\" | \"beam\";\n\nexport interface PrismSweepTransitionProps {\n  /** Content rendered underneath the sweep. */\n  children: ReactNode;\n  /** Optional wrapper className. */\n  className?: string;\n  /** Direction the prism field travels from. */\n  direction?: PrismSweepDirection;\n  /** Total sweep duration, in milliseconds. */\n  duration?: number;\n  /** Called after the sweep finishes. */\n  onRest?: () => void;\n  /** Optional tone preset for the shader palette and field behavior. */\n  tone?: PrismSweepTone;\n  /** Key that starts a new sweep when it changes. */\n  transitionKey: string | number;\n}\n\nconst DEFAULT_DURATION_MS = 1240;\nconst OUTRO_MS = 82;\nconst CONTENT_SWAP_RATIO = 0.52;\nconst MAX_DEVICE_PIXEL_RATIO = 2;\nconst MIN_PHASE_MS = 80;\nconst HIDDEN_OPACITY = 0;\nconst VISIBLE_OPACITY = 1;\n\nconst VERTEX_SHADER = `\nattribute vec2 position;\nvarying vec2 vUv;\n\nvoid main() {\n  vUv = position * 0.5 + 0.5;\n  gl_Position = vec4(position, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT_SHADER = `\nprecision mediump float;\n\nvarying vec2 vUv;\n\nuniform vec2 uResolution;\nuniform float uClock;\nuniform float uTravel;\nuniform float uOpacity;\nuniform float uAxis;\nuniform float uForward;\nuniform float uSpan;\nuniform float uSoftness;\nuniform float uBend;\nuniform float uFacet;\nuniform float uBloom;\nuniform vec3 uInk;\nuniform vec3 uPearl;\nuniform vec3 uWarm;\nuniform vec3 uCool;\n\nfloat saturate(float value) {\n  return clamp(value, 0.0, 1.0);\n}\n\nfloat triangleWave(float value) {\n  return abs(fract(value) * 2.0 - 1.0);\n}\n\nfloat softRidge(float distance, float width) {\n  return exp(-distance * distance / max(width, 0.0001));\n}\n\nvoid main() {\n  vec2 uv = vUv;\n  vec2 pixel = 1.0 / max(uResolution, vec2(1.0));\n\n  float mainAxis = mix(uv.x, uv.y, uAxis);\n  float crossAxis = mix(uv.y, uv.x, uAxis);\n  mainAxis = mix(mainAxis, 1.0 - mainAxis, step(0.0, -uForward));\n\n  float time = uClock * 0.42;\n  float drift =\n      sin(crossAxis * 7.0 + time * 1.8) * 0.024\n    + sin(crossAxis * 17.0 - time * 1.1 + 1.6) * 0.010\n    + sin((mainAxis + crossAxis) * 11.0 + time * 0.7) * 0.006;\n  drift *= uBend;\n\n  float travel = -0.34 + uTravel * 1.68;\n  float d = mainAxis - travel - drift;\n  float ad = abs(d);\n\n  float field = 1.0 - smoothstep(uSpan, uSpan + uSoftness, ad);\n  float inner = 1.0 - smoothstep(uSpan * 0.28, uSpan, ad);\n  float frontRidge = softRidge(d - uSpan * 0.34, 0.0017 + pixel.x * 16.0);\n  float backRidge = softRidge(d + uSpan * 0.54, 0.0035 + pixel.y * 14.0) * 0.55;\n\n  float facetGrid = triangleWave(crossAxis * uFacet + mainAxis * 2.1 + time * 0.35);\n  float facetCuts = smoothstep(0.18, 0.92, facetGrid);\n  float diagonal = triangleWave((crossAxis - mainAxis * 0.36) * (uFacet * 0.44) - time * 0.2);\n  float caustic = pow(1.0 - diagonal, 4.0) * 0.42 + pow(facetCuts, 2.2) * 0.22;\n\n  float glass = saturate(field * (0.5 + inner * 0.42) + frontRidge * 0.52 + backRidge * 0.2);\n  float gateIn = smoothstep(0.02, 0.14, uTravel);\n  float gateOut = 1.0 - smoothstep(0.9, 1.0, uTravel);\n  float envelope = gateIn * gateOut;\n\n  vec3 base = mix(uCool, uWarm, saturate(crossAxis * 0.82 + drift * 4.5));\n  vec3 prism = mix(base, uPearl, inner * 0.36 + frontRidge * 0.5);\n  prism = mix(prism, uInk, smoothstep(0.55, 0.0, ad) * 0.1);\n  prism += caustic * uBloom * mix(uWarm, uPearl, 0.38);\n  prism += frontRidge * uBloom * mix(uWarm, uCool, 0.22) * 0.72;\n  prism += backRidge * uBloom * uCool * 0.7;\n\n  float vignette = smoothstep(0.0, 0.025, crossAxis) * smoothstep(1.0, 0.975, crossAxis);\n  float alpha = saturate(glass * 0.64 + frontRidge * 0.24 + backRidge * 0.14);\n  alpha *= uOpacity * envelope * vignette;\n\n  gl_FragColor = vec4(prism * alpha, alpha);\n}\n`;\n\nconst FULLSCREEN_TRIANGLES = new Float32Array([\n  -1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1,\n]);\n\nconst TONE_CONFIGS = {\n  aperture: {\n    bend: 0.42,\n    bloom: 0.86,\n    cool: [0.92, 0.78, 0.66],\n    facet: 9.5,\n    ink: [0.24, 0.16, 0.12],\n    pearl: [1, 0.96, 0.88],\n    softness: 0.076,\n    span: 0.17,\n    warm: [1, 0.62, 0.38],\n  },\n  beam: {\n    bend: 0.12,\n    bloom: 1.08,\n    cool: [0.86, 0.72, 0.5],\n    facet: 18,\n    ink: [0.2, 0.13, 0.07],\n    pearl: [1, 0.9, 0.68],\n    softness: 0.038,\n    span: 0.092,\n    warm: [1, 0.48, 0.18],\n  },\n  chroma: {\n    bend: 0.9,\n    bloom: 0.78,\n    cool: [0.38, 0.86, 0.84],\n    facet: 13.5,\n    ink: [0.18, 0.16, 0.3],\n    pearl: [0.96, 0.88, 1],\n    softness: 0.082,\n    span: 0.15,\n    warm: [1, 0.34, 0.72],\n  },\n  prism: {\n    bend: 0.76,\n    bloom: 0.58,\n    cool: [0.28, 0.78, 0.86],\n    facet: 12,\n    ink: [0.2, 0.16, 0.26],\n    pearl: [0.94, 0.82, 0.9],\n    softness: 0.07,\n    span: 0.14,\n    warm: [0.98, 0.28, 0.58],\n  },\n} as const satisfies Record<\n  PrismSweepTone,\n  {\n    bend: number;\n    bloom: number;\n    cool: readonly [number, number, number];\n    facet: number;\n    ink: readonly [number, number, number];\n    pearl: readonly [number, number, number];\n    softness: number;\n    span: number;\n    warm: readonly [number, number, number];\n  }\n>;\n\ninterface UniformMap {\n  uAxis: WebGLUniformLocation | null;\n  uBend: WebGLUniformLocation | null;\n  uBloom: WebGLUniformLocation | null;\n  uClock: WebGLUniformLocation | null;\n  uCool: WebGLUniformLocation | null;\n  uFacet: WebGLUniformLocation | null;\n  uForward: WebGLUniformLocation | null;\n  uInk: WebGLUniformLocation | null;\n  uOpacity: WebGLUniformLocation | null;\n  uPearl: WebGLUniformLocation | null;\n  uResolution: WebGLUniformLocation | null;\n  uSoftness: WebGLUniformLocation | null;\n  uSpan: WebGLUniformLocation | null;\n  uTravel: WebGLUniformLocation | null;\n  uWarm: WebGLUniformLocation | null;\n}\n\ninterface ShaderController {\n  destroy: () => void;\n  getTravel: () => number;\n  setDirection: (direction: PrismSweepDirection) => void;\n  setOpacity: (opacity: number) => void;\n  setTone: (tone: PrismSweepTone) => void;\n  setTravel: (travel: number) => void;\n}\n\ninterface ShaderControllerState {\n  animationFrame: number;\n  buffer: WebGLBuffer | null;\n  direction: PrismSweepDirection;\n  gl: WebGLRenderingContext;\n  opacity: number;\n  program: WebGLProgram;\n  startedAt: number;\n  tone: PrismSweepTone;\n  travel: number;\n  uniforms: UniformMap;\n}\n\ninterface SweepPlayback {\n  cancel: () => void;\n}\n\nconst clamp01 = (value: number) => Math.max(0, Math.min(1, value));\n\nconst directionToShader = (direction: PrismSweepDirection) => {\n  switch (direction) {\n    case \"right\":\n      return { axis: 0, forward: -1 };\n    case \"up\":\n      return { axis: 1, forward: 1 };\n    case \"down\":\n      return { axis: 1, forward: -1 };\n    default:\n      return { axis: 0, forward: 1 };\n  }\n};\n\nconst easePrismTravel = (progress: number) => {\n  const clamped = clamp01(progress);\n  return 1 - (1 - clamped) ** 3.35;\n};\n\nconst easeOpacityOut = (progress: number) => {\n  const clamped = clamp01(progress);\n  return clamped * clamped * (3 - 2 * clamped);\n};\n\nconst compileShader = (\n  gl: WebGLRenderingContext,\n  type: number,\n  source: string\n) => {\n  const shader = gl.createShader(type);\n  if (!shader) {\n    return null;\n  }\n\n  gl.shaderSource(shader, source);\n  gl.compileShader(shader);\n\n  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {\n    gl.deleteShader(shader);\n    return null;\n  }\n\n  return shader;\n};\n\nconst createProgram = (gl: WebGLRenderingContext) => {\n  const vertex = compileShader(gl, gl.VERTEX_SHADER, VERTEX_SHADER);\n  const fragment = compileShader(gl, gl.FRAGMENT_SHADER, FRAGMENT_SHADER);\n\n  if (!(vertex && fragment)) {\n    return null;\n  }\n\n  const program = gl.createProgram();\n  if (!program) {\n    return null;\n  }\n\n  gl.attachShader(program, vertex);\n  gl.attachShader(program, fragment);\n  gl.linkProgram(program);\n  gl.deleteShader(vertex);\n  gl.deleteShader(fragment);\n\n  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {\n    gl.deleteProgram(program);\n    return null;\n  }\n\n  return program;\n};\n\nconst resizeCanvas = (canvas: HTMLCanvasElement, gl: WebGLRenderingContext) => {\n  const rect = canvas.getBoundingClientRect();\n  const dpr = Math.min(window.devicePixelRatio || 1, MAX_DEVICE_PIXEL_RATIO);\n  const width = Math.max(1, Math.round(rect.width * dpr));\n  const height = Math.max(1, Math.round(rect.height * dpr));\n\n  if (canvas.width !== width || canvas.height !== height) {\n    canvas.width = width;\n    canvas.height = height;\n  }\n\n  gl.viewport(0, 0, width, height);\n};\n\nconst getUniforms = (gl: WebGLRenderingContext, program: WebGLProgram) => ({\n  uAxis: gl.getUniformLocation(program, \"uAxis\"),\n  uBend: gl.getUniformLocation(program, \"uBend\"),\n  uBloom: gl.getUniformLocation(program, \"uBloom\"),\n  uClock: gl.getUniformLocation(program, \"uClock\"),\n  uCool: gl.getUniformLocation(program, \"uCool\"),\n  uFacet: gl.getUniformLocation(program, \"uFacet\"),\n  uForward: gl.getUniformLocation(program, \"uForward\"),\n  uInk: gl.getUniformLocation(program, \"uInk\"),\n  uOpacity: gl.getUniformLocation(program, \"uOpacity\"),\n  uPearl: gl.getUniformLocation(program, \"uPearl\"),\n  uResolution: gl.getUniformLocation(program, \"uResolution\"),\n  uSoftness: gl.getUniformLocation(program, \"uSoftness\"),\n  uSpan: gl.getUniformLocation(program, \"uSpan\"),\n  uTravel: gl.getUniformLocation(program, \"uTravel\"),\n  uWarm: gl.getUniformLocation(program, \"uWarm\"),\n});\n\nconst setRgbUniform = (\n  gl: WebGLRenderingContext,\n  location: WebGLUniformLocation | null,\n  value: readonly [number, number, number]\n) => {\n  gl.uniform3f(location, value[0], value[1], value[2]);\n};\n\nconst drawShader = (\n  canvas: HTMLCanvasElement,\n  state: ShaderControllerState\n) => {\n  const { gl, uniforms } = state;\n  const directionUniforms = directionToShader(state.direction);\n  const toneConfig = TONE_CONFIGS[state.tone];\n\n  resizeCanvas(canvas, gl);\n  gl.clearColor(0, 0, 0, 0);\n  gl.clear(gl.COLOR_BUFFER_BIT);\n\n  gl.uniform2f(uniforms.uResolution, canvas.width, canvas.height);\n  gl.uniform1f(uniforms.uClock, (performance.now() - state.startedAt) / 1000);\n  gl.uniform1f(uniforms.uTravel, state.travel);\n  gl.uniform1f(uniforms.uOpacity, state.opacity);\n  gl.uniform1f(uniforms.uAxis, directionUniforms.axis);\n  gl.uniform1f(uniforms.uForward, directionUniforms.forward);\n  gl.uniform1f(uniforms.uSpan, toneConfig.span);\n  gl.uniform1f(uniforms.uSoftness, toneConfig.softness);\n  gl.uniform1f(uniforms.uBend, toneConfig.bend);\n  gl.uniform1f(uniforms.uFacet, toneConfig.facet);\n  gl.uniform1f(uniforms.uBloom, toneConfig.bloom);\n  setRgbUniform(gl, uniforms.uInk, toneConfig.ink);\n  setRgbUniform(gl, uniforms.uPearl, toneConfig.pearl);\n  setRgbUniform(gl, uniforms.uWarm, toneConfig.warm);\n  setRgbUniform(gl, uniforms.uCool, toneConfig.cool);\n  gl.drawArrays(gl.TRIANGLES, 0, 6);\n};\n\nconst createShaderController = (\n  canvas: HTMLCanvasElement,\n  direction: PrismSweepDirection,\n  tone: PrismSweepTone\n) => {\n  const gl = canvas.getContext(\"webgl\", {\n    alpha: true,\n    antialias: false,\n    depth: false,\n    premultipliedAlpha: true,\n    preserveDrawingBuffer: false,\n    stencil: false,\n  });\n\n  if (!gl) {\n    return null;\n  }\n\n  const program = createProgram(gl);\n  if (!program) {\n    return null;\n  }\n\n  const buffer = gl.createBuffer();\n  const position = gl.getAttribLocation(program, \"position\");\n\n  // biome-ignore lint/correctness/useHookAtTopLevel: WebGLRenderingContext.useProgram is not a React hook.\n  gl.useProgram(program);\n  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);\n  gl.bufferData(gl.ARRAY_BUFFER, FULLSCREEN_TRIANGLES, gl.STATIC_DRAW);\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 state: ShaderControllerState = {\n    animationFrame: 0,\n    buffer,\n    direction,\n    gl,\n    opacity: HIDDEN_OPACITY,\n    program,\n    startedAt: performance.now(),\n    tone,\n    travel: 0,\n    uniforms: getUniforms(gl, program),\n  };\n\n  let destroyed = false;\n  const tick = () => {\n    if (destroyed) {\n      return;\n    }\n\n    drawShader(canvas, state);\n    state.animationFrame = requestAnimationFrame(tick);\n  };\n\n  state.animationFrame = requestAnimationFrame(tick);\n\n  return {\n    destroy: () => {\n      destroyed = true;\n      cancelAnimationFrame(state.animationFrame);\n      state.gl.clear(state.gl.COLOR_BUFFER_BIT);\n      if (state.buffer) {\n        state.gl.deleteBuffer(state.buffer);\n      }\n      state.gl.deleteProgram(state.program);\n    },\n    getTravel: () => state.travel,\n    setDirection: (nextDirection: PrismSweepDirection) => {\n      state.direction = nextDirection;\n    },\n    setOpacity: (opacity: number) => {\n      state.opacity = clamp01(opacity);\n    },\n    setTone: (nextTone: PrismSweepTone) => {\n      state.tone = nextTone;\n    },\n    setTravel: (travel: number) => {\n      state.travel = clamp01(travel);\n    },\n  } satisfies ShaderController;\n};\n\nconst useShaderController = ({\n  canvasRef,\n  direction,\n  tone,\n}: {\n  canvasRef: RefObject<HTMLCanvasElement | null>;\n  direction: PrismSweepDirection;\n  tone: PrismSweepTone;\n}) => {\n  const controllerRef = useRef<ShaderController | null>(null);\n\n  useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) {\n      return;\n    }\n\n    const controller = createShaderController(canvas, direction, tone);\n    controllerRef.current = controller;\n\n    return () => {\n      controller?.destroy();\n      controllerRef.current = null;\n    };\n  }, [canvasRef, direction, tone]);\n\n  useEffect(() => {\n    controllerRef.current?.setDirection(direction);\n    controllerRef.current?.setTone(tone);\n  }, [direction, tone]);\n\n  return controllerRef;\n};\n\nconst playFrameSweep = (\n  controller: ShaderController,\n  {\n    direction,\n    duration,\n    midpoint,\n    onComplete,\n    onMidpoint,\n    outroMs,\n  }: {\n    direction: PrismSweepDirection;\n    duration: number;\n    midpoint: number;\n    onComplete: () => void;\n    onMidpoint: () => void;\n    outroMs: number;\n  }\n): SweepPlayback => {\n  let animationFrame = 0;\n  let midpointReached = false;\n  let cancelled = false;\n  const startedAt = performance.now();\n  const startTravel = controller.getTravel();\n  const sweepMs = Math.max(MIN_PHASE_MS, duration * (1 - startTravel));\n\n  controller.setDirection(direction);\n  controller.setOpacity(VISIBLE_OPACITY);\n\n  const finishSweep = () => {\n    const outroStartedAt = performance.now();\n\n    const fadeOut = () => {\n      if (cancelled) {\n        return;\n      }\n\n      const outroRaw = clamp01((performance.now() - outroStartedAt) / outroMs);\n      controller.setOpacity(VISIBLE_OPACITY * (1 - easeOpacityOut(outroRaw)));\n\n      if (outroRaw < 1) {\n        animationFrame = requestAnimationFrame(fadeOut);\n        return;\n      }\n\n      controller.setOpacity(HIDDEN_OPACITY);\n      controller.setTravel(0);\n      onComplete();\n    };\n\n    animationFrame = requestAnimationFrame(fadeOut);\n  };\n\n  const advance = () => {\n    if (cancelled) {\n      return;\n    }\n\n    const raw = clamp01((performance.now() - startedAt) / sweepMs);\n    const nextTravel = startTravel + (1 - startTravel) * easePrismTravel(raw);\n    controller.setTravel(nextTravel);\n\n    if (!(midpointReached || nextTravel < midpoint)) {\n      midpointReached = true;\n      onMidpoint();\n    }\n\n    if (raw < 1) {\n      animationFrame = requestAnimationFrame(advance);\n      return;\n    }\n\n    if (!midpointReached) {\n      onMidpoint();\n    }\n    controller.setTravel(1);\n    finishSweep();\n  };\n\n  animationFrame = requestAnimationFrame(advance);\n\n  return {\n    cancel: () => {\n      cancelled = true;\n      cancelAnimationFrame(animationFrame);\n    },\n  };\n};\n\n/**\n * PrismSweepTransition — a frame-level route/state transition with a custom\n * faceted caustic shader tuned for SmoothUI previews.\n */\nconst PrismSweepTransition = ({\n  children,\n  className,\n  direction = \"left\",\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  tone = \"prism\",\n  transitionKey,\n}: PrismSweepTransitionProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const canvasRef = useRef<HTMLCanvasElement>(null);\n  const playbackRef = useRef<SweepPlayback | null>(null);\n  const [renderedChildren, setRenderedChildren] = useState(children);\n  const [isSweeping, setIsSweeping] = useState(false);\n  const previousKey = useRef(transitionKey);\n  const controllerRef = useShaderController({ canvasRef, direction, tone });\n\n  useEffect(() => {\n    if (previousKey.current === transitionKey) {\n      setRenderedChildren(children);\n      return;\n    }\n\n    previousKey.current = transitionKey;\n    playbackRef.current?.cancel();\n\n    if (shouldReduceMotion) {\n      setRenderedChildren(children);\n      setIsSweeping(false);\n      onRest?.();\n      return;\n    }\n\n    const controller = controllerRef.current;\n    if (!controller) {\n      setRenderedChildren(children);\n      setIsSweeping(false);\n      onRest?.();\n      return;\n    }\n\n    setIsSweeping(true);\n    playbackRef.current = playFrameSweep(controller, {\n      direction,\n      duration: Math.max(MIN_PHASE_MS, duration),\n      midpoint: CONTENT_SWAP_RATIO,\n      onComplete: () => {\n        setIsSweeping(false);\n        playbackRef.current = null;\n        onRest?.();\n      },\n      onMidpoint: () => setRenderedChildren(children),\n      outroMs: OUTRO_MS,\n    });\n\n    return () => {\n      playbackRef.current?.cancel();\n    };\n  }, [\n    children,\n    controllerRef,\n    direction,\n    duration,\n    onRest,\n    shouldReduceMotion,\n    transitionKey,\n  ]);\n\n  return (\n    <div\n      className={cn(\n        \"relative isolate overflow-hidden rounded-2xl bg-background text-foreground\",\n        className\n      )}\n      data-sweeping={isSweeping ? \"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          isSweeping && !shouldReduceMotion ? \"opacity-100\" : \"opacity-0\"\n        )}\n        ref={canvasRef}\n      />\n    </div>\n  );\n};\n\nexport default PrismSweepTransition;\n","path":"index.tsx","target":"components/smoothui/prism-sweep-transition/index.tsx","type":"registry:ui"}],"name":"prism-sweep-transition","registryDependencies":[],"title":"Prism Sweep Transition","type":"registry:ui"}