{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "hole-background",
  "type": "registry:ui",
  "title": "Hole Background",
  "description": "A background with a hole animation effect.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/backgrounds/hole/index.tsx",
      "content": "'use client';\n\nimport * as React from 'react';\nimport { motion } from 'motion/react';\n\nimport { cn } from '@/lib/utils';\n\ntype HoleBackgroundProps = React.ComponentProps<'div'> & {\n  strokeColor?: string;\n  numberOfLines?: number;\n  numberOfDiscs?: number;\n  particleRGBColor?: [number, number, number];\n};\n\nfunction HoleBackground({\n  strokeColor = '#737373',\n  numberOfLines = 50,\n  numberOfDiscs = 50,\n  particleRGBColor = [255, 255, 255],\n  className,\n  children,\n  ...props\n}: HoleBackgroundProps) {\n  const canvasRef = React.useRef<HTMLCanvasElement>(null);\n  const animationFrameIdRef = React.useRef<number>(0);\n  const stateRef = React.useRef<any>({\n    discs: [] as any[],\n    lines: [] as any[],\n    particles: [] as any[],\n    clip: {},\n    startDisc: {},\n    endDisc: {},\n    rect: { width: 0, height: 0 },\n    render: { width: 0, height: 0, dpi: 1 },\n    particleArea: {},\n    linesCanvas: null,\n  });\n\n  const linear = (p: number) => p;\n  const easeInExpo = (p: number) => (p === 0 ? 0 : Math.pow(2, 10 * (p - 1)));\n\n  const tweenValue = React.useCallback(\n    (start: number, end: number, p: number, ease: 'inExpo' | null = null) => {\n      const delta = end - start;\n      const easeFn = ease === 'inExpo' ? easeInExpo : linear;\n      return start + delta * easeFn(p);\n    },\n    [],\n  );\n\n  const tweenDisc = React.useCallback(\n    (disc: any) => {\n      const { startDisc, endDisc } = stateRef.current;\n      disc.x = tweenValue(startDisc.x, endDisc.x, disc.p);\n      disc.y = tweenValue(startDisc.y, endDisc.y, disc.p, 'inExpo');\n      disc.w = tweenValue(startDisc.w, endDisc.w, disc.p);\n      disc.h = tweenValue(startDisc.h, endDisc.h, disc.p);\n    },\n    [tweenValue],\n  );\n\n  const setSize = React.useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const rect = canvas.getBoundingClientRect();\n    stateRef.current.rect = { width: rect.width, height: rect.height };\n    stateRef.current.render = {\n      width: rect.width,\n      height: rect.height,\n      dpi: window.devicePixelRatio || 1,\n    };\n    canvas.width = stateRef.current.render.width * stateRef.current.render.dpi;\n    canvas.height =\n      stateRef.current.render.height * stateRef.current.render.dpi;\n  }, []);\n\n  const setDiscs = React.useCallback(() => {\n    const { width, height } = stateRef.current.rect;\n    stateRef.current.discs = [];\n    stateRef.current.startDisc = {\n      x: width * 0.5,\n      y: height * 0.45,\n      w: width * 0.75,\n      h: height * 0.7,\n    };\n    stateRef.current.endDisc = {\n      x: width * 0.5,\n      y: height * 0.95,\n      w: 0,\n      h: 0,\n    };\n    let prevBottom = height;\n    stateRef.current.clip = {};\n    for (let i = 0; i < numberOfDiscs; i++) {\n      const p = i / numberOfDiscs;\n      const disc = { p, x: 0, y: 0, w: 0, h: 0 };\n      tweenDisc(disc);\n      const bottom = disc.y + disc.h;\n      if (bottom <= prevBottom) {\n        stateRef.current.clip = { disc: { ...disc }, i };\n      }\n      prevBottom = bottom;\n      stateRef.current.discs.push(disc);\n    }\n    const clipPath = new Path2D();\n    const disc = stateRef.current.clip.disc;\n    clipPath.ellipse(disc.x, disc.y, disc.w, disc.h, 0, 0, Math.PI * 2);\n    clipPath.rect(disc.x - disc.w, 0, disc.w * 2, disc.y);\n    stateRef.current.clip.path = clipPath;\n  }, [numberOfDiscs, tweenDisc]);\n\n  const setLines = React.useCallback(() => {\n    const { width, height } = stateRef.current.rect;\n    stateRef.current.lines = [];\n    const linesAngle = (Math.PI * 2) / numberOfLines;\n    for (let i = 0; i < numberOfLines; i++) {\n      stateRef.current.lines.push([]);\n    }\n    stateRef.current.discs.forEach((disc: any) => {\n      for (let i = 0; i < numberOfLines; i++) {\n        const angle = i * linesAngle;\n        const p = {\n          x: disc.x + Math.cos(angle) * disc.w,\n          y: disc.y + Math.sin(angle) * disc.h,\n        };\n        stateRef.current.lines[i].push(p);\n      }\n    });\n    const offCanvas = document.createElement('canvas');\n    offCanvas.width = width;\n    offCanvas.height = height;\n    const ctx = offCanvas.getContext('2d');\n    if (!ctx) return;\n    stateRef.current.lines.forEach((line: any) => {\n      ctx.save();\n      let lineIsIn = false;\n      line.forEach((p1: any, j: number) => {\n        if (j === 0) return;\n        const p0 = line[j - 1];\n        if (\n          !lineIsIn &&\n          (ctx.isPointInPath(stateRef.current.clip.path, p1.x, p1.y) ||\n            ctx.isPointInStroke(stateRef.current.clip.path, p1.x, p1.y))\n        ) {\n          lineIsIn = true;\n        } else if (lineIsIn) {\n          ctx.clip(stateRef.current.clip.path);\n        }\n        ctx.beginPath();\n        ctx.moveTo(p0.x, p0.y);\n        ctx.lineTo(p1.x, p1.y);\n        ctx.strokeStyle = strokeColor;\n        ctx.lineWidth = 2;\n        ctx.stroke();\n        ctx.closePath();\n      });\n      ctx.restore();\n    });\n    stateRef.current.linesCanvas = offCanvas;\n  }, [numberOfLines, strokeColor]);\n\n  const initParticle = React.useCallback(\n    (start: boolean = false) => {\n      const sx =\n        stateRef.current.particleArea.sx +\n        stateRef.current.particleArea.sw * Math.random();\n      const ex =\n        stateRef.current.particleArea.ex +\n        stateRef.current.particleArea.ew * Math.random();\n      const dx = ex - sx;\n      const y = start\n        ? stateRef.current.particleArea.h * Math.random()\n        : stateRef.current.particleArea.h;\n      const r = 0.5 + Math.random() * 4;\n      const vy = 0.5 + Math.random();\n      return {\n        x: sx,\n        sx,\n        dx,\n        y,\n        vy,\n        p: 0,\n        r,\n        c: `rgba(${particleRGBColor[0]}, ${particleRGBColor[1]}, ${particleRGBColor[2]}, ${Math.random()})`,\n      };\n    },\n    [particleRGBColor],\n  );\n\n  const setParticles = React.useCallback(() => {\n    const { width, height } = stateRef.current.rect;\n    stateRef.current.particles = [];\n    const disc = stateRef.current.clip.disc;\n    stateRef.current.particleArea = {\n      sw: disc.w * 0.5,\n      ew: disc.w * 2,\n      h: height * 0.85,\n    };\n    stateRef.current.particleArea.sx =\n      (width - stateRef.current.particleArea.sw) / 2;\n    stateRef.current.particleArea.ex =\n      (width - stateRef.current.particleArea.ew) / 2;\n    const totalParticles = 100;\n    for (let i = 0; i < totalParticles; i++) {\n      stateRef.current.particles.push(initParticle(true));\n    }\n  }, [initParticle]);\n\n  const drawDiscs = React.useCallback(\n    (ctx: CanvasRenderingContext2D) => {\n      ctx.strokeStyle = strokeColor;\n      ctx.lineWidth = 2;\n      const outerDisc = stateRef.current.startDisc;\n      ctx.beginPath();\n      ctx.ellipse(\n        outerDisc.x,\n        outerDisc.y,\n        outerDisc.w,\n        outerDisc.h,\n        0,\n        0,\n        Math.PI * 2,\n      );\n      ctx.stroke();\n      ctx.closePath();\n      stateRef.current.discs.forEach((disc: any, i: number) => {\n        if (i % 5 !== 0) return;\n        if (disc.w < stateRef.current.clip.disc.w - 5) {\n          ctx.save();\n          ctx.clip(stateRef.current.clip.path);\n        }\n        ctx.beginPath();\n        ctx.ellipse(disc.x, disc.y, disc.w, disc.h, 0, 0, Math.PI * 2);\n        ctx.stroke();\n        ctx.closePath();\n        if (disc.w < stateRef.current.clip.disc.w - 5) {\n          ctx.restore();\n        }\n      });\n    },\n    [strokeColor],\n  );\n\n  const drawLines = React.useCallback((ctx: CanvasRenderingContext2D) => {\n    if (stateRef.current.linesCanvas) {\n      ctx.drawImage(stateRef.current.linesCanvas, 0, 0);\n    }\n  }, []);\n\n  const drawParticles = React.useCallback((ctx: CanvasRenderingContext2D) => {\n    ctx.save();\n    ctx.clip(stateRef.current.clip.path);\n    stateRef.current.particles.forEach((particle: any) => {\n      ctx.fillStyle = particle.c;\n      ctx.beginPath();\n      ctx.rect(particle.x, particle.y, particle.r, particle.r);\n      ctx.closePath();\n      ctx.fill();\n    });\n    ctx.restore();\n  }, []);\n\n  const moveDiscs = React.useCallback(() => {\n    stateRef.current.discs.forEach((disc: any) => {\n      disc.p = (disc.p + 0.001) % 1;\n      tweenDisc(disc);\n    });\n  }, [tweenDisc]);\n\n  const moveParticles = React.useCallback(() => {\n    stateRef.current.particles.forEach((particle: any, idx: number) => {\n      particle.p = 1 - particle.y / stateRef.current.particleArea.h;\n      particle.x = particle.sx + particle.dx * particle.p;\n      particle.y -= particle.vy;\n      if (particle.y < 0) {\n        stateRef.current.particles[idx] = initParticle();\n      }\n    });\n  }, [initParticle]);\n\n  const tick = React.useCallback(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    const ctx = canvas.getContext('2d');\n    if (!ctx) return;\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    ctx.save();\n    ctx.scale(stateRef.current.render.dpi, stateRef.current.render.dpi);\n    moveDiscs();\n    moveParticles();\n    drawDiscs(ctx);\n    drawLines(ctx);\n    drawParticles(ctx);\n    ctx.restore();\n    animationFrameIdRef.current = requestAnimationFrame(tick);\n  }, [moveDiscs, moveParticles, drawDiscs, drawLines, drawParticles]);\n\n  const init = React.useCallback(() => {\n    setSize();\n    setDiscs();\n    setLines();\n    setParticles();\n  }, [setSize, setDiscs, setLines, setParticles]);\n\n  React.useEffect(() => {\n    const canvas = canvasRef.current;\n    if (!canvas) return;\n    init();\n    tick();\n    const handleResize = () => {\n      setSize();\n      setDiscs();\n      setLines();\n      setParticles();\n    };\n    window.addEventListener('resize', handleResize);\n    return () => {\n      window.removeEventListener('resize', handleResize);\n      cancelAnimationFrame(animationFrameIdRef.current);\n    };\n  }, [init, tick, setSize, setDiscs, setLines, setParticles]);\n\n  return (\n    <div\n      data-slot=\"hole-background\"\n      className={cn(\n        'relative size-full overflow-hidden',\n        'before:content-[\"\"] before:absolute before:top-1/2 before:left-1/2 before:block before:size-[140%] dark:before:[background:radial-gradient(ellipse_at_50%_55%,transparent_10%,black_50%)] before:[background:radial-gradient(ellipse_at_50%_55%,transparent_10%,white_50%)] before:[transform:translate3d(-50%,-50%,0)]',\n        'after:content-[\"\"] after:absolute after:z-[5] after:top-1/2 after:left-1/2 after:block after:size-full after:[background:radial-gradient(ellipse_at_50%_75%,#a900ff_20%,transparent_75%)] after:[transform:translate3d(-50%,-50%,0)] after:mix-blend-overlay',\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <canvas\n        ref={canvasRef}\n        className=\"absolute inset-0 block size-full dark:opacity-20 opacity-10\"\n      />\n      <motion.div\n        className={cn(\n          'absolute top-[-71.5%] left-1/2 z-[3] w-[30%] h-[140%] rounded-b-full blur-3xl opacity-75 dark:mix-blend-plus-lighter mix-blend-plus-darker [transform:translate3d(-50%,0,0)] [background-position:0%_100%] [background-size:100%_200%]',\n          'dark:[background:linear-gradient(20deg,#00f8f1,#ffbd1e20_16.5%,#fe848f_33%,#fe848f20_49.5%,#00f8f1_66%,#00f8f160_85.5%,#ffbd1e_100%)_0_100%_/_100%_200%] [background:linear-gradient(20deg,#00f8f1,#ffbd1e40_16.5%,#fe848f_33%,#fe848f40_49.5%,#00f8f1_66%,#00f8f180_85.5%,#ffbd1e_100%)_0_100%_/_100%_200%]',\n        )}\n        animate={{ backgroundPosition: '0% 300%' }}\n        transition={{ duration: 5, ease: 'linear', repeat: Infinity }}\n      />\n      <div className=\"absolute top-0 left-0 z-[7] size-full dark:[background:repeating-linear-gradient(transparent,transparent_1px,white_1px,white_2px)] mix-blend-overlay opacity-50\" />\n    </div>\n  );\n}\n\nexport { HoleBackground, type HoleBackgroundProps };\n",
      "type": "registry:ui",
      "target": "components/animate-ui/backgrounds/hole.tsx"
    }
  ]
}