You are given a task to integrate an existing React component in the codebase

The codebase should support:
- shadcn project structure  
- Tailwind CSS
- Typescript

If it doesn't, provide instructions on how to setup project via shadcn CLI, install Tailwind or Typescript.

Determine the default path for components and styles. 
If default path for components is not /components/ui, provide instructions on why it's important to create this folder
Copy-paste this component to /components/ui folder:
```tsx
meta-ball-hero.tsx
'use client';

import React, { FC, useEffect, useRef } from 'react';
import { Renderer, Program, Mesh, Triangle, Transform, Vec3, Camera } from 'ogl';

const MAX_BALLS = 50;
const TWO_PI = Math.PI * 2;

type MetaBallsProps = {
  color?: string;
  speed?: number;
  enableMouseInteraction?: boolean;
  hoverSmoothness?: number;
  animationSize?: number;
  ballCount?: number;
  clumpFactor?: number;
  cursorBallSize?: number;
  cursorBallColor?: string;
  enableTransparency?: boolean;
  className?: string;
  style?: React.CSSProperties;
};

type BallParams = {
  st: number;
  dtFactor: number;
  baseScale: number;
  toggle: 0 | 1;
  radius: number;
};

type Vec3Tuple = [number, number, number];

function clamp(n: number, min: number, max: number) {
  return Math.min(max, Math.max(min, n));
}

function hexToRgb01(hex: string | undefined): Vec3Tuple {
  if (!hex) return [1, 1, 1];
  let c = hex.trim().replace('#', '');
  if (c.length === 3) c = c.split('').map((ch) => ch + ch).join('');
  if (!/^[0-9a-fA-F]{6}$/.test(c)) return [1, 1, 1];
  return [0, 2, 4].map((i) => parseInt(c.slice(i, i + 2), 16) / 255) as Vec3Tuple;
}

function fract(x: number) {
  return x - Math.floor(x);
}

function hash31(n: number): Vec3Tuple {
  let r0 = fract(n * 0.1031);
  let r1 = fract(n * 0.103);
  let r2 = fract(n * 0.0973);
  const dotVal = r0 * (r1 + 33.33) + r1 * (r2 + 33.33) + r2 * (r0 + 33.33);
  r0 = fract(r0 + dotVal);
  r1 = fract(r1 + dotVal);
  r2 = fract(r2 + dotVal);
  return [r0, r1, r2];
}

function hash33(v: Vec3Tuple): Vec3Tuple {
  let p0 = fract(v[0] * 0.1031);
  let p1 = fract(v[1] * 0.103);
  let p2 = fract(v[2] * 0.0973);
  const dotVal = p0 * (p1 + 33.33) + p1 * (p2 + 33.33) + p2 * (p0 + 33.33);
  p0 = fract(p0 + dotVal);
  p1 = fract(p1 + dotVal);
  p2 = fract(p2 + dotVal);
  const r0 = fract((p0 + p1) * p2);
  const r1 = fract((p0 + p0) * p1);
  const r2 = fract((p1 + p0) * p0);
  return [r0, r1, r2];
}

const vertex = `#version 300 es
precision highp float;
layout(location = 0) in vec2 position;
void main() {
  gl_Position = vec4(position, 0.0, 1.0);
}
`;

const fragment = `#version 300 es
precision highp float;

uniform vec3  iResolution;
uniform float iTime;
uniform vec3  iMouse;

uniform vec3  iColor;
uniform vec3  iCursorColor;
uniform float iAnimationSize;
uniform int   iBallCount;
uniform float iCursorBallSize;
uniform vec3  iMetaBalls[50];
uniform bool  enableTransparency;

out vec4 outColor;

float metaVal(vec2 c, float r, vec2 p) {
  vec2 d = p - c;
  float dist2 = dot(d, d);
  return (dist2 < 1e-6) ? 1e6 : (r * r) / dist2;
}

