<template>
  <div
    ref="containerRef"
    :class="['w-full h-full relative overflow-hidden', className]"
    :style="style"
    aria-label="PixelBlast interactive background"
  />
</template>

<script setup lang="ts">
import { Effect, EffectComposer, EffectPass, RenderPass } from 'postprocessing';
import * as THREE from 'three';
import { onBeforeUnmount, onMounted, ref, useTemplateRef, watch, type CSSProperties } from 'vue';

export type PixelBlastVariant = 'square' | 'circle' | 'triangle' | 'diamond';

type PixelBlastProps = {
  variant?: PixelBlastVariant;
  pixelSize?: number;
  color?: string;
  className?: string;
  style?: CSSProperties;
  antialias?: boolean;
  patternScale?: number;
  patternDensity?: number;
  liquid?: boolean;
  liquidStrength?: number;
  liquidRadius?: number;
  pixelSizeJitter?: number;
  enableRipples?: boolean;
  rippleIntensityScale?: number;
  rippleThickness?: number;
  rippleSpeed?: number;
  liquidWobbleSpeed?: number;
  autoPauseOffscreen?: boolean;
  speed?: number;
  transparent?: boolean;
  edgeFade?: number;
  noiseAmount?: number;
};

const createTouchTexture = () => {
  const size = 64;
  const canvas = document.createElement('canvas');
  canvas.width = size;
  canvas.height = size;
  const ctx = canvas.getContext('2d');
  if (!ctx) throw new Error('2D context not available');
  ctx.fillStyle = 'black';
  ctx.fillRect(0, 0, canvas.width, canvas.height);
  const texture = new THREE.Texture(canvas);
  texture.minFilter = THREE.LinearFilter;
  texture.magFilter = THREE.LinearFilter;
  texture.generateMipmaps = false;
  const trail: {
    x: number;
    y: number;
    vx: number;
    vy: number;
    force: number;
    age: number;
  }[] = [];
  let last: { x: number; y: number } | null = null;
  const maxAge = 64;
  let radius = 0.1 * size;
  const speed = 1 / maxAge;
  const clear = () => {
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
  };
  const drawPoint = (p: { x: number; y: number; vx: number; vy: number; force: number; age: number }) => {
    const pos = { x: p.x * size, y: (1 - p.y) * size };
    let intensity = 1;
    const easeOutSine = (t: number) => Math.sin((t * Math.PI) / 2);
    const easeOutQuad = (t: number) => -t * (t - 2);
    if (p.age < maxAge * 0.3) intensity = easeOutSine(p.age / (maxAge * 0.3));
    else intensity = easeOutQuad(1 - (p.age - maxAge * 0.3) / (maxAge * 0.7)) || 0;
    intensity *= p.force;
    const color = `${((p.vx + 1) / 2) * 255}, ${((p.vy + 1) / 2) * 255}, ${intensity * 255}`;
    const offset = size * 5;
    ctx.shadowOffsetX = offset;
    ctx.shadowOffsetY = offset;
    ctx.shadowBlur = radius;
    ctx.shadowColor = `rgba(${color},${0.22 * intensity})`;
    ctx.beginPath();
    ctx.fillStyle = 'rgba(255,0,0,1)';
    ctx.arc(pos.x - offset, pos.y - offset, radius, 0, Math.PI * 2);
    ctx.fill();
  };
  const addTouch = (norm: { x: number; y: number }) => {
    let force = 0;
    let vx = 0;
    let vy = 0;
    if (last) {
      const dx = norm.x - last.x;
      const dy = norm.y - last.y;
      if (dx === 0 && dy === 0) return;
      const dd = dx * dx + dy * dy;
      const d = Math.sqrt(dd);
      vx = dx / (d || 1);
      vy = dy / (d || 1);
      force = Math.min(dd * 10000, 1);
    }
    last = { x: norm.x, y: norm.y };
    trail.push({ x: norm.x, y: norm.y, age: 0, force, vx, vy });
  };
  const update = () => {
    clear();
    for (let i = trail.length - 1; i >= 0; i--) {
      const point = trail[i];
      const f = point.force * speed * (1 - point.age / maxAge);
      point.x += point.vx * f;
      point.y += point.vy * f;
      point.age++;
      if (point.age > maxAge) trail.splice(i, 1);
    }
    for (let i = 0; i < trail.length; i++) drawPoint(trail[i]);
    texture.needsUpdate = true;
  };
  return {
    canvas,
    texture,
    addTouch,
    update,
    set radiusScale(v: number) {
      radius = 0.1 * size * v;
    },
    get radiusScale() {
      return radius / (0.1 * size);
    },
    size
  };
};

