{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "exploding-input",
  "title": "Exploding Input",
  "description": "Input component that spawns particle effects when typing.",
  "dependencies": [],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/spell-ui/exploding-input.tsx",
      "content": "\"use client\";\n\nimport React, {\n  useEffect,\n  useRef,\n  useCallback,\n  type ReactNode,\n  type CSSProperties,\n} from \"react\";\n\ntype HorizontalDirection = \"left\" | \"center\" | \"right\";\ntype VerticalDirection = \"top\" | \"center\" | \"bottom\";\n\ninterface ExplodingInputProps {\n  /** Content to render as particles (React nodes) */\n  content?: ReactNode[];\n  /** Number of particles to spawn per input event */\n  count?: number;\n  /** Direction of particle movement */\n  direction?: {\n    horizontal?: HorizontalDirection;\n    vertical?: VerticalDirection;\n  };\n  /** Gravity value from -1 to 1 (negative = upward, positive = downward) */\n  gravity?: number;\n  /** Duration of particle animation in seconds */\n  duration?: number;\n  /** Scale configuration for particles */\n  scale?: {\n    value?: number;\n    randomize?: boolean;\n    randomVariation?: number;\n  };\n  /** Rotation configuration for particles */\n  rotation?: {\n    value?: number;\n    animate?: boolean;\n  };\n  /** Custom styles for the container */\n  style?: CSSProperties;\n  /** Class name for the container */\n  className?: string;\n}\n\ninterface Particle {\n  id: number;\n  x: number;\n  y: number;\n  scale: number;\n  rotate: number;\n  opacity: number;\n  vx: number;\n  vy: number;\n  gravity: number;\n  birthTime: number;\n  lifeMs: number;\n  contentIdx: number;\n  scaleStart: number;\n  scaleEnd: number;\n  rotateStart: number;\n  rotateEnd: number;\n  element: HTMLDivElement;\n  isDead: boolean;\n}\n\nfunction mapLinear(\n  value: number,\n  inMin: number,\n  inMax: number,\n  outMin: number,\n  outMax: number\n): number {\n  if (inMax === inMin) return outMin;\n  const t = (value - inMin) / (inMax - inMin);\n  return outMin + t * (outMax - outMin);\n}\n\nfunction createPRNG(seed: number): () => number {\n  let s = seed;\n  return function () {\n    s |= 0;\n    s = (s + 1831565813) | 0;\n    let t = Math.imul(s ^ (s >>> 15), 1 | s);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;\n  };\n}\n\nexport function ExplodingInput({\n  content = [],\n  count = 1,\n  direction = { horizontal: \"center\", vertical: \"top\" },\n  gravity = 0.7,\n  duration = 3,\n  scale = { value: 1, randomize: false, randomVariation: 0 },\n  rotation = { value: 0, animate: false },\n  style,\n  className,\n}: ExplodingInputProps) {\n  const particleIdCounter = useRef(0);\n  const containerRef = useRef<HTMLDivElement>(null);\n  const particleContainerRef = useRef<HTMLDivElement>(null);\n  const particlesRef = useRef<Particle[]>([]);\n  const randRef = useRef<() => number>(() => Math.random());\n  const inputRef = useRef<HTMLInputElement | null>(null);\n  const rafIdRef = useRef<number | null>(null);\n\n  // Initialize PRNG and cleanup on unmount\n  useEffect(() => {\n    const timeBits = (Date.now() & 4294967295) >>> 0;\n    const extra = Math.floor(Math.random() * 4294967295) >>> 0;\n    const seed = (timeBits ^ extra) >>> 0;\n    randRef.current = createPRNG(seed);\n\n    return () => {\n      particlesRef.current.forEach((p) => {\n        if (p.element && p.element.parentNode) {\n          p.element.parentNode.removeChild(p.element);\n        }\n      });\n      particlesRef.current = [];\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current);\n      }\n    };\n  }, []);\n\n  const getInputSpawnPosition = useCallback(\n    (input: HTMLInputElement): { x: number; y: number } | null => {\n      const container = containerRef.current;\n      if (!container || !input) return null;\n\n      const inputRect = input.getBoundingClientRect();\n      const containerRect = container.getBoundingClientRect();\n      const inputValue = input.value;\n\n      const getTextWidth = (text: string, inp: HTMLInputElement): number => {\n        const canvas = document.createElement(\"canvas\");\n        const context = canvas.getContext(\"2d\");\n        if (!context) return 0;\n        const computedStyle = window.getComputedStyle(inp);\n        context.font = `${computedStyle.fontSize} ${computedStyle.fontFamily}`;\n        return context.measureText(text).width;\n      };\n\n      const computedStyle = window.getComputedStyle(input);\n      const paddingLeft = parseInt(computedStyle.paddingLeft, 10) || 0;\n      const paddingRight = parseInt(computedStyle.paddingRight, 10) || 0;\n\n      let x = 0;\n      let y = 0;\n\n      if (inputValue.length > 0) {\n        const textWidth = getTextWidth(inputValue, input);\n        const inputStartX = inputRect.left - containerRect.left;\n        const maxX = inputStartX + inputRect.width - paddingRight;\n        x = Math.min(textWidth + inputStartX + paddingLeft, maxX);\n      } else {\n        x = inputRect.left - containerRect.left;\n      }\n      y = inputRect.top - containerRect.top + inputRect.height / 2;\n\n      return { x, y };\n    },\n    []\n  );\n\n  const createParticlesAtPosition = useCallback(\n    (x: number, y: number) => {\n      const spawnOne = () => {\n        const horizontalValue =\n          direction.horizontal === \"left\"\n            ? -0.4\n            : direction.horizontal === \"right\"\n              ? 0.4\n              : 0;\n        const baseVx = mapLinear(horizontalValue, -1, 1, -800, 800);\n        const spreadVx = 300;\n        const vx = baseVx + (randRef.current() * 2 - 1) * spreadVx;\n\n        const verticalValue =\n          direction.vertical === \"top\"\n            ? -0.7\n            : direction.vertical === \"bottom\"\n              ? 0.7\n              : 0;\n        const baseVy = mapLinear(verticalValue, -1, 1, -800, 800);\n        const spreadVy = 300;\n        const vy = baseVy + (randRef.current() * 2 - 1) * spreadVy;\n\n        particleIdCounter.current += 1;\n\n        const randBetween = (min: number, max: number) =>\n          min + randRef.current() * (max - min);\n\n        const baseScale = scale.value ?? 1;\n        let particleScale = baseScale;\n        if (\n          scale.randomize &&\n          scale.randomVariation !== undefined &&\n          scale.randomVariation > 0\n        ) {\n          const variation = (scale.randomVariation / 100) * baseScale;\n          const minScale = baseScale - variation;\n          const maxScale = baseScale + variation;\n          particleScale = randBetween(minScale, maxScale);\n        }\n        const safeScale = Math.max(0.1, Math.min(4, particleScale));\n\n        const baseRotation = rotation.value ?? 0;\n        let initRot = baseRotation;\n        let endRot = baseRotation;\n        if (rotation.animate) {\n          initRot = randBetween(-180, 180);\n          const rotationDelta = randBetween(-360, 360);\n          endRot = initRot + rotationDelta;\n        }\n\n        const el = document.createElement(\"div\");\n        el.style.position = \"absolute\";\n        el.style.left = \"0\";\n        el.style.top = \"0\";\n        el.style.display = \"flex\";\n        el.style.alignItems = \"center\";\n        el.style.justifyContent = \"center\";\n        el.style.pointerEvents = \"none\";\n        el.style.willChange = \"transform, opacity\";\n        el.style.transformOrigin = \"50% 50%\";\n        el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -50%) scale(${safeScale}) rotate(${initRot}deg)`;\n        el.style.opacity = \"1\";\n\n        if (particleContainerRef.current) {\n          particleContainerRef.current.appendChild(el);\n        }\n\n        const newParticle: Particle = {\n          id: particleIdCounter.current,\n          x,\n          y,\n          scale: safeScale,\n          rotate: initRot,\n          opacity: 1,\n          vx,\n          vy,\n          gravity: mapLinear(\n            Math.max(-1, Math.min(1, gravity ?? 0.45)),\n            -1,\n            1,\n            -2000,\n            2000\n          ),\n          birthTime: performance.now(),\n          lifeMs: duration * 1000,\n          contentIdx:\n            content.length > 0\n              ? (particleIdCounter.current - 1) % content.length\n              : -1,\n          scaleStart: safeScale,\n          scaleEnd: safeScale,\n          rotateStart: initRot,\n          rotateEnd: endRot,\n          element: el,\n          isDead: false,\n        };\n\n        // Render content\n        if (content.length > 0 && newParticle.contentIdx >= 0) {\n          const contentElement = content[newParticle.contentIdx];\n          if (contentElement) {\n            import(\"react-dom/client\").then(({ createRoot }) => {\n              const root = createRoot(el);\n              root.render(<>{contentElement}</>);\n            });\n          }\n        } else {\n          const fallback = document.createElement(\"div\");\n          fallback.style.width = \"16px\";\n          fallback.style.height = \"16px\";\n          fallback.style.borderRadius = \"6px\";\n          fallback.style.backgroundColor = \"#6366f1\";\n          el.appendChild(fallback);\n        }\n\n        particlesRef.current.push(newParticle);\n\n        setTimeout(() => {\n          newParticle.isDead = true;\n          if (newParticle.element && newParticle.element.parentNode) {\n            newParticle.element.parentNode.removeChild(newParticle.element);\n          }\n          particlesRef.current = particlesRef.current.filter(\n            (p) => p.id !== newParticle.id\n          );\n        }, duration * 1000);\n      };\n\n      const particlesToSpawn = Math.max(1, Math.min(5, Math.round(count)));\n      for (let i = 0; i < particlesToSpawn; i++) spawnOne();\n    },\n    [content, count, direction, duration, gravity, rotation, scale]\n  );\n\n  // Find input element and listen to changes\n  useEffect(() => {\n    const container = containerRef.current;\n    if (!container) return;\n\n    const label = container.closest(\"label\");\n    const input = label?.querySelector(\"input\") ?? null;\n    if (!input) return;\n\n    inputRef.current = input;\n\n    const handleInput = () => {\n      const pos = getInputSpawnPosition(input);\n      if (pos) {\n        createParticlesAtPosition(pos.x, pos.y);\n      }\n    };\n\n    input.addEventListener(\"input\", handleInput);\n    return () => {\n      input.removeEventListener(\"input\", handleInput);\n      inputRef.current = null;\n    };\n  }, [createParticlesAtPosition, getInputSpawnPosition]);\n\n  // Physics animation loop\n  useEffect(() => {\n    let lastTime = performance.now();\n\n    const updateParticles = (currentTime: number) => {\n      const delta = currentTime - lastTime;\n      lastTime = currentTime;\n      const dtMs = Math.min(32, delta);\n      const dt = dtMs / 1000;\n      const now = performance.now();\n\n      particlesRef.current.forEach((p) => {\n        if (p.isDead) return;\n        const age = now - p.birthTime;\n        if (!p.element || age >= p.lifeMs) return;\n\n        const progress = age / p.lifeMs;\n\n        p.vy = p.vy + p.gravity * dt;\n        p.x = p.x + p.vx * dt;\n        p.y = p.y + p.vy * dt;\n\n        p.scale = mapLinear(progress, 0, 1, p.scaleStart, p.scaleEnd);\n        p.rotate = mapLinear(progress, 0, 1, p.rotateStart, p.rotateEnd);\n\n        const fadeStart = 0.7;\n        p.opacity = progress > fadeStart ? mapLinear(progress, fadeStart, 1, 1, 0) : 1;\n\n        if (isNaN(p.x) || isNaN(p.y) || isNaN(p.scale)) return;\n\n        const clampedScale = Math.max(0.1, Math.min(3, p.scale));\n        p.element.style.transform = `translate(${p.x}px, ${p.y}px) translate(-50%, -50%) scale(${clampedScale}) rotate(${p.rotate}deg)`;\n        p.element.style.opacity = String(p.opacity);\n      });\n\n      rafIdRef.current = requestAnimationFrame(updateParticles);\n    };\n\n    rafIdRef.current = requestAnimationFrame(updateParticles);\n\n    return () => {\n      if (rafIdRef.current !== null) {\n        cancelAnimationFrame(rafIdRef.current);\n      }\n    };\n  }, []);\n\n  return (\n    <div\n      ref={containerRef}\n      className={className}\n      style={{\n        ...style,\n        position: \"relative\",\n        width: \"0px\",\n        height: \"0px\",\n        overflow: \"visible\",\n        backgroundColor: \"transparent\",\n        transform: \"translateZ(0)\",\n      }}\n    >\n      <div\n        ref={particleContainerRef}\n        style={{\n          position: \"absolute\",\n          left: 0,\n          top: 0,\n          width: \"100%\",\n          height: \"100%\",\n          pointerEvents: \"none\",\n        }}\n      />\n    </div>\n  );\n}\n\nexport default ExplodingInput;",
      "type": "registry:component"
    }
  ],
  "type": "registry:component"
}