{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "explosion",
  "title": "Map Explosion",
  "description": "Animated explosion burst effect with radial particles and flash.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/explosion.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useRef, useState } from \"react\"\nimport { useMap } from \"./hooks\"\nimport type { MapCoordinates } from \"./types\"\n\ntype RgbColor = {\n  r: number\n  g: number\n  b: number\n}\n\ntype ExplosionType = \"burst\" | \"nuclear\"\n\ntype ExplosionParticle = {\n  x: number\n  y: number\n  velocityX: number\n  velocityY: number\n  radius: number\n  life: number\n  maxLife: number\n  color: RgbColor\n  type: \"debris\" | \"fireball\" | \"mushroom\" | \"shockwave\"\n}\n\ntype MapExplosionProps = {\n  id: string\n  coordinates: MapCoordinates\n  type?: ExplosionType\n  size?: number\n  particleCount?: number\n  duration?: number\n  coreColor?: string\n  outerColor?: string\n  autoStart?: boolean\n  loop?: boolean\n  loopDelay?: number\n}\n\ntype ExplosionRenderer = {\n  width: number\n  height: number\n  data: Uint8ClampedArray\n  context?: CanvasRenderingContext2D\n  particles: ExplosionParticle[]\n  isExploding: boolean\n  explosionTime: number\n  shockwaveRadius: number\n  onAdd: () => void\n  render: () => boolean\n  trigger: () => void\n  reset: () => void\n}\n\ntype ExplosionControl = {\n  trigger: () => void\n  reset: () => void\n  isExploding: boolean\n}\n\nconst DEFAULT_SIZE = 150\nconst DEFAULT_PARTICLE_COUNT = 60\nconst DEFAULT_DURATION = 3000\nconst DEFAULT_CORE_COLOR = \"#ffffff\"\nconst DEFAULT_OUTER_COLOR = \"#ff6600\"\nconst DEFAULT_LOOP_DELAY = 3000\nconst FRAME_RATE = 60\n\nconst BURST_SPEED_MIN = 1.5\nconst BURST_SPEED_RANGE = 4\nconst BURST_CORE_PROBABILITY = 0.3\nconst BURST_LIFE_VARIATION_MIN = 0.7\nconst BURST_LIFE_VARIATION_RANGE = 0.3\nconst BURST_CORE_RADIUS_MIN = 4\nconst BURST_CORE_RADIUS_RANGE = 6\nconst BURST_OUTER_RADIUS_MIN = 3\nconst BURST_OUTER_RADIUS_RANGE = 8\n\nconst NUCLEAR_LIFE_VARIATION_MIN = 0.6\nconst NUCLEAR_LIFE_VARIATION_RANGE = 0.4\nconst NUCLEAR_FIREBALL_DISTANCE_RATIO = 0.1\nconst NUCLEAR_FIREBALL_VELOCITY_SPREAD = 0.5\nconst NUCLEAR_FIREBALL_VELOCITY_MIN = -0.8\nconst NUCLEAR_FIREBALL_VELOCITY_RANGE = -1.2\nconst NUCLEAR_FIREBALL_RADIUS_MIN = 8\nconst NUCLEAR_FIREBALL_RADIUS_RANGE = 12\nconst NUCLEAR_FIREBALL_LIFE_FACTOR = 0.9\nconst NUCLEAR_MUSHROOM_SPREAD_RATIO = 0.15\nconst NUCLEAR_MUSHROOM_Y_OFFSET_RATIO = 0.25\nconst NUCLEAR_MUSHROOM_VELOCITY_MIN = 0.5\nconst NUCLEAR_MUSHROOM_VELOCITY_RANGE = 1\nconst NUCLEAR_MUSHROOM_VERTICAL_MIN = -0.3\nconst NUCLEAR_MUSHROOM_VERTICAL_RANGE = -0.5\nconst NUCLEAR_MUSHROOM_RADIUS_MIN = 10\nconst NUCLEAR_MUSHROOM_RADIUS_RANGE = 15\nconst NUCLEAR_DEBRIS_SPEED_MIN = 2\nconst NUCLEAR_DEBRIS_SPEED_RANGE = 3\nconst NUCLEAR_DEBRIS_VERTICAL_FACTOR = 0.6\nconst NUCLEAR_DEBRIS_RADIUS_MIN = 3\nconst NUCLEAR_DEBRIS_RADIUS_RANGE = 5\nconst NUCLEAR_DEBRIS_LIFE_FACTOR = 0.8\nconst NUCLEAR_FIREBALL_RATIO = 0.3\nconst NUCLEAR_MUSHROOM_RATIO = 0.4\nconst NUCLEAR_MIN_SIZE = 200\nconst NUCLEAR_MIN_PARTICLE_COUNT = 80\nconst NUCLEAR_MIN_DURATION = 4000\nconst NUCLEAR_CENTER_Y_RATIO = 0.65\n\nconst SHOCKWAVE_ELLIPSE_Y_RATIO = 0.4\nconst SHOCKWAVE_START_PROGRESS = 0.05\nconst SHOCKWAVE_END_PROGRESS = 0.5\nconst SHOCKWAVE_DURATION_RATIO = 0.45\nconst SHOCKWAVE_MAX_RADIUS_RATIO = 0.5\nconst SHOCKWAVE_MAX_ALPHA = 0.6\nconst SHOCKWAVE_MAX_LINE_WIDTH = 4\n\nconst FLASH_NUCLEAR_END_PROGRESS = 0.2\nconst FLASH_NUCLEAR_RADIUS_RATIO = 0.5\nconst FLASH_NUCLEAR_MIN_SCALE = 0.3\nconst FLASH_NUCLEAR_SCALE_RANGE = 0.7\nconst FLASH_BURST_END_PROGRESS = 0.15\nconst FLASH_BURST_RADIUS_RATIO = 0.35\n\nconst FIREBALL_VELOCITY_DECAY_X = 0.995\nconst FIREBALL_VELOCITY_DECAY_Y = 0.997\nconst MUSHROOM_VELOCITY_DECAY_X = 0.99\nconst MUSHROOM_VELOCITY_DECAY_Y = 0.985\nconst DEBRIS_VELOCITY_DECAY = 0.985\nconst DEBRIS_GRAVITY = 0.03\n\nconst PARTICLE_FADE_START = 0.5\nconst PARTICLE_FADE_POWER = 1.5\nconst PARTICLE_SHRINK_FACTOR = 0.15\n\nconst hexToRgb = (hex: string): RgbColor => {\n  const result = /^#?([a-f\\d]{2})([a-f\\d]{2})([a-f\\d]{2})$/i.exec(hex)\n\n  if (!result) {\n    return { r: 255, g: 102, b: 0 }\n  }\n\n  return {\n    r: parseInt(result[1], 16),\n    g: parseInt(result[2], 16),\n    b: parseInt(result[3], 16),\n  }\n}\n\nconst createBurstParticle = (\n  centerX: number,\n  centerY: number,\n  coreRgb: RgbColor,\n  outerRgb: RgbColor,\n  durationFrames: number\n): ExplosionParticle => {\n  const angle = Math.random() * Math.PI * 2\n  const speed = BURST_SPEED_MIN + Math.random() * BURST_SPEED_RANGE\n  const isCore = Math.random() < BURST_CORE_PROBABILITY\n  const lifeVariation = BURST_LIFE_VARIATION_MIN + Math.random() * BURST_LIFE_VARIATION_RANGE\n\n  return {\n    x: centerX,\n    y: centerY,\n    velocityX: Math.cos(angle) * speed,\n    velocityY: Math.sin(angle) * speed,\n    radius: isCore\n      ? BURST_CORE_RADIUS_MIN + Math.random() * BURST_CORE_RADIUS_RANGE\n      : BURST_OUTER_RADIUS_MIN + Math.random() * BURST_OUTER_RADIUS_RANGE,\n    life: 0,\n    maxLife: Math.floor(durationFrames * lifeVariation),\n    color: isCore ? coreRgb : outerRgb,\n    type: \"debris\",\n  }\n}\n\nconst createNuclearParticle = (\n  centerX: number,\n  centerY: number,\n  size: number,\n  coreRgb: RgbColor,\n  outerRgb: RgbColor,\n  particleType: \"fireball\" | \"mushroom\" | \"debris\",\n  durationFrames: number\n): ExplosionParticle => {\n  const lifeVariation = NUCLEAR_LIFE_VARIATION_MIN + Math.random() * NUCLEAR_LIFE_VARIATION_RANGE\n\n  if (particleType === \"fireball\") {\n    const angle = Math.random() * Math.PI * 2\n    const distance = Math.random() * size * NUCLEAR_FIREBALL_DISTANCE_RATIO\n    return {\n      x: centerX + Math.cos(angle) * distance,\n      y: centerY + Math.sin(angle) * distance,\n      velocityX: (Math.random() - 0.5) * NUCLEAR_FIREBALL_VELOCITY_SPREAD,\n      velocityY: NUCLEAR_FIREBALL_VELOCITY_MIN + Math.random() * NUCLEAR_FIREBALL_VELOCITY_RANGE,\n      radius: NUCLEAR_FIREBALL_RADIUS_MIN + Math.random() * NUCLEAR_FIREBALL_RADIUS_RANGE,\n      life: 0,\n      maxLife: Math.floor(durationFrames * lifeVariation * NUCLEAR_FIREBALL_LIFE_FACTOR),\n      color: coreRgb,\n      type: \"fireball\",\n    }\n  }\n\n  if (particleType === \"mushroom\") {\n    const angle = Math.random() * Math.PI * 2\n    const spread = Math.random() * size * NUCLEAR_MUSHROOM_SPREAD_RATIO\n    return {\n      x: centerX + Math.cos(angle) * spread,\n      y: centerY - size * NUCLEAR_MUSHROOM_Y_OFFSET_RATIO,\n      velocityX: Math.cos(angle) * (NUCLEAR_MUSHROOM_VELOCITY_MIN + Math.random() * NUCLEAR_MUSHROOM_VELOCITY_RANGE),\n      velocityY: NUCLEAR_MUSHROOM_VERTICAL_MIN + Math.random() * NUCLEAR_MUSHROOM_VERTICAL_RANGE,\n      radius: NUCLEAR_MUSHROOM_RADIUS_MIN + Math.random() * NUCLEAR_MUSHROOM_RADIUS_RANGE,\n      life: 0,\n      maxLife: Math.floor(durationFrames * lifeVariation),\n      color: outerRgb,\n      type: \"mushroom\",\n    }\n  }\n\n  const angle = Math.random() * Math.PI * 2\n  const speed = NUCLEAR_DEBRIS_SPEED_MIN + Math.random() * NUCLEAR_DEBRIS_SPEED_RANGE\n  return {\n    x: centerX,\n    y: centerY,\n    velocityX: Math.cos(angle) * speed,\n    velocityY: Math.sin(angle) * speed * NUCLEAR_DEBRIS_VERTICAL_FACTOR,\n    radius: NUCLEAR_DEBRIS_RADIUS_MIN + Math.random() * NUCLEAR_DEBRIS_RADIUS_RANGE,\n    life: 0,\n    maxLife: Math.floor(durationFrames * lifeVariation * NUCLEAR_DEBRIS_LIFE_FACTOR),\n    color: outerRgb,\n    type: \"debris\",\n  }\n}\n\nconst fadeCanvasEdges = (context: CanvasRenderingContext2D, width: number, height: number) => {\n  const edgeSize = Math.max(4, Math.floor(width * 0.06))\n  context.globalCompositeOperation = \"destination-out\"\n\n  const topGradient = context.createLinearGradient(0, 0, 0, edgeSize)\n  topGradient.addColorStop(0, \"rgba(0, 0, 0, 1)\")\n  topGradient.addColorStop(1, \"rgba(0, 0, 0, 0)\")\n  context.fillStyle = topGradient\n  context.fillRect(0, 0, width, edgeSize)\n\n  const bottomGradient = context.createLinearGradient(0, height - edgeSize, 0, height)\n  bottomGradient.addColorStop(0, \"rgba(0, 0, 0, 0)\")\n  bottomGradient.addColorStop(1, \"rgba(0, 0, 0, 1)\")\n  context.fillStyle = bottomGradient\n  context.fillRect(0, height - edgeSize, width, edgeSize)\n\n  const leftGradient = context.createLinearGradient(0, 0, edgeSize, 0)\n  leftGradient.addColorStop(0, \"rgba(0, 0, 0, 1)\")\n  leftGradient.addColorStop(1, \"rgba(0, 0, 0, 0)\")\n  context.fillStyle = leftGradient\n  context.fillRect(0, 0, edgeSize, height)\n\n  const rightGradient = context.createLinearGradient(width - edgeSize, 0, width, 0)\n  rightGradient.addColorStop(0, \"rgba(0, 0, 0, 0)\")\n  rightGradient.addColorStop(1, \"rgba(0, 0, 0, 1)\")\n  context.fillStyle = rightGradient\n  context.fillRect(width - edgeSize, 0, edgeSize, height)\n\n  context.globalCompositeOperation = \"source-over\"\n}\n\nconst drawNuclearFlash = (\n  context: CanvasRenderingContext2D,\n  centerX: number,\n  centerY: number,\n  size: number,\n  progress: number\n) => {\n  if (progress >= FLASH_NUCLEAR_END_PROGRESS) {\n    return\n  }\n\n  const flashProgress = progress / FLASH_NUCLEAR_END_PROGRESS\n  const flashIntensity = 1 - Math.pow(flashProgress, 2)\n  const flashRadius =\n    size * FLASH_NUCLEAR_RADIUS_RATIO * (FLASH_NUCLEAR_MIN_SCALE + flashProgress * FLASH_NUCLEAR_SCALE_RANGE)\n\n  const flashGradient = context.createRadialGradient(centerX, centerY, 0, centerX, centerY, flashRadius)\n  flashGradient.addColorStop(0, `rgba(255, 255, 255, ${flashIntensity})`)\n  flashGradient.addColorStop(0.3, `rgba(255, 255, 200, ${flashIntensity * 0.8})`)\n  flashGradient.addColorStop(0.6, `rgba(255, 200, 100, ${flashIntensity * 0.5})`)\n  flashGradient.addColorStop(1, \"rgba(255, 100, 0, 0)\")\n\n  context.beginPath()\n  context.arc(centerX, centerY, flashRadius, 0, Math.PI * 2)\n  context.fillStyle = flashGradient\n  context.fill()\n}\n\nconst drawShockwave = (\n  context: CanvasRenderingContext2D,\n  centerX: number,\n  centerY: number,\n  size: number,\n  progress: number,\n  renderer: ExplosionRenderer\n) => {\n  if (progress <= SHOCKWAVE_START_PROGRESS || progress >= SHOCKWAVE_END_PROGRESS) {\n    return\n  }\n\n  const shockProgress = (progress - SHOCKWAVE_START_PROGRESS) / SHOCKWAVE_DURATION_RATIO\n  renderer.shockwaveRadius = size * SHOCKWAVE_MAX_RADIUS_RATIO * shockProgress\n  const shockAlpha = SHOCKWAVE_MAX_ALPHA * (1 - shockProgress)\n  const radiusX = renderer.shockwaveRadius\n  const radiusY = renderer.shockwaveRadius * SHOCKWAVE_ELLIPSE_Y_RATIO\n\n  context.beginPath()\n  context.ellipse(centerX, centerY, radiusX, radiusY, 0, 0, Math.PI * 2)\n  context.strokeStyle = `rgba(255, 200, 100, ${shockAlpha})`\n  context.lineWidth = SHOCKWAVE_MAX_LINE_WIDTH * (1 - shockProgress) + 1\n  context.stroke()\n}\n\nconst drawBurstFlash = (\n  context: CanvasRenderingContext2D,\n  centerX: number,\n  centerY: number,\n  size: number,\n  progress: number\n) => {\n  if (progress >= FLASH_BURST_END_PROGRESS) {\n    return\n  }\n\n  const flashIntensity = 1 - progress / FLASH_BURST_END_PROGRESS\n  const flashGradient = context.createRadialGradient(\n    centerX,\n    centerY,\n    0,\n    centerX,\n    centerY,\n    size * FLASH_BURST_RADIUS_RATIO\n  )\n  flashGradient.addColorStop(0, `rgba(255, 255, 255, ${flashIntensity})`)\n  flashGradient.addColorStop(0.5, `rgba(255, 200, 100, ${flashIntensity * 0.6})`)\n  flashGradient.addColorStop(1, \"rgba(255, 100, 0, 0)\")\n  context.beginPath()\n  context.arc(centerX, centerY, size * FLASH_BURST_RADIUS_RATIO, 0, Math.PI * 2)\n  context.fillStyle = flashGradient\n  context.fill()\n}\n\nconst updateParticle = (particle: ExplosionParticle) => {\n  if (particle.type === \"fireball\") {\n    particle.x += particle.velocityX\n    particle.y += particle.velocityY\n    particle.velocityX *= FIREBALL_VELOCITY_DECAY_X\n    particle.velocityY *= FIREBALL_VELOCITY_DECAY_Y\n  } else if (particle.type === \"mushroom\") {\n    particle.x += particle.velocityX\n    particle.y += particle.velocityY\n    particle.velocityX *= MUSHROOM_VELOCITY_DECAY_X\n    particle.velocityY *= MUSHROOM_VELOCITY_DECAY_Y\n  } else {\n    particle.x += particle.velocityX\n    particle.y += particle.velocityY\n    particle.velocityX *= DEBRIS_VELOCITY_DECAY\n    particle.velocityY *= DEBRIS_VELOCITY_DECAY\n    particle.velocityY += DEBRIS_GRAVITY\n  }\n\n  particle.life++\n}\n\nconst drawParticle = (context: CanvasRenderingContext2D, particle: ExplosionParticle) => {\n  const lifeRatio = particle.life / particle.maxLife\n  const alphaMultiplier =\n    lifeRatio < PARTICLE_FADE_START\n      ? 1\n      : 1 - Math.pow((lifeRatio - PARTICLE_FADE_START) / (1 - PARTICLE_FADE_START), PARTICLE_FADE_POWER)\n  const alpha = Math.max(0, alphaMultiplier)\n  const currentRadius = particle.radius * (1 - lifeRatio * PARTICLE_SHRINK_FACTOR)\n\n  const gradient = context.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, currentRadius)\n  const { r, g, b } = particle.color\n\n  if (particle.type === \"fireball\") {\n    gradient.addColorStop(0, `rgba(255, 255, 255, ${alpha})`)\n    gradient.addColorStop(0.3, `rgba(255, 255, 200, ${alpha * 0.9})`)\n    gradient.addColorStop(0.6, `rgba(${r}, ${g}, ${b}, ${alpha * 0.7})`)\n    gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`)\n  } else if (particle.type === \"mushroom\") {\n    gradient.addColorStop(0, `rgba(${r}, ${g}, ${b}, ${alpha * 0.8})`)\n    gradient.addColorStop(\n      0.5,\n      `rgba(${Math.floor(r * 0.7)}, ${Math.floor(g * 0.5)}, ${Math.floor(b * 0.3)}, ${alpha * 0.6})`\n    )\n    gradient.addColorStop(1, `rgba(100, 80, 60, 0)`)\n  } else {\n    gradient.addColorStop(0, `rgba(255, 255, 255, ${alpha})`)\n    gradient.addColorStop(0.4, `rgba(${r}, ${g}, ${b}, ${alpha * 0.8})`)\n    gradient.addColorStop(1, `rgba(${r}, ${g}, ${b}, 0)`)\n  }\n\n  context.beginPath()\n  context.arc(particle.x, particle.y, currentRadius, 0, Math.PI * 2)\n  context.fillStyle = gradient\n  context.fill()\n}\n\nconst createExplosionRenderer = (\n  explosionType: ExplosionType,\n  size: number,\n  particleCount: number,\n  duration: number,\n  coreColor: string,\n  outerColor: string\n): ExplosionRenderer => {\n  const coreRgb = hexToRgb(coreColor)\n  const outerRgb = hexToRgb(outerColor)\n  const centerX = size / 2\n  const centerY = explosionType === \"nuclear\" ? size * NUCLEAR_CENTER_Y_RATIO : size / 2\n  const durationFrames = Math.floor((duration / 1000) * FRAME_RATE)\n\n  const renderer: ExplosionRenderer = {\n    width: size,\n    height: size,\n    data: new Uint8ClampedArray(size * size * 4),\n    particles: [],\n    isExploding: false,\n    explosionTime: 0,\n    shockwaveRadius: 0,\n\n    onAdd() {\n      const canvas = document.createElement(\"canvas\")\n      canvas.width = this.width\n      canvas.height = this.height\n      this.context = canvas.getContext(\"2d\", { willReadFrequently: true }) || undefined\n    },\n\n    trigger() {\n      this.particles = []\n      this.shockwaveRadius = 0\n\n      if (explosionType === \"nuclear\") {\n        const fireballCount = Math.floor(particleCount * NUCLEAR_FIREBALL_RATIO)\n        const mushroomCount = Math.floor(particleCount * NUCLEAR_MUSHROOM_RATIO)\n        const debrisCount = particleCount - fireballCount - mushroomCount\n\n        for (let index = 0; index < fireballCount; index++) {\n          this.particles.push(\n            createNuclearParticle(centerX, centerY, size, coreRgb, outerRgb, \"fireball\", durationFrames)\n          )\n        }\n        for (let index = 0; index < mushroomCount; index++) {\n          this.particles.push(\n            createNuclearParticle(centerX, centerY, size, coreRgb, outerRgb, \"mushroom\", durationFrames)\n          )\n        }\n        for (let index = 0; index < debrisCount; index++) {\n          this.particles.push(\n            createNuclearParticle(centerX, centerY, size, coreRgb, outerRgb, \"debris\", durationFrames)\n          )\n        }\n      } else {\n        for (let index = 0; index < particleCount; index++) {\n          this.particles.push(createBurstParticle(centerX, centerY, coreRgb, outerRgb, durationFrames))\n        }\n      }\n\n      this.isExploding = true\n      this.explosionTime = performance.now()\n    },\n\n    reset() {\n      this.particles = []\n      this.isExploding = false\n      this.explosionTime = 0\n      this.shockwaveRadius = 0\n      if (this.context) {\n        this.context.clearRect(0, 0, this.width, this.height)\n        this.data = this.context.getImageData(0, 0, this.width, this.height).data\n      }\n    },\n\n    render() {\n      if (!this.context) {\n        return false\n      }\n\n      this.context.clearRect(0, 0, this.width, this.height)\n\n      if (!this.isExploding) {\n        this.data = this.context.getImageData(0, 0, this.width, this.height).data\n        return true\n      }\n\n      const elapsed = performance.now() - this.explosionTime\n      const progress = Math.min(elapsed / duration, 1)\n\n      if (explosionType === \"nuclear\") {\n        drawNuclearFlash(this.context, centerX, centerY, size, progress)\n        drawShockwave(this.context, centerX, centerY, size, progress, this)\n      } else {\n        drawBurstFlash(this.context, centerX, centerY, size, progress)\n      }\n\n      let activeParticles = 0\n\n      for (let particleIndex = 0; particleIndex < this.particles.length; particleIndex++) {\n        const particle = this.particles[particleIndex]\n        updateParticle(particle)\n\n        if (particle.life >= particle.maxLife) {\n          continue\n        }\n\n        activeParticles++\n        drawParticle(this.context, particle)\n      }\n\n      if (progress >= 1 || activeParticles === 0) {\n        this.isExploding = false\n      }\n\n      fadeCanvasEdges(this.context, this.width, this.height)\n      this.data = this.context.getImageData(0, 0, this.width, this.height).data\n\n      return true\n    },\n  }\n\n  return renderer\n}\n\nconst explosionControls = new Map<string, ExplosionControl>()\n\nexport const MapExplosion = ({\n  id,\n  coordinates,\n  type: explosionType = \"burst\",\n  size = DEFAULT_SIZE,\n  particleCount = DEFAULT_PARTICLE_COUNT,\n  duration = DEFAULT_DURATION,\n  coreColor = DEFAULT_CORE_COLOR,\n  outerColor = DEFAULT_OUTER_COLOR,\n  autoStart = true,\n  loop = false,\n  loopDelay = DEFAULT_LOOP_DELAY,\n}: MapExplosionProps) => {\n  const { map, isLoaded } = useMap()\n  const animationFrameRef = useRef<number | null>(null)\n  const rendererRef = useRef<ExplosionRenderer | null>(null)\n  const loopTimeoutRef = useRef<NodeJS.Timeout | null>(null)\n  const sourceId = `${id}-source`\n  const layerId = `${id}-layer`\n\n  useEffect(() => {\n    if (!isLoaded || !map) {\n      return\n    }\n\n    const actualSize = explosionType === \"nuclear\" ? Math.max(size, NUCLEAR_MIN_SIZE) : size\n    const actualParticleCount =\n      explosionType === \"nuclear\" ? Math.max(particleCount, NUCLEAR_MIN_PARTICLE_COUNT) : particleCount\n    const actualDuration = explosionType === \"nuclear\" ? Math.max(duration, NUCLEAR_MIN_DURATION) : duration\n\n    const explosionRenderer = createExplosionRenderer(\n      explosionType,\n      actualSize,\n      actualParticleCount,\n      actualDuration,\n      coreColor,\n      outerColor\n    )\n    rendererRef.current = explosionRenderer\n\n    const control: ExplosionControl = {\n      trigger: () => {\n        rendererRef.current?.trigger()\n      },\n      reset: () => {\n        rendererRef.current?.reset()\n      },\n      get isExploding() {\n        return rendererRef.current?.isExploding || false\n      },\n    }\n    explosionControls.set(id, control)\n\n    const addImage = () => {\n      if (!map.hasImage(id)) {\n        map.addImage(id, explosionRenderer, { pixelRatio: 2 })\n      }\n    }\n\n    addImage()\n\n    if (autoStart) {\n      explosionRenderer.trigger()\n    }\n\n    const animate = () => {\n      const isActive = rendererRef.current?.isExploding || false\n\n      if (isActive) {\n        map.triggerRepaint()\n      }\n\n      if (loop && rendererRef.current && !rendererRef.current.isExploding && autoStart) {\n        if (!loopTimeoutRef.current) {\n          loopTimeoutRef.current = setTimeout(() => {\n            if (rendererRef.current) {\n              rendererRef.current.trigger()\n            }\n            loopTimeoutRef.current = null\n          }, loopDelay)\n        }\n      }\n\n      animationFrameRef.current = requestAnimationFrame(animate)\n    }\n    animationFrameRef.current = requestAnimationFrame(animate)\n\n    const handleStyleLoad = () => {\n      addImage()\n    }\n\n    map.on(\"style.load\", handleStyleLoad)\n\n    return () => {\n      map.off(\"style.load\", handleStyleLoad)\n      explosionControls.delete(id)\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n      if (loopTimeoutRef.current) {\n        clearTimeout(loopTimeoutRef.current)\n      }\n      if (!map || !map.getStyle()) {\n        return\n      }\n      if (map.hasImage(id)) {\n        map.removeImage(id)\n      }\n    }\n  }, [\n    map,\n    isLoaded,\n    id,\n    explosionType,\n    size,\n    particleCount,\n    duration,\n    coreColor,\n    outerColor,\n    autoStart,\n    loop,\n    loopDelay,\n  ])\n\n  useEffect(() => {\n    if (!isLoaded || !map) {\n      return\n    }\n\n    let addLayersFrameId: number\n\n    const addLayers = () => {\n      if (!map.isStyleLoaded() || !map.hasImage(id)) {\n        addLayersFrameId = requestAnimationFrame(addLayers)\n        return\n      }\n\n      if (!map.getSource(sourceId)) {\n        map.addSource(sourceId, {\n          type: \"geojson\",\n          data: {\n            type: \"FeatureCollection\",\n            features: [\n              {\n                type: \"Feature\",\n                geometry: { type: \"Point\", coordinates },\n                properties: {},\n              },\n            ],\n          },\n        })\n      }\n\n      if (!map.getLayer(layerId)) {\n        map.addLayer({\n          id: layerId,\n          type: \"symbol\",\n          source: sourceId,\n          layout: {\n            \"icon-image\": id,\n            \"icon-allow-overlap\": true,\n          },\n        })\n      }\n    }\n\n    addLayers()\n\n    const handleStyleLoad = () => {\n      addLayers()\n    }\n\n    map.on(\"style.load\", handleStyleLoad)\n\n    return () => {\n      cancelAnimationFrame(addLayersFrameId)\n      map.off(\"style.load\", handleStyleLoad)\n      if (!map || !map.getStyle()) {\n        return\n      }\n      if (map.getLayer(layerId)) {\n        map.removeLayer(layerId)\n      }\n      if (map.getSource(sourceId)) {\n        map.removeSource(sourceId)\n      }\n    }\n  }, [map, isLoaded, coordinates, id, sourceId, layerId])\n\n  return null\n}\n\nexport const useExplosionControl = (id: string): ExplosionControl | null => {\n  const [, forceUpdate] = useState(0)\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      forceUpdate((previous) => {\n        return previous + 1\n      })\n    }, 100)\n\n    return () => {\n      clearInterval(interval)\n    }\n  }, [])\n\n  return explosionControls.get(id) || null\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/explosion.tsx"
    }
  ],
  "type": "registry:ui"
}