const createLiquidEffect = (texture: THREE.Texture, opts?: { strength?: number; freq?: number }) => {
  const fragment = `
    uniform sampler2D uTexture;
    uniform float uStrength;
    uniform float uTime;
    uniform float uFreq;

    void mainUv(inout vec2 uv) {
      vec4 tex = texture2D(uTexture, uv);
      float vx = tex.r * 2.0 - 1.0;
      float vy = tex.g * 2.0 - 1.0;
      float intensity = tex.b;

      float wave = 0.5 + 0.5 * sin(uTime * uFreq + intensity * 6.2831853);

      float amt = uStrength * intensity * wave;

      uv += vec2(vx, vy) * amt;
    }
    `;
  return new Effect('LiquidEffect', fragment, {
    uniforms: new Map<string, THREE.Uniform>([
      ['uTexture', new THREE.Uniform(texture)],
      ['uStrength', new THREE.Uniform(opts?.strength ?? 0.025)],
      ['uTime', new THREE.Uniform(0)],
      ['uFreq', new THREE.Uniform(opts?.freq ?? 4.5)]
    ])
  });
};

const SHAPE_MAP: Record<PixelBlastVariant, number> = {
  square: 0,
  circle: 1,
  triangle: 2,
  diamond: 3
};

const VERTEX_SRC = `
void main() {
  gl_Position = vec4(position, 1.0);
}
`;

