{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "backgrounds-hole",
  "type": "registry:ui",
  "title": "Hole Background",
  "description": "A background component featuring an animated hole grid.",
  "dependencies": [
    "@animate-ui/react",
    "cn",
    "motion"
  ],
  "files": [
    {
      "path": "registry/backgrounds/hole/index.tsx",
      "type": "registry:ui",
      "target": "@components/animate-ui/backgrounds/hole.tsx",
      "content": "/* eslint-disable @typescript-eslint/no-explicit-any */\n\n'use client';\n\nimport * as React from 'react';\nimport { motion, useInView } from 'motion/react';\n\nimport {\n  ReducedMotionConfig,\n  useShouldReduceMotion,\n} from '@animate-ui/react/utils/motion';\nimport { usePageVisible } from '@animate-ui/react/utils/use-page-visible';\nimport { cn } from 'cn';\n\ntype HoleBackgroundProps = React.ComponentProps<'div'> & {\n  strokeColor?: string;\n  numberOfLines?: number;\n  numberOfDiscs?: number;\n  particleRGBColor?: [number, number, number];\n};\n\nfunction HoleBackgroundContent({\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 [particleR, particleG, particleB] = particleRGBColor;\n\n  const inView = useInView(canvasRef);\n  const pageVisible = usePageVisible();\n  const reduce = useShouldReduceMotion();\n  const playing = inView && pageVisible && !reduce;\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: Math.min(window.devicePixelRatio || 1, 2),\n    };\n    canvas.width = Math.max(\n      1,\n      Math.round(stateRef.current.render.width * stateRef.current.render.dpi),\n    );\n    canvas.height = Math.max(\n      1,\n      Math.round(stateRef.current.render.height * stateRef.current.render.dpi),\n    );\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    if (!stateRef.current.clip.disc)\n      stateRef.current.clip = { disc: { ...stateRef.current.startDisc }, i: 0 };\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 { dpi } = stateRef.current.render;\n    const offCanvas = document.createElement('canvas');\n    offCanvas.width = Math.max(1, Math.round(width * dpi));\n    offCanvas.height = Math.max(1, Math.round(height * dpi));\n    const ctx = offCanvas.getContext('2d');\n    if (!ctx) return;\n    ctx.scale(dpi, dpi);\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(\n            stateRef.current.clip.path,\n            p1.x * dpi,\n            p1.y * dpi,\n          ) ||\n            ctx.isPointInStroke(\n              stateRef.current.clip.path,\n              p1.x * dpi,\n              p1.y * dpi,\n            ))\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(${particleR}, ${particleG}, ${particleB}, ${Math.random()})`,\n      };\n    },\n    [particleR, particleG, particleB],\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      const { width, height } = stateRef.current.rect;\n      ctx.drawImage(stateRef.current.linesCanvas, 0, 0, width, height);\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 draw = 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    drawDiscs(ctx);\n    drawLines(ctx);\n    drawParticles(ctx);\n    ctx.restore();\n  }, [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    const handleResize = () => {\n      const rect = canvas.getBoundingClientRect();\n      const { width, height, dpi } = stateRef.current.render;\n      if (\n        rect.width === width &&\n        rect.height === height &&\n        Math.min(window.devicePixelRatio || 1, 2) === dpi\n      )\n        return;\n      init();\n      draw();\n    };\n    init();\n    draw();\n    const resizeObserver = new ResizeObserver(handleResize);\n    resizeObserver.observe(canvas);\n    return () => resizeObserver.disconnect();\n  }, [init, draw]);\n\n  React.useEffect(() => {\n    if (!playing) return;\n    let frameId = 0;\n    const tick = () => {\n      moveDiscs();\n      moveParticles();\n      draw();\n      frameId = requestAnimationFrame(tick);\n    };\n    frameId = requestAnimationFrame(tick);\n    return () => cancelAnimationFrame(frameId);\n  }, [playing, moveDiscs, moveParticles, draw]);\n\n  return (\n    <div\n      data-slot=\"hole-background\"\n      className={cn(\n        'relative size-full overflow-hidden',\n        'before:pointer-events-none after:pointer-events-none',\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        aria-hidden=\"true\"\n        className=\"pointer-events-none absolute inset-0 block size-full dark:opacity-20 opacity-10\"\n      />\n      <motion.div\n        aria-hidden=\"true\"\n        className={cn(\n          'pointer-events-none 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: playing ? '0% 300%' : '0% 100%' }}\n        transition={\n          playing\n            ? { duration: 5, ease: 'linear', repeat: Infinity }\n            : { duration: 0 }\n        }\n      />\n      <div\n        aria-hidden=\"true\"\n        className=\"pointer-events-none 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      />\n    </div>\n  );\n}\n\nfunction HoleBackground(props: HoleBackgroundProps) {\n  return (\n    <ReducedMotionConfig>\n      <HoleBackgroundContent {...props} />\n    </ReducedMotionConfig>\n  );\n}\n\nexport { HoleBackground, type HoleBackgroundProps };\n"
    }
  ]
}