void main() {
  vec2 fc = gl_FragCoord.xy;
  float scale = iAnimationSize / max(1.0, iResolution.y);
  vec2 coord = (fc - iResolution.xy * 0.5) * scale;
  vec2 mouseW = (iMouse.xy - iResolution.xy * 0.5) * scale;

  float m1 = 0.0;
  for (int i = 0; i < 50; i++) {
    if (i >= iBallCount) break;
    m1 += metaVal(iMetaBalls[i].xy, iMetaBalls[i].z, coord);
  }

  float m2 = metaVal(mouseW, iCursorBallSize, coord);
  float total = m1 + m2;

  float f = smoothstep(-1.0, 1.0, (total - 1.3) / max(0.0001, fwidth(total)));

  vec3 cFinal = vec3(0.0);
  if (total > 0.0) {
    float a1 = m1 / total;
    float a2 = m2 / total;
    cFinal = iColor * a1 + iCursorColor * a2;
  }

  outColor = vec4(cFinal * f, enableTransparency ? f : 1.0);
}
`;

function makeBallParams(count: number): BallParams[] {
  const arr: BallParams[] = [];
  for (let i = 0; i < count; i++) {
    const idx = i + 1;
    const h1 = hash31(idx);
    const st = h1[0] * TWO_PI;
    const dtFactor = 0.1 * Math.PI + h1[1] * (0.4 * Math.PI - 0.1 * Math.PI);
    const baseScale = 5.0 + h1[1] * (10.0 - 5.0);
    const h2 = hash33(h1);
    const toggle = (Math.floor(h2[0] * 2.0) % 2) as 0 | 1;
    const radius = 0.5 + h2[2] * (2.0 - 0.5);
    arr.push({ st, dtFactor, baseScale, toggle, radius });
  }
  return arr;
}

const MetaBalls: FC<MetaBallsProps> = ({
  color = '#00ffff',
  speed = 0.5,
  enableMouseInteraction = true,
  hoverSmoothness = 0.08,
  animationSize = 35,
  ballCount = 200,
  clumpFactor = 5.2,
  cursorBallSize = 4,
  cursorBallColor = '#ff00ff',
  enableTransparency = true,
  className = '',
  style = {},
}) => {
  const containerRef = useRef<HTMLDivElement>(null);

  const rendererRef = useRef<Renderer | null>(null);
  const programRef = useRef<Program | null>(null);
  const meshRef = useRef<Mesh | null>(null);
  const cameraRef = useRef<Camera | null>(null);
  const sceneRef = useRef<Transform | null>(null);

  const metaBallsUniformArrayRef = useRef<Vec3[]>([]);
  const ballParamsArrayRef = useRef<BallParams[]>([]);
  const mouseBallPosRef = useRef({ x: 0, y: 0 });
  const pointerStateRef = useRef({ inside: false, x: 0, y: 0 });

  const rafRef = useRef<number | null>(null);
  const roRef = useRef<ResizeObserver | null>(null);

  // live config consumed by RAF without re-init
  const configRef = useRef({
    speed,
    hoverSmoothness,
    clumpFactor,
    enableMouseInteraction,
    ballCount: clamp(ballCount, 0, MAX_BALLS),
  });

  // keep config in sync
  useEffect(() => {
    configRef.current.speed = speed;
  }, [speed]);

  useEffect(() => {
    configRef.current.hoverSmoothness = hoverSmoothness;
  }, [hoverSmoothness]);

  useEffect(() => {
    configRef.current.clumpFactor = clumpFactor;
  }, [clumpFactor]);

  useEffect(() => {
    configRef.current.enableMouseInteraction = enableMouseInteraction;
  }, [enableMouseInteraction]);

  useEffect(() => {
    const count = clamp(ballCount, 0, MAX_BALLS);
    configRef.current.ballCount = count;
    programRef.current && (programRef.current.uniforms.iBallCount.value = count);
    if (ballParamsArrayRef.current.length !== count) {
      ballParamsArrayRef.current = makeBallParams(count);
    }
  }, [ballCount]);

  // update uniforms that can change without re-init
  useEffect(() => {
    const prog = programRef.current;
    if (!prog) return;
    const [r, g, b] = hexToRgb01(color);
    prog.uniforms.iColor.value.set(r, g, b);
  }, [color]);

  useEffect(() => {
    const prog = programRef.current;
    if (!prog) return;
    const [r, g, b] = hexToRgb01(cursorBallColor);
    prog.uniforms.iCursorColor.value.set(r, g, b);
  }, [cursorBallColor]);

  useEffect(() => {
    const prog = programRef.current;
    if (prog) prog.uniforms.iAnimationSize.value = animationSize;
  }, [animationSize]);

  useEffect(() => {
    const prog = programRef.current;
    if (prog) prog.uniforms.iCursorBallSize.value = cursorBallSize;
  }, [cursorBallSize]);

  useEffect(() => {
    const gl = rendererRef.current?.gl || null;
    const prog = programRef.current;
    if (gl) gl.clearColor(0, 0, 0, enableTransparency ? 0 : 1);
    if (prog) prog.uniforms.enableTransparency.value = enableTransparency;
  }, [enableTransparency]);

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;

    const renderer = new Renderer({
      dpr: Math.min(2, window.devicePixelRatio || 1),
      alpha: true,
      premultipliedAlpha: false,
      // antialias: false by default; metaballs look crisp with derivatives
    });
    rendererRef.current = renderer;

    const gl = renderer.gl;
    gl.clearColor(0, 0, 0, enableTransparency ? 0 : 1);
    gl.canvas.style.width = '100%';
    gl.canvas.style.height = '100%';
    container.appendChild(gl.canvas);

    const camera = new Camera(gl, {
      left: -1,
      right: 1,
      top: 1,
      bottom: -1,
      near: 0.1,
      far: 10,
    });
    camera.position.z = 1;
    cameraRef.current = camera;

    const scene = new Transform();
    sceneRef.current = scene;

    const geometry = new Triangle(gl);

    const [r1, g1, b1] = hexToRgb01(color);
    const [r2, g2, b2] = hexToRgb01(cursorBallColor);

    metaBallsUniformArrayRef.current = Array.from({ length: MAX_BALLS }, () => new Vec3(0, 0, 0));

    const program = new Program(gl, {
      vertex,
      fragment,
      uniforms: {
        iTime: { value: 0 },
        iResolution: { value: new Vec3(0, 0, 0) },
        iMouse: { value: new Vec3(0, 0, 0) },
        iColor: { value: new Vec3(r1, g1, b1) },
        iCursorColor: { value: new Vec3(r2, g2, b2) },
        iAnimationSize: { value: animationSize },
        iBallCount: { value: clamp(ballCount, 0, MAX_BALLS) },
        iCursorBallSize: { value: cursorBallSize },
        iMetaBalls: { value: metaBallsUniformArrayRef.current },
        enableTransparency: { value: enableTransparency },
      },
    });
    programRef.current = program;

    const mesh = new Mesh(gl, { geometry, program });
    meshRef.current = mesh;
    mesh.setParent(scene);

    // generate ball params once based on count
    const effectiveCount = clamp(ballCount, 0, MAX_BALLS);
    ballParamsArrayRef.current = makeBallParams(effectiveCount);

    // start mouse at center
    mouseBallPosRef.current = { x: gl.drawingBufferWidth / 2, y: gl.drawingBufferHeight / 2 };
    pointerStateRef.current = { inside: false, x: 0, y: 0 };

    const resize = () => {
      const cont = containerRef.current;
      const rend = rendererRef.current;
      const prog = programRef.current;
      if (!cont || !rend || !prog) return;
      const dpr = Math.min(2, window.devicePixelRatio || 1);
      const w = Math.max(1, cont.clientWidth);
      const h = Math.max(1, cont.clientHeight);
      rend.setSize(w * dpr, h * dpr);
      prog.uniforms.iResolution.value.set(rend.gl.drawingBufferWidth, rend.gl.drawingBufferHeight, 0);
    };

    const ro = new ResizeObserver(resize);
    ro.observe(container);
    roRef.current = ro;
    window.addEventListener('resize', resize, { passive: true });
    resize();

    const onMove = (e: PointerEvent) => {
      if (!configRef.current.enableMouseInteraction) return;
      const cont = containerRef.current;
      const rend = rendererRef.current;
      if (!cont || !rend) return;
      const rect = cont.getBoundingClientRect();
      const px = e.clientX - rect.left;
      const py = e.clientY - rect.top;
      pointerStateRef.current.x = (px / rect.width) * rend.gl.drawingBufferWidth;
      pointerStateRef.current.y = (1 - py / rect.height) * rend.gl.drawingBufferHeight;
    };
    const onEnter = () => {
      if (configRef.current.enableMouseInteraction) pointerStateRef.current.inside = true;
    };
    const onLeave = () => {
      if (configRef.current.enableMouseInteraction) pointerStateRef.current.inside = false;
    };
    container.addEventListener('pointermove', onMove);
    container.addEventListener('pointerenter', onEnter);
    container.addEventListener('pointerleave', onLeave);

    const startTime = performance.now();

    const frame = (t: number) => {
      rafRef.current = requestAnimationFrame(frame);

      const rend = rendererRef.current;
      const prog = programRef.current;
      const scn = sceneRef.current;
      const cam = cameraRef.current;
      if (!rend || !prog || !scn || !cam) return;

      const elapsed = (t - startTime) * 0.001;
      prog.uniforms.iTime.value = elapsed;

      const { speed: spd, clumpFactor: clump, hoverSmoothness: smooth, ballCount: count } = configRef.current;
      prog.uniforms.iBallCount.value = count;

      const params = ballParamsArrayRef.current;
      for (let i = 0; i < count; i++) {
        const p = params[i];
        const dt = elapsed * spd * p.dtFactor;
        const th = p.st + dt;
        const x = Math.cos(th);
        const y = Math.sin(th + dt * p.toggle);
        const posX = x * p.baseScale * clump;
        const posY = y * p.baseScale * clump;
        metaBallsUniformArrayRef.current[i].set(posX, posY, p.radius);
      }

      const glLocal = rend.gl;
      let targetX: number;
      let targetY: number;
      if (pointerStateRef.current.inside) {
        targetX = pointerStateRef.current.x;
        targetY = pointerStateRef.current.y;
      } else {
        const cx = glLocal.drawingBufferWidth * 0.5;
        const cy = glLocal.drawingBufferHeight * 0.5;
        const rx = glLocal.drawingBufferWidth * 0.15;
        const ry = glLocal.drawingBufferHeight * 0.15;
        targetX = cx + Math.cos(elapsed * spd) * rx;
        targetY = cy + Math.sin(elapsed * spd) * ry;
      }

      mouseBallPosRef.current.x += (targetX - mouseBallPosRef.current.x) * smooth;
      mouseBallPosRef.current.y += (targetY - mouseBallPosRef.current.y) * smooth;
      prog.uniforms.iMouse.value.set(mouseBallPosRef.current.x, mouseBallPosRef.current.y, 0);

      rend.render({ scene: scn, camera: cam });
    };

    rafRef.current = requestAnimationFrame(frame);

    return () => {
      if (rafRef.current) cancelAnimationFrame(rafRef.current);
      ro.disconnect();
      window.removeEventListener('resize', resize);
      container.removeEventListener('pointermove', onMove);
      container.removeEventListener('pointerenter', onEnter);
      container.removeEventListener('pointerleave', onLeave);

      const rendererToDispose = rendererRef.current;
      if (rendererToDispose) {
        const canvas = rendererToDispose.gl.canvas as HTMLCanvasElement;
        if (canvas.parentNode === container) container.removeChild(canvas);
        const lose = rendererToDispose.gl.getExtension('WEBGL_lose_context');
        lose?.loseContext();
      }

      rendererRef.current = null;
      programRef.current = null;
      meshRef.current = null;
      cameraRef.current = null;
      sceneRef.current = null;
    };
  }, []); // init once

  return (
    <div
      ref={containerRef}
      className={`metaballs-container relative w-full h-full ${className}`}
      style={style}
    />
  );
};

export default MetaBalls;

demo.tsx
import MetaBalls from "@/components/ui/meta-ball-hero";

export default function DemoOne() {
  return (
    <div className="relative w-full h-screen bg-background overflow-hidden">
      <div className="absolute inset-0 w-full h-full">
        <MetaBalls
          color="#20f2ff"
          cursorBallColor="#f30010"
          speed={0.95}
          ballCount={7}
          animationSize={22}
          enableMouseInteraction={false}
          enableTransparency={false}
          hoverSmoothness={0.08}
          clumpFactor={0.64}
          cursorBallSize={3}
        />
      </div>

      <div className="relative z-10 flex flex-col items-center justify-center h-full text-center px-4">
        <h1 className="text-6xl md:text-8xl font-bold text-foreground mb-6 mix-blend-difference">
          HERO
        </h1>
        <h2 className="text-4xl md:text-6xl font-light text-foreground mb-8 mix-blend-difference">
          MetaBalls
        </h2>
        <p className="text-lg md:text-xl text-muted-foreground max-w-2xl leading-relaxed mix-blend-difference">
          Experience fluid, organic shapes. Built with WebGL and advanced shader techniques for smooth, real-time animation.
        </p>
      </div>
    </div>
  );
};

export function DemoTwo() {
  return (
    <div className="relative w-full h-screen bg-background overflow-hidden">
      <div className="absolute inset-0 w-full h-full">
        <MetaBalls
          color="#00ffff"
          cursorBallColor="#ff00ff"
          speed={0.5}
          ballCount={5}
          animationSize={35}
          enableMouseInteraction={true}
          enableTransparency={true}
          hoverSmoothness={0.08}
          clumpFactor={0.4}
          cursorBallSize={2}
        />
      </div>
    </div>
  );
};
```

Install NPM dependencies:
```bash
ogl
```

Implementation Guidelines
 1. Analyze the component structure and identify all required dependencies
 2. Review the component's argumens and state
 3. Identify any required context providers or hooks and install them
 4. Questions to Ask
 - What data/props will be passed to this component?
 - Are there any specific state management requirements?
 - Are there any required assets (images, icons, etc.)?
 - What is the expected responsive behavior?
 - What is the best place to use this component in the app?

Steps to integrate
 0. Copy paste all the code above in the correct directories
 1. Install external dependencies
 2. Fill image assets with Unsplash stock images you know exist
 3. Use lucide-react icons for svgs or logos if component requires them