const FRAGMENT_SRC = `
precision highp float;

uniform vec3  uColor;
uniform vec2  uResolution;
uniform float uTime;
uniform float uPixelSize;
uniform float uScale;
uniform float uDensity;
uniform float uPixelJitter;
uniform int   uEnableRipples;
uniform float uRippleSpeed;
uniform float uRippleThickness;
uniform float uRippleIntensity;
uniform float uEdgeFade;

uniform int   uShapeType;
const int SHAPE_SQUARE   = 0;
const int SHAPE_CIRCLE   = 1;
const int SHAPE_TRIANGLE = 2;
const int SHAPE_DIAMOND  = 3;

const int   MAX_CLICKS = 10;

uniform vec2  uClickPos  [MAX_CLICKS];
uniform float uClickTimes[MAX_CLICKS];

out vec4 fragColor;

float Bayer2(vec2 a) {
  a = floor(a);
  return fract(a.x / 2. + a.y * a.y * .75);
}
#define Bayer4(a) (Bayer2(.5*(a))*0.25 + Bayer2(a))
#define Bayer8(a) (Bayer4(.5*(a))*0.25 + Bayer2(a))

#define FBM_OCTAVES     5
#define FBM_LACUNARITY  1.25
#define FBM_GAIN        1.0

float hash11(float n){ return fract(sin(n)*43758.5453); }

float vnoise(vec3 p){
  vec3 ip = floor(p);
  vec3 fp = fract(p);
  float n000 = hash11(dot(ip + vec3(0.0,0.0,0.0), vec3(1.0,57.0,113.0)));
  float n100 = hash11(dot(ip + vec3(1.0,0.0,0.0), vec3(1.0,57.0,113.0)));
  float n010 = hash11(dot(ip + vec3(0.0,1.0,0.0), vec3(1.0,57.0,113.0)));
  float n110 = hash11(dot(ip + vec3(1.0,1.0,0.0), vec3(1.0,57.0,113.0)));
  float n001 = hash11(dot(ip + vec3(0.0,0.0,1.0), vec3(1.0,57.0,113.0)));
  float n101 = hash11(dot(ip + vec3(1.0,0.0,1.0), vec3(1.0,57.0,113.0)));
  float n011 = hash11(dot(ip + vec3(0.0,1.0,1.0), vec3(1.0,57.0,113.0)));
  float n111 = hash11(dot(ip + vec3(1.0,1.0,1.0), vec3(1.0,57.0,113.0)));
  vec3 w = fp*fp*fp*(fp*(fp*6.0-15.0)+10.0);
  float x00 = mix(n000, n100, w.x);
  float x10 = mix(n010, n110, w.x);
  float x01 = mix(n001, n101, w.x);
  float x11 = mix(n011, n111, w.x);
  float y0  = mix(x00, x10, w.y);
  float y1  = mix(x01, x11, w.y);
  return mix(y0, y1, w.z) * 2.0 - 1.0;
}

float fbm2(vec2 uv, float t){
  vec3 p = vec3(uv * uScale, t);
  float amp = 1.0;
  float freq = 1.0;
  float sum = 1.0;
  for (int i = 0; i < FBM_OCTAVES; ++i){
    sum  += amp * vnoise(p * freq);
    freq *= FBM_LACUNARITY;
    amp  *= FBM_GAIN;
  }
  return sum * 0.5 + 0.5;
}

float maskCircle(vec2 p, float cov){
  float r = sqrt(cov) * .25;
  float d = length(p - 0.5) - r;
  float aa = 0.5 * fwidth(d);
  return cov * (1.0 - smoothstep(-aa, aa, d * 2.0));
}

float maskTriangle(vec2 p, vec2 id, float cov){
  bool flip = mod(id.x + id.y, 2.0) > 0.5;
  if (flip) p.x = 1.0 - p.x;
  float r = sqrt(cov);
  float d  = p.y - r*(1.0 - p.x);
  float aa = fwidth(d);
  return cov * clamp(0.5 - d/aa, 0.0, 1.0);
}

float maskDiamond(vec2 p, float cov){
  float r = sqrt(cov) * 0.564;
  return step(abs(p.x - 0.49) + abs(p.y - 0.49), r);
}

void main(){
  float pixelSize = uPixelSize;
  vec2 fragCoord = gl_FragCoord.xy - uResolution * .5;
  float aspectRatio = uResolution.x / uResolution.y;

  vec2 pixelId = floor(fragCoord / pixelSize);
  vec2 pixelUV = fract(fragCoord / pixelSize);

  float cellPixelSize = 8.0 * pixelSize;
  vec2 cellId = floor(fragCoord / cellPixelSize);
  vec2 cellCoord = cellId * cellPixelSize;
  vec2 uv = cellCoord / uResolution * vec2(aspectRatio, 1.0);

  float base = fbm2(uv, uTime * 0.05);
  base = base * 0.5 - 0.65;

  float feed = base + (uDensity - 0.5) * 0.3;

  float speed     = uRippleSpeed;
  float thickness = uRippleThickness;
  const float dampT     = 1.0;
  const float dampR     = 10.0;

  if (uEnableRipples == 1) {
    for (int i = 0; i < MAX_CLICKS; ++i){
      vec2 pos = uClickPos[i];
      if (pos.x < 0.0) continue;
      float cellPixelSize = 8.0 * pixelSize;
      vec2 cuv = (((pos - uResolution * .5 - cellPixelSize * .5) / (uResolution))) * vec2(aspectRatio, 1.0);
      float t = max(uTime - uClickTimes[i], 0.0);
      float r = distance(uv, cuv);
      float waveR = speed * t;
      float ring  = exp(-pow((r - waveR) / thickness, 2.0));
      float atten = exp(-dampT * t) * exp(-dampR * r);
      feed = max(feed, ring * atten * uRippleIntensity);
    }
  }

  float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
  float bw = step(0.5, feed + bayer);

  float h = fract(sin(dot(floor(fragCoord / uPixelSize), vec2(127.1, 311.7))) * 43758.5453);
  float jitterScale = 1.0 + (h - 0.5) * uPixelJitter;
  float coverage = bw * jitterScale;
  float M;
  if      (uShapeType == SHAPE_CIRCLE)   M = maskCircle (pixelUV, coverage);
  else if (uShapeType == SHAPE_TRIANGLE) M = maskTriangle(pixelUV, pixelId, coverage);
  else if (uShapeType == SHAPE_DIAMOND)  M = maskDiamond(pixelUV, coverage);
  else                                   M = coverage;

  if (uEdgeFade > 0.0) {
    vec2 norm = gl_FragCoord.xy / uResolution;
    float edge = min(min(norm.x, norm.y), min(1.0 - norm.x, 1.0 - norm.y));
    float fade = smoothstep(0.0, uEdgeFade, edge);
    M *= fade;
  }

  vec3 color = uColor;
  fragColor = vec4(color, M);
}
`;

