{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "snow",
  "title": "Map Snow",
  "description": "Falling snowflakes effect with gentle drift and wind.",
  "dependencies": ["mapbox-gl"],
  "devDependencies": ["@types/mapbox-gl"],
  "registryDependencies": ["https://www.terrae.dev/map.json"],
  "files": [
    {
      "path": "src/registry/map/snow.tsx",
      "content": "\"use client\"\n\nimport { useEffect, useId, useRef, useState, useCallback } from \"react\"\nimport { createPortal } from \"react-dom\"\nimport { useMap } from \"./hooks\"\n\ntype RgbColor = {\n  red: number\n  green: number\n  blue: number\n}\n\ntype SnowParticle = {\n  x: number\n  y: number\n  size: number\n  speed: number\n  opacity: number\n  wobble: number\n  wobbleSpeed: number\n  wobbleAmount: number\n}\n\ntype SnowControl = {\n  start: () => void\n  stop: () => void\n  setIntensity: (intensity: number) => void\n  isActive: boolean\n}\n\ntype SnowOverlayProps = {\n  intensity: number\n  particleCount: number\n  color: string\n  windSpeed: number\n  windDirection: number\n  fallSpeed: number\n  isActive: boolean\n  onControlReady: (control: SnowControl) => void\n}\n\ntype MapSnowProps = {\n  id?: string\n  intensity?: number\n  particleCount?: number\n  color?: string\n  windSpeed?: number\n  windDirection?: number\n  fallSpeed?: number\n  autoStart?: boolean\n}\n\nconst DEFAULT_INTENSITY = 1\nconst DEFAULT_PARTICLE_COUNT = 150\nconst DEFAULT_COLOR = \"#ffffff\"\nconst DEFAULT_WIND_SPEED = 0.5\nconst DEFAULT_WIND_DIRECTION = 0\nconst DEFAULT_FALL_SPEED = 1\n\nconst MIN_INTENSITY = 0.1\nconst MAX_INTENSITY = 3\nconst PARTICLE_SPAWN_OFFSET = 100\nconst PARTICLE_MIN_SIZE = 2\nconst PARTICLE_SIZE_RANGE = 4\nconst PARTICLE_MIN_SPEED = 0.5\nconst PARTICLE_SPEED_RANGE = 1.5\nconst PARTICLE_MIN_OPACITY = 0.4\nconst PARTICLE_OPACITY_RANGE = 0.6\nconst PARTICLE_MIN_WOBBLE_SPEED = 0.01\nconst PARTICLE_WOBBLE_SPEED_RANGE = 0.03\nconst PARTICLE_MIN_WOBBLE_AMOUNT = 0.5\nconst PARTICLE_WOBBLE_AMOUNT_RANGE = 1.5\nconst BOUNDS_PADDING = 20\nconst DEGREES_TO_RADIANS = Math.PI / 180\nconst WOBBLE_DRIFT_FACTOR = 0.3\nconst GRADIENT_MID_OPACITY_FACTOR = 0.6\nconst CONTROL_UPDATE_INTERVAL = 100\n\nconst snowControls = new Map<string, SnowControl>()\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 { red: 255, green: 255, blue: 255 }\n  }\n\n  return {\n    red: parseInt(result[1], 16),\n    green: parseInt(result[2], 16),\n    blue: parseInt(result[3], 16),\n  }\n}\n\nconst createParticle = (canvasWidth: number, canvasHeight: number, intensity: number): SnowParticle => {\n  return {\n    x: Math.random() * canvasWidth,\n    y: -BOUNDS_PADDING - Math.random() * PARTICLE_SPAWN_OFFSET,\n    size: PARTICLE_MIN_SIZE + Math.random() * PARTICLE_SIZE_RANGE * intensity,\n    speed: PARTICLE_MIN_SPEED + Math.random() * PARTICLE_SPEED_RANGE,\n    opacity: PARTICLE_MIN_OPACITY + Math.random() * PARTICLE_OPACITY_RANGE,\n    wobble: Math.random() * Math.PI * 2,\n    wobbleSpeed: PARTICLE_MIN_WOBBLE_SPEED + Math.random() * PARTICLE_WOBBLE_SPEED_RANGE,\n    wobbleAmount: PARTICLE_MIN_WOBBLE_AMOUNT + Math.random() * PARTICLE_WOBBLE_AMOUNT_RANGE,\n  }\n}\n\nconst updateParticlePosition = (particle: SnowParticle, windX: number, fallSpeed: number): void => {\n  particle.wobble += particle.wobbleSpeed\n  const wobbleOffset = Math.sin(particle.wobble) * particle.wobbleAmount\n\n  particle.x += windX + wobbleOffset * WOBBLE_DRIFT_FACTOR\n  particle.y += particle.speed * fallSpeed\n}\n\nconst isParticleOutOfBounds = (particle: SnowParticle, canvasWidth: number, canvasHeight: number): boolean => {\n  return (\n    particle.y > canvasHeight + BOUNDS_PADDING ||\n    particle.x < -BOUNDS_PADDING ||\n    particle.x > canvasWidth + BOUNDS_PADDING\n  )\n}\n\nconst drawParticle = (\n  context: CanvasRenderingContext2D,\n  particle: SnowParticle,\n  color: RgbColor,\n  intensity: number\n): void => {\n  const gradient = context.createRadialGradient(particle.x, particle.y, 0, particle.x, particle.y, particle.size)\n\n  const particleOpacity = particle.opacity * intensity\n  gradient.addColorStop(0, `rgba(${color.red}, ${color.green}, ${color.blue}, ${particleOpacity})`)\n  gradient.addColorStop(\n    0.5,\n    `rgba(${color.red}, ${color.green}, ${color.blue}, ${particleOpacity * GRADIENT_MID_OPACITY_FACTOR})`\n  )\n  gradient.addColorStop(1, `rgba(${color.red}, ${color.green}, ${color.blue}, 0)`)\n\n  context.beginPath()\n  context.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2)\n  context.fillStyle = gradient\n  context.fill()\n}\n\nconst adjustParticleCount = (\n  particles: SnowParticle[],\n  targetCount: number,\n  canvasWidth: number,\n  canvasHeight: number,\n  intensity: number\n): SnowParticle[] => {\n  if (particles.length < targetCount) {\n    const toAdd = targetCount - particles.length\n    for (let index = 0; index < toAdd; index++) {\n      particles.push(createParticle(canvasWidth, canvasHeight, intensity))\n    }\n    return particles\n  }\n\n  if (particles.length > targetCount) {\n    return particles.slice(0, targetCount)\n  }\n\n  return particles\n}\n\nexport const MapSnow = ({\n  id,\n  intensity = DEFAULT_INTENSITY,\n  particleCount = DEFAULT_PARTICLE_COUNT,\n  color = DEFAULT_COLOR,\n  windSpeed = DEFAULT_WIND_SPEED,\n  windDirection = DEFAULT_WIND_DIRECTION,\n  fallSpeed = DEFAULT_FALL_SPEED,\n  autoStart = true,\n}: MapSnowProps) => {\n  const { map, isLoaded } = useMap()\n  const [container, setContainer] = useState<HTMLElement | null>(null)\n  const autoId = useId()\n  const controlId = id ?? autoId\n\n  const handleControlReady = (control: SnowControl) => {\n    snowControls.set(controlId, control)\n  }\n\n  useEffect(() => {\n    if (!map || !isLoaded) {\n      return\n    }\n\n    const mapContainer = map.getContainer()\n    setContainer(mapContainer)\n\n    return () => {\n      snowControls.delete(controlId)\n    }\n  }, [map, isLoaded, controlId])\n\n  if (!container) {\n    return null\n  }\n\n  return createPortal(\n    <SnowOverlay\n      intensity={intensity}\n      particleCount={particleCount}\n      color={color}\n      windSpeed={windSpeed}\n      windDirection={windDirection}\n      fallSpeed={fallSpeed}\n      isActive={autoStart}\n      onControlReady={handleControlReady}\n    />,\n    container\n  )\n}\n\nconst SnowOverlay = ({\n  intensity,\n  particleCount,\n  color,\n  windSpeed,\n  windDirection,\n  fallSpeed,\n  isActive: initialActive,\n  onControlReady,\n}: SnowOverlayProps) => {\n  const canvasRef = useRef<HTMLCanvasElement | null>(null)\n  const particlesRef = useRef<SnowParticle[]>([])\n  const animationFrameRef = useRef<number | null>(null)\n  const isActiveRef = useRef(initialActive)\n  const intensityRef = useRef(intensity)\n  const [, forceUpdate] = useState(0)\n\n  const start = useCallback(() => {\n    isActiveRef.current = true\n    forceUpdate((previous) => {\n      return previous + 1\n    })\n  }, [])\n\n  const stop = useCallback(() => {\n    isActiveRef.current = false\n    forceUpdate((previous) => {\n      return previous + 1\n    })\n  }, [])\n\n  const setIntensity = useCallback((newIntensity: number) => {\n    intensityRef.current = Math.max(MIN_INTENSITY, Math.min(MAX_INTENSITY, newIntensity))\n  }, [])\n\n  useEffect(() => {\n    const control: SnowControl = {\n      start,\n      stop,\n      setIntensity,\n      get isActive() {\n        return isActiveRef.current\n      },\n    }\n    onControlReady(control)\n  }, [start, stop, setIntensity, onControlReady])\n\n  useEffect(() => {\n    intensityRef.current = intensity\n  }, [intensity])\n\n  useEffect(() => {\n    const canvas = canvasRef.current\n\n    if (!canvas) {\n      return\n    }\n\n    const context = canvas.getContext(\"2d\")\n\n    if (!context) {\n      return\n    }\n\n    const updateCanvasSize = () => {\n      canvas.width = window.innerWidth\n      canvas.height = window.innerHeight\n    }\n\n    const initParticles = () => {\n      particlesRef.current = []\n      const count = Math.floor(particleCount * intensity)\n\n      for (let index = 0; index < count; index++) {\n        const particle = createParticle(canvas.width, canvas.height, intensity)\n        particle.y = Math.random() * canvas.height\n        particlesRef.current.push(particle)\n      }\n    }\n\n    updateCanvasSize()\n    window.addEventListener(\"resize\", updateCanvasSize)\n    initParticles()\n\n    const particleColor = hexToRgb(color)\n    const windAngle = windDirection * DEGREES_TO_RADIANS\n    const windX = Math.cos(windAngle) * windSpeed\n\n    const animate = () => {\n      context.clearRect(0, 0, canvas.width, canvas.height)\n\n      if (!isActiveRef.current) {\n        animationFrameRef.current = requestAnimationFrame(animate)\n        return\n      }\n\n      const currentIntensity = intensityRef.current\n      const targetCount = Math.floor(particleCount * currentIntensity)\n\n      particlesRef.current = adjustParticleCount(\n        particlesRef.current,\n        targetCount,\n        canvas.width,\n        canvas.height,\n        currentIntensity\n      )\n\n      for (let index = 0; index < particlesRef.current.length; index++) {\n        const particle = particlesRef.current[index]\n\n        updateParticlePosition(particle, windX, fallSpeed)\n\n        if (isParticleOutOfBounds(particle, canvas.width, canvas.height)) {\n          particlesRef.current[index] = createParticle(canvas.width, canvas.height, currentIntensity)\n          particlesRef.current[index].x = Math.random() * (canvas.width + BOUNDS_PADDING * 2) - BOUNDS_PADDING\n          continue\n        }\n\n        drawParticle(context, particle, particleColor, currentIntensity)\n      }\n\n      animationFrameRef.current = requestAnimationFrame(animate)\n    }\n\n    animationFrameRef.current = requestAnimationFrame(animate)\n\n    return () => {\n      window.removeEventListener(\"resize\", updateCanvasSize)\n      if (animationFrameRef.current) {\n        cancelAnimationFrame(animationFrameRef.current)\n      }\n    }\n  }, [color, windSpeed, windDirection, fallSpeed, particleCount, intensity])\n\n  return (\n    <canvas\n      ref={canvasRef}\n      style={{\n        position: \"absolute\",\n        top: 0,\n        left: 0,\n        width: \"100%\",\n        height: \"100%\",\n        pointerEvents: \"none\",\n        zIndex: 10,\n      }}\n    />\n  )\n}\n\nexport const useSnowControl = (id: string): SnowControl | null => {\n  const [, forceUpdate] = useState(0)\n\n  useEffect(() => {\n    const interval = setInterval(() => {\n      forceUpdate((previous) => {\n        return previous + 1\n      })\n    }, CONTROL_UPDATE_INTERVAL)\n\n    return () => {\n      clearInterval(interval)\n    }\n  }, [])\n\n  return snowControls.get(id) || null\n}\n",
      "type": "registry:ui",
      "target": "components/ui/map/snow.tsx"
    }
  ],
  "type": "registry:ui"
}
