{
  "name": "mouse-effect-card",
  "type": "registry:component",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "card",
    "button"
  ],
  "files": [
    {
      "type": "registry:component",
      "content": "\"use client\";\n\n/**\n * @author: @dorianbaffier\n * @description: Mouse Effect Card - Interactive card with animated dot pattern that responds to mouse movement\n * @version: 1.0.0\n * @date: 2025-01-30\n * @license: MIT\n * @website: https://kokonutui.com\n * @github: https://github.com/kokonut-labs/kokonutui\n */\n\nimport { motion, useMotionValue, useSpring, useTransform } from \"motion/react\";\nimport { useEffect, useRef, useState } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Card, CardContent } from \"@/components/ui/card\";\nimport { cn } from \"@/lib/utils\";\n\nconst SPRING_CONFIG = { stiffness: 300, damping: 30, mass: 0.5 };\nconst OPACITY_DURATION_BASE = 0.8;\nconst OPACITY_DURATION_VARIATION = 0.2;\nconst OPACITY_EASE = [0.4, 0, 0.2, 1] as const;\nconst OPACITY_DELAY_CYCLE = 1.5;\nconst OPACITY_DELAY_STEP = 0.02;\nconst MIN_OPACITY_MULTIPLIER = 0.5;\nconst MAX_OPACITY_MULTIPLIER = 1.5;\nconst MIN_OPACITY_FALLBACK = 0.3;\nconst PROXIMITY_MULTIPLIER = 1.2;\nconst PROXIMITY_OPACITY_BOOST = 0.8;\n\nexport interface MouseEffectCardProps {\n  className?: string;\n  children?: React.ReactNode;\n  dotSize?: number;\n  dotSpacing?: number;\n  repulsionRadius?: number;\n  repulsionStrength?: number;\n  title?: string;\n  subtitle?: string;\n  topText?: string;\n  topSubtext?: string;\n  primaryCtaText?: string;\n  primaryCtaUrl?: string;\n  secondaryCtaText?: string;\n  secondaryCtaUrl?: string;\n  footerText?: string;\n  ariaLabel?: string;\n}\n\ninterface Dot {\n  id: string;\n  x: number;\n  y: number;\n  baseX: number;\n  baseY: number;\n  opacity: number;\n}\n\ninterface DotComponentProps {\n  dot: Dot;\n  index: number;\n  dotSize: number;\n  mouseX: ReturnType<typeof useMotionValue<number>>;\n  mouseY: ReturnType<typeof useMotionValue<number>>;\n  repulsionRadius: number;\n  repulsionStrength: number;\n}\n\nfunction calculateDistance(\n  x1: number,\n  y1: number,\n  x2: number,\n  y2: number\n): number {\n  const dx = x1 - x2;\n  const dy = y1 - y2;\n  return Math.sqrt(dx * dx + dy * dy);\n}\n\nfunction generateDots(width: number, height: number, spacing: number): Dot[] {\n  const dots: Dot[] = [];\n  const cols = Math.ceil(width / spacing);\n  const rows = Math.ceil(height / spacing);\n  const centerX = width / 2;\n  const centerY = height / 2;\n  const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY);\n\n  for (let row = 0; row <= rows; row++) {\n    for (let col = 0; col <= cols; col++) {\n      const x = col * spacing;\n      const y = row * spacing;\n\n      // Calculate distance from center\n      const dx = x - centerX;\n      const dy = y - centerY;\n      const distanceFromCenter = Math.sqrt(dx * dx + dy * dy);\n\n      // Calculate edge factor (0 at edges, 1 at center)\n      const edgeFactor = Math.min(distanceFromCenter / (maxDistance * 0.7), 1);\n\n      // Skip dots near edges with probability based on distance\n      if (Math.random() > edgeFactor) {\n        continue;\n      }\n\n      const pattern = (row + col) % 3;\n      const baseOpacities = [0.3, 0.5, 0.7];\n      const opacity = baseOpacities[pattern] * edgeFactor;\n\n      dots.push({\n        id: `dot-${row}-${col}`,\n        x,\n        y,\n        baseX: x,\n        baseY: y,\n        opacity,\n      });\n    }\n  }\n\n  return dots;\n}\n\nfunction DotComponent({\n  dot,\n  index,\n  dotSize,\n  mouseX,\n  mouseY,\n  repulsionRadius,\n  repulsionStrength,\n}: DotComponentProps) {\n  const posX = useTransform([mouseX, mouseY], () => {\n    const mx = mouseX.get();\n    const my = mouseY.get();\n\n    if (!(Number.isFinite(mx) && Number.isFinite(my))) {\n      return 0;\n    }\n\n    const dx = dot.baseX - mx;\n    const dy = dot.baseY - my;\n    const distance = Math.sqrt(dx * dx + dy * dy);\n\n    if (distance < repulsionRadius) {\n      const force = (1 - distance / repulsionRadius) * repulsionStrength;\n      const angle = Math.atan2(dy, dx);\n      return Math.cos(angle) * force;\n    }\n\n    return 0;\n  });\n\n  const posY = useTransform([mouseX, mouseY], () => {\n    const mx = mouseX.get();\n    const my = mouseY.get();\n\n    if (!(Number.isFinite(mx) && Number.isFinite(my))) {\n      return 0;\n    }\n\n    const dx = dot.baseX - mx;\n    const dy = dot.baseY - my;\n    const distance = Math.sqrt(dx * dx + dy * dy);\n\n    if (distance < repulsionRadius) {\n      const force = (1 - distance / repulsionRadius) * repulsionStrength;\n      const angle = Math.atan2(dy, dx);\n      return Math.sin(angle) * force;\n    }\n\n    return 0;\n  });\n\n  const opacityBoost = useTransform([mouseX, mouseY], () => {\n    const mx = mouseX.get();\n    const my = mouseY.get();\n\n    if (!(Number.isFinite(mx) && Number.isFinite(my))) return 0;\n\n    const distance = calculateDistance(dot.baseX, dot.baseY, mx, my);\n    const maxDistance = repulsionRadius * PROXIMITY_MULTIPLIER;\n\n    if (distance < maxDistance) {\n      const proximityFactor = 1 - distance / maxDistance;\n      return proximityFactor * PROXIMITY_OPACITY_BOOST;\n    }\n\n    return 0;\n  });\n\n  const x = useSpring(posX, SPRING_CONFIG);\n  const y = useSpring(posY, SPRING_CONFIG);\n\n  const baseMinOpacity = Math.max(\n    dot.opacity * MIN_OPACITY_MULTIPLIER,\n    MIN_OPACITY_FALLBACK\n  );\n  const baseMaxOpacity = Math.min(dot.opacity * MAX_OPACITY_MULTIPLIER, 1);\n\n  const minOpacityWithBoost = useTransform(opacityBoost, (boost) =>\n    Math.min(baseMinOpacity + boost, 1)\n  );\n\n  const delay = (index * OPACITY_DELAY_STEP) % OPACITY_DELAY_CYCLE;\n\n  return (\n    <motion.div\n      animate={{\n        opacity: [baseMinOpacity, baseMaxOpacity, baseMinOpacity],\n      }}\n      className=\"absolute rounded-full bg-zinc-400 will-change-transform dark:bg-zinc-600\"\n      initial={{ opacity: baseMinOpacity }}\n      style={{\n        width: dotSize,\n        height: dotSize,\n        left: dot.baseX,\n        top: dot.baseY,\n        x,\n        y,\n        opacity: useSpring(minOpacityWithBoost, {\n          stiffness: 150,\n          damping: 25,\n        }),\n      }}\n      transition={{\n        opacity: {\n          duration:\n            OPACITY_DURATION_BASE + (index % 4) * OPACITY_DURATION_VARIATION,\n          repeat: Number.POSITIVE_INFINITY,\n          ease: OPACITY_EASE,\n          delay,\n          times: [0, 0.5, 1],\n        },\n      }}\n    />\n  );\n}\n\nexport default function MouseEffectCard({\n  className,\n  children,\n  dotSize = 2,\n  dotSpacing = 16,\n  repulsionRadius = 80,\n  repulsionStrength = 20,\n  title = \"Acme\",\n  subtitle = \"Build interfaces with interactive patterns\",\n  topText = \"Case Study\",\n  topSubtext = \"Discover something new\",\n  primaryCtaText = \"Get Started\",\n  primaryCtaUrl = \"#\",\n  secondaryCtaText = \"View Docs\",\n  secondaryCtaUrl = \"#\",\n  footerText = \"We do it all\",\n  ariaLabel,\n}: MouseEffectCardProps) {\n  const innerContainerRef = useRef<HTMLDivElement>(null);\n  const mouseX = useMotionValue(Number.POSITIVE_INFINITY);\n  const mouseY = useMotionValue(Number.POSITIVE_INFINITY);\n  const [dots, setDots] = useState<Dot[]>([]);\n\n  useEffect(() => {\n    const updateDots = () => {\n      if (!innerContainerRef.current) return;\n      const rect = innerContainerRef.current.getBoundingClientRect();\n      const newDots = generateDots(rect.width, rect.height, dotSpacing);\n      setDots(newDots);\n    };\n\n    updateDots();\n\n    const resizeObserver = new ResizeObserver(updateDots);\n    if (innerContainerRef.current) {\n      resizeObserver.observe(innerContainerRef.current);\n    }\n\n    return () => {\n      resizeObserver.disconnect();\n    };\n  }, [dotSpacing]);\n\n  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {\n    if (!innerContainerRef.current) return;\n\n    const rect = innerContainerRef.current.getBoundingClientRect();\n    const x = e.clientX - rect.left;\n    const y = e.clientY - rect.top;\n\n    mouseX.set(x);\n    mouseY.set(y);\n  };\n\n  const handleMouseLeave = () => {\n    mouseX.set(Number.POSITIVE_INFINITY);\n    mouseY.set(Number.POSITIVE_INFINITY);\n  };\n\n  const handleFocus = () => {\n    if (!innerContainerRef.current) return;\n    const rect = innerContainerRef.current.getBoundingClientRect();\n    mouseX.set(rect.width / 2);\n    mouseY.set(rect.height / 2);\n  };\n\n  const handleBlur = () => {\n    mouseX.set(Number.POSITIVE_INFINITY);\n    mouseY.set(Number.POSITIVE_INFINITY);\n  };\n\n  const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {\n    if (!innerContainerRef.current) return;\n    const rect = innerContainerRef.current.getBoundingClientRect();\n    const step = Math.min(rect.width, rect.height) * 0.2;\n    const currentX = Number.isFinite(mouseX.get())\n      ? mouseX.get()\n      : rect.width / 2;\n    const currentY = Number.isFinite(mouseY.get())\n      ? mouseY.get()\n      : rect.height / 2;\n\n    switch (e.key) {\n      case \"ArrowUp\":\n        e.preventDefault();\n        mouseY.set(Math.max(0, currentY - step));\n        mouseX.set(currentX);\n        break;\n      case \"ArrowDown\":\n        e.preventDefault();\n        mouseY.set(Math.min(rect.height, currentY + step));\n        mouseX.set(currentX);\n        break;\n      case \"ArrowLeft\":\n        e.preventDefault();\n        mouseX.set(Math.max(0, currentX - step));\n        mouseY.set(currentY);\n        break;\n      case \"ArrowRight\":\n        e.preventDefault();\n        mouseX.set(Math.min(rect.width, currentX + step));\n        mouseY.set(currentY);\n        break;\n    }\n  };\n\n  return (\n    <Card\n      className={cn(\n        \"relative w-full max-w-md overflow-hidden rounded-2xl border border-white/40 p-0 shadow-none dark:border-white/10\",\n        className\n      )}\n    >\n      <CardContent\n        aria-label={ariaLabel ?? `${title} — ${subtitle}`}\n        className=\"relative h-[400px] w-full overflow-hidden p-0\"\n        onBlur={handleBlur}\n        onFocus={handleFocus}\n        onKeyDown={handleKeyDown}\n        onMouseLeave={handleMouseLeave}\n        onMouseMove={handleMouseMove}\n        ref={innerContainerRef}\n        tabIndex={0}\n      >\n        {dots.map((dot, index) => (\n          <DotComponent\n            dot={dot}\n            dotSize={dotSize}\n            index={index}\n            key={dot.id}\n            mouseX={mouseX}\n            mouseY={mouseY}\n            repulsionRadius={repulsionRadius}\n            repulsionStrength={repulsionStrength}\n          />\n        ))}\n\n        {topText && (\n          <div className=\"absolute top-6 left-6 z-10\">\n            <div className=\"relative\">\n              <div className=\"absolute inset-0 rounded-lg bg-white/60 blur-lg dark:bg-zinc-950/60\" />\n              <div className=\"relative flex flex-col gap-1\">\n                <p className=\"font-bold text-sm text-zinc-900 dark:text-white\">\n                  {topText}\n                </p>\n                {topSubtext && (\n                  <p className=\"font-medium text-xs text-zinc-600 opacity-70 dark:text-zinc-400\">\n                    {topSubtext}\n                  </p>\n                )}\n              </div>\n            </div>\n          </div>\n        )}\n\n        <div className=\"relative z-10 flex h-full flex-col items-center justify-center px-2\">\n          <div className=\"flex flex-col items-center gap-6\">\n            <div className=\"relative\">\n              <div className=\"absolute inset-0 rounded-full bg-white/80 blur-2xl dark:bg-zinc-950/80\" />\n              <h2 className=\"relative text-center font-bold text-4xl text-zinc-900 tracking-tight dark:text-white\">\n                {title}\n              </h2>\n            </div>\n            {(subtitle || children) && (\n              <div className=\"relative\">\n                <div className=\"absolute inset-0 rounded-lg bg-white/60 blur-xl dark:bg-zinc-950/60\" />\n                <p className=\"relative max-w-sm text-center font-medium text-base text-zinc-700 leading-relaxed dark:text-zinc-300\">\n                  {children || subtitle}\n                </p>\n              </div>\n            )}\n            <div className=\"mt-2 flex items-center gap-3\">\n              <Button asChild className=\"rounded-full shadow-lg\" size=\"lg\">\n                <a\n                  href={primaryCtaUrl}\n                  onClick={(e) => {\n                    if (primaryCtaUrl === \"#\") {\n                      e.preventDefault();\n                    }\n                  }}\n                >\n                  {primaryCtaText}\n                </a>\n              </Button>\n              {secondaryCtaText && (\n                <Button\n                  asChild\n                  className=\"rounded-full\"\n                  size=\"lg\"\n                  variant=\"outline\"\n                >\n                  <a\n                    href={secondaryCtaUrl}\n                    onClick={(e) => {\n                      if (secondaryCtaUrl === \"#\") {\n                        e.preventDefault();\n                      }\n                    }}\n                  >\n                    {secondaryCtaText}\n                  </a>\n                </Button>\n              )}\n            </div>\n          </div>\n        </div>\n\n        {footerText && (\n          <div className=\"absolute right-0 bottom-6 left-0 z-10 flex justify-center\">\n            <div className=\"relative\">\n              <div className=\"absolute inset-0 rounded-full bg-white/60 blur-lg dark:bg-zinc-950/60\" />\n              <p className=\"relative px-4 py-1 font-medium text-xs text-zinc-600 dark:text-zinc-400\">\n                {footerText}\n              </p>\n            </div>\n          </div>\n        )}\n      </CardContent>\n    </Card>\n  );\n}\n",
      "path": "/components/kokonutui/mouse-effect-card.tsx",
      "target": "components/kokonutui/mouse-effect-card.tsx"
    }
  ]
}