const MAX_CLICKS = 10;

const props = withDefaults(defineProps<PixelBlastProps>(), {
  variant: 'square',
  pixelSize: 3,
  color: '#B19EEF',
  antialias: true,
  patternScale: 2,
  patternDensity: 1,
  liquid: false,
  liquidStrength: 0.1,
  liquidRadius: 1,
  pixelSizeJitter: 0,
  enableRipples: true,
  rippleIntensityScale: 1,
  rippleThickness: 0.1,
  rippleSpeed: 0.3,
  liquidWobbleSpeed: 4.5,
  autoPauseOffscreen: true,
  speed: 0.5,
  transparent: true,
  edgeFade: 0.5,
  noiseAmount: 0
});

const containerRef = useTemplateRef<HTMLDivElement>('containerRef');
const visibilityRef = ref({ visible: true });
const speedRef = ref(props.speed);

const threeRef = ref<{
  renderer: THREE.WebGLRenderer;
  scene: THREE.Scene;
  camera: THREE.OrthographicCamera;
  material: THREE.ShaderMaterial;
  clock: THREE.Clock;
  clickIx: number;
  uniforms: {
    uResolution: { value: THREE.Vector2 };
    uTime: { value: number };
    uColor: { value: THREE.Color };
    uClickPos: { value: THREE.Vector2[] };
    uClickTimes: { value: Float32Array };
    uShapeType: { value: number };
    uPixelSize: { value: number };
    uScale: { value: number };
    uDensity: { value: number };
    uPixelJitter: { value: number };
    uEnableRipples: { value: number };
    uRippleSpeed: { value: number };
    uRippleThickness: { value: number };
    uRippleIntensity: { value: number };
    uEdgeFade: { value: number };
  };
  resizeObserver?: ResizeObserver;
  raf?: number;
  quad?: THREE.Mesh<THREE.PlaneGeometry, THREE.ShaderMaterial>;
  timeOffset?: number;
  composer?: EffectComposer;
  touch?: ReturnType<typeof createTouchTexture>;
  liquidEffect?: Effect;
} | null>(null);
const prevConfigRef = ref<any>(null);

let cleanup: (() => void) | null = null;

const setup = () => {
  const container = containerRef.value;
  if (!container) return;
  speedRef.value = props.speed;
  const needsReinitKeys = ['antialias', 'liquid', 'noiseAmount'];
  const cfg = { antialias: props.antialias, liquid: props.liquid, noiseAmount: props.noiseAmount };
  let mustReinit = false;
  if (!threeRef.value) mustReinit = true;
  else if (prevConfigRef.value) {
    for (const k of needsReinitKeys)
      if (prevConfigRef.value[k] !== (cfg as any)[k]) {
        mustReinit = true;
        break;
      }
  }
  if (mustReinit) {
    if (threeRef.value) {
      const t = threeRef.value;
      t.resizeObserver?.disconnect();
      cancelAnimationFrame(t.raf!);
      t.quad?.geometry.dispose();
      t.material.dispose();
      t.composer?.dispose();
      t.renderer.dispose();
      if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
      threeRef.value = null;
    }
    const canvas = document.createElement('canvas');
    const gl = canvas.getContext('webgl2', { antialias: props.antialias, alpha: true });
    if (!gl) return;
    const renderer = new THREE.WebGLRenderer({
      canvas,
      context: gl as WebGL2RenderingContext,
      antialias: props.antialias,
      alpha: true
    });
    renderer.domElement.style.width = '100%';
    renderer.domElement.style.height = '100%';
    renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
    container.appendChild(renderer.domElement);
    const uniforms = {
      uResolution: { value: new THREE.Vector2(0, 0) },
      uTime: { value: 0 },
      uColor: { value: new THREE.Color(props.color) },
      uClickPos: {
        value: Array.from({ length: MAX_CLICKS }, () => new THREE.Vector2(-1, -1))
      },
      uClickTimes: { value: new Float32Array(MAX_CLICKS) },
      uShapeType: { value: SHAPE_MAP[props.variant] ?? 0 },
      uPixelSize: { value: props.pixelSize * renderer.getPixelRatio() },
      uScale: { value: props.patternScale },
      uDensity: { value: props.patternDensity },
      uPixelJitter: { value: props.pixelSizeJitter },
      uEnableRipples: { value: props.enableRipples ? 1 : 0 },
      uRippleSpeed: { value: props.rippleSpeed },
      uRippleThickness: { value: props.rippleThickness },
      uRippleIntensity: { value: props.rippleIntensityScale },
      uEdgeFade: { value: props.edgeFade }
    };
    const scene = new THREE.Scene();
    const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1);
    const material = new THREE.ShaderMaterial({
      vertexShader: VERTEX_SRC,
      fragmentShader: FRAGMENT_SRC,
      uniforms,
      transparent: true,
      glslVersion: THREE.GLSL3,
      depthTest: false,
      depthWrite: false
    });
    const quadGeom = new THREE.PlaneGeometry(2, 2);
    const quad = new THREE.Mesh(quadGeom, material);
    scene.add(quad);
    const clock = new THREE.Clock();
    const setSize = () => {
      const w = container.clientWidth || 1;
      const h = container.clientHeight || 1;
      renderer.setSize(w, h, false);
      uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height);
      if (threeRef.value?.composer)
        threeRef.value.composer.setSize(renderer.domElement.width, renderer.domElement.height);
      uniforms.uPixelSize.value = props.pixelSize * renderer.getPixelRatio();
    };
    setSize();
    const ro = new ResizeObserver(setSize);
    ro.observe(container);
    const randomFloat = () => {
      if (typeof window !== 'undefined' && (window as any).crypto?.getRandomValues) {
        const u32 = new Uint32Array(1);
        window.crypto.getRandomValues(u32);
        return u32[0] / 0xffffffff;
      }
      return Math.random();
    };
    const timeOffset = randomFloat() * 1000;
    let composer: EffectComposer | undefined;
    let touch: ReturnType<typeof createTouchTexture> | undefined;
    let liquidEffect: Effect | undefined;
    if (props.liquid) {
      touch = createTouchTexture();
      touch.radiusScale = props.liquidRadius;
      composer = new EffectComposer(renderer);
      const renderPass = new RenderPass(scene, camera);
      liquidEffect = createLiquidEffect(touch.texture, {
        strength: props.liquidStrength,
        freq: props.liquidWobbleSpeed
      });
      const effectPass = new EffectPass(camera, liquidEffect);
      effectPass.renderToScreen = true;
      composer.addPass(renderPass);
      composer.addPass(effectPass);
    }
    if (props.noiseAmount > 0) {
      if (!composer) {
        composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(scene, camera));
      }
      const noiseEffect = new Effect(
        'NoiseEffect',
        `uniform float uTime; uniform float uAmount; float hash(vec2 p){ return fract(sin(dot(p, vec2(127.1,311.7))) * 43758.5453);} void mainUv(inout vec2 uv){} void mainImage(const in vec4 inputColor,const in vec2 uv,out vec4 outputColor){ float n=hash(floor(uv*vec2(1920.0,1080.0))+floor(uTime*60.0)); float g=(n-0.5)*uAmount; outputColor=inputColor+vec4(vec3(g),0.0);} `,
        {
          uniforms: new Map<string, THREE.Uniform>([
            ['uTime', new THREE.Uniform(0)],
            ['uAmount', new THREE.Uniform(props.noiseAmount)]
          ])
        }
      );
      const noisePass = new EffectPass(camera, noiseEffect);
      noisePass.renderToScreen = true;
      if (composer && composer.passes.length > 0) composer.passes.forEach(p => ((p as any).renderToScreen = false));
      composer.addPass(noisePass);
    }
    if (composer) composer.setSize(renderer.domElement.width, renderer.domElement.height);
    const mapToPixels = (e: PointerEvent) => {
      const rect = renderer.domElement.getBoundingClientRect();
      const scaleX = renderer.domElement.width / rect.width;
      const scaleY = renderer.domElement.height / rect.height;
      const fx = (e.clientX - rect.left) * scaleX;
      const fy = (rect.height - (e.clientY - rect.top)) * scaleY;
      return {
        fx,
        fy,
        w: renderer.domElement.width,
        h: renderer.domElement.height
      };
    };
    const onPointerDown = (e: PointerEvent) => {
      const { fx, fy } = mapToPixels(e);
      const ix = threeRef.value?.clickIx ?? 0;
      uniforms.uClickPos.value[ix].set(fx, fy);
      uniforms.uClickTimes.value[ix] = uniforms.uTime.value;
      if (threeRef.value) threeRef.value.clickIx = (ix + 1) % MAX_CLICKS;
    };
    const onPointerMove = (e: PointerEvent) => {
      if (!touch) return;
      const { fx, fy, w, h } = mapToPixels(e);
      touch.addTouch({ x: fx / w, y: fy / h });
    };
    renderer.domElement.addEventListener('pointerdown', onPointerDown, {
      passive: true
    });
    renderer.domElement.addEventListener('pointermove', onPointerMove, {
      passive: true
    });
    let raf = 0;
    const animate = () => {
      if (props.autoPauseOffscreen && !visibilityRef.value.visible) {
        raf = requestAnimationFrame(animate);
        return;
      }
      uniforms.uTime.value = timeOffset + clock.getElapsedTime() * speedRef.value;
      if (liquidEffect) (liquidEffect as any).uniforms.get('uTime').value = uniforms.uTime.value;
      if (composer) {
        if (touch) touch.update();
        composer.passes.forEach(p => {
          const effs = (p as any).effects;
          if (effs)
            effs.forEach((eff: any) => {
              const u = eff.uniforms?.get('uTime');
              if (u) u.value = uniforms.uTime.value;
            });
        });
        composer.render();
      } else renderer.render(scene, camera);
      raf = requestAnimationFrame(animate);
    };
    raf = requestAnimationFrame(animate);
    threeRef.value = {
      renderer,
      scene,
      camera,
      material,
      clock,
      clickIx: 0,
      uniforms,
      resizeObserver: ro,
      raf,
      quad,
      timeOffset,
      composer,
      touch,
      liquidEffect
    };
  } else {
    const t = threeRef.value!;
    t.uniforms.uShapeType.value = SHAPE_MAP[props.variant] ?? 0;
    t.uniforms.uPixelSize.value = props.pixelSize * t.renderer.getPixelRatio();
    t.uniforms.uColor.value.set(props.color);
    t.uniforms.uScale.value = props.patternScale;
    t.uniforms.uDensity.value = props.patternDensity;
    t.uniforms.uPixelJitter.value = props.pixelSizeJitter;
    t.uniforms.uEnableRipples.value = props.enableRipples ? 1 : 0;
    t.uniforms.uRippleIntensity.value = props.rippleIntensityScale;
    t.uniforms.uRippleThickness.value = props.rippleThickness;
    t.uniforms.uRippleSpeed.value = props.rippleSpeed;
    t.uniforms.uEdgeFade.value = props.edgeFade;
    if (props.transparent) t.renderer.setClearAlpha(0);
    else t.renderer.setClearColor(0x000000, 1);
    if (t.liquidEffect) {
      const uStrength = (t.liquidEffect as any).uniforms.get('uStrength');
      if (uStrength) uStrength.value = props.liquidStrength;
      const uFreq = (t.liquidEffect as any).uniforms.get('uFreq');
      if (uFreq) uFreq.value = props.liquidWobbleSpeed;
    }
    if (t.touch) t.touch.radiusScale = props.liquidRadius;
  }
  prevConfigRef.value = cfg;

  cleanup = () => {
    if (threeRef.value && mustReinit) return;
    if (!threeRef.value) return;
    const t = threeRef.value;
    t.resizeObserver?.disconnect();
    cancelAnimationFrame(t.raf!);
    t.quad?.geometry.dispose();
    t.material.dispose();
    t.composer?.dispose();
    t.renderer.dispose();
    if (t.renderer.domElement.parentElement === container) container.removeChild(t.renderer.domElement);
    threeRef.value = null;
  };
};

onMounted(() => {
  setup();
});

onBeforeUnmount(() => {
  cleanup?.();
});

watch(
  props,
  () => {
    cleanup?.();
    setup();
  },
  { deep: true }
);
</script>
