{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A persistent WebGL SDF blob transition wrapper for route and state changes.","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 SdfBlobVariant = \"circle\" | \"warped\" | \"merge\" | \"radial\" | \"final\";\n\nexport interface SdfBlobTransitionProps {\n  children: ReactNode;\n  className?: string;\n  duration?: number;\n  onRest?: () => void;\n  transitionKey: string | number;\n  variant?: SdfBlobVariant;\n}\n\nconst DEFAULT_DURATION_MS = 1120;\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}\n\nfloat circleSdf(vec2 p, vec2 c, float r){ return length(p - c) - r; }\nfloat smin(float a, float b, float k){\n  float h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);\n  return mix(b, a, h) - k * h * (1.0 - h);\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  float grow = smoothstep(0.0, 0.86, progress);\n  float exitFade = smoothstep(1.0, 0.72, progress);\n\n  float wobble = noise(uv * 7.0 + uTime * 0.18) * 0.035;\n  float ringNoise = noise(vec2(atan(p.y, p.x) * 2.0, uTime * 0.18)) * 0.075;\n  float dCircle = circleSdf(p, vec2(0.0), 0.04 + grow * 0.64);\n  float dWarped = circleSdf(p, vec2(0.0), 0.04 + grow * 0.62 + ringNoise * smoothstep(0.08, 0.96, progress));\n\n  float m0 = circleSdf(p, vec2(-0.26, -0.08), 0.06 + grow * 0.42 + wobble);\n  float m1 = circleSdf(p, vec2(0.24, 0.12), 0.06 + grow * 0.40 - wobble * 0.5);\n  float dMerge = smin(m0, m1, 0.18);\n\n  float angle = atan(p.y, p.x);\n  float sector3 = 2.0 * PI / 3.0;\n  float sector5 = 2.0 * PI / 5.0;\n  float a3 = floor((angle + PI) / sector3 + 0.5) * sector3 - PI;\n  float a5 = floor((angle + PI) / sector5 + 0.5) * sector5 - PI;\n  float r0 = 0.17 + grow * 0.24;\n  float r1 = 0.30 + grow * 0.18;\n  float dR0 = circleSdf(p, vec2(cos(a3), sin(a3)) * r0, 0.04 + grow * 0.22 + wobble * 0.4);\n  float dR1 = circleSdf(p, vec2(cos(a5), sin(a5)) * r1, 0.035 + grow * 0.18);\n  float dRadial = smin(dR0, dR1, 0.14);\n\n  float f0 = circleSdf(p, vec2(-0.28, -0.10), 0.08 + grow * 0.46 + wobble);\n  float f1 = circleSdf(p, vec2(0.00, 0.16), 0.06 + grow * 0.42 - wobble * 0.5);\n  float f2 = circleSdf(p, vec2(0.30, -0.06), 0.05 + grow * 0.40 + wobble * 0.6);\n  float f3 = circleSdf(p, vec2(-0.02, -0.30), 0.04 + grow * 0.34);\n  float dFinal = smin(smin(f0, f1, 0.18), smin(f2, f3, 0.16), 0.20);\n  dFinal = smin(dFinal, dRadial, 0.16);\n\n  float d = dFinal;\n  if (uVariant < 0.5) {\n    d = dCircle;\n  } else if (uVariant < 1.5) {\n    d = dWarped;\n  } else if (uVariant < 2.5) {\n    d = dMerge;\n  } else if (uVariant < 3.5) {\n    d = dRadial;\n  }\n\n  float edge = exp(-d * d * 160.0) * exitFade;\n  float fill = smoothstep(0.08, -0.10, d) * smoothstep(0.0, 0.18, progress) * exitFade;\n  float cell = hash(floor((uv + noise(uv * 11.0) * 0.01) * uRes / 7.0) + floor(uTime * 10.0));\n  float flecks = step(0.78, cell) * edge * 0.22 * smoothstep(1.5, 4.0, uVariant);\n\n  vec3 rose = vec3(1.0, 0.23, 0.58);\n  vec3 apricot = vec3(1.0, 0.70, 0.38);\n  vec3 mint = vec3(0.42, 0.94, 0.78);\n  vec3 color = mix(rose, apricot, smoothstep(-0.35, 0.35, p.x + noise(uv * 5.0) * 0.18));\n  color = mix(color, mint, smoothstep(0.12, 0.7, p.y + grow * 0.16));\n\n  float alpha = (fill * 0.24 + edge * 0.72 + flecks) * uAlpha;\n  gl_FragColor = vec4(color * alpha + vec3(1.0) * edge * 0.18 * uAlpha, clamp(alpha, 0.0, 0.9));\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: SdfBlobVariant) => 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 variantToUniform = (variant: SdfBlobVariant) => {\n  switch (variant) {\n    case \"circle\":\n      return 0;\n    case \"warped\":\n      return 1;\n    case \"merge\":\n      return 2;\n    case \"radial\":\n      return 3;\n    default:\n      return 4;\n  }\n};\n\nconst createController = (\n  canvas: HTMLCanvasElement,\n  initialVariant: SdfBlobVariant\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: SdfBlobVariant) => {\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 SdfBlobTransition = ({\n  children,\n  className,\n  duration = DEFAULT_DURATION_MS,\n  onRest,\n  transitionKey,\n  variant = \"final\",\n}: SdfBlobTransitionProps) => {\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 SdfBlobTransition;\n","path":"index.tsx","target":"components/smoothui/sdf-blob-transition/index.tsx","type":"registry:ui"}],"name":"sdf-blob-transition","registryDependencies":[],"title":"Sdf Blob Transition","type":"registry:ui"}