{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "primitives-effects-click",
  "title": "Click",
  "description": "An effect that creates animated effects at the click position, adding interactive feedback to user actions.",
  "dependencies": [
    "motion"
  ],
  "files": [
    {
      "path": "registry/primitives/effects/click/index.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { createPortal } from \"react-dom\";\nimport {\n  motion,\n  AnimatePresence,\n  type HTMLMotionProps,\n  type SVGMotionProps,\n} from \"motion/react\";\n\ntype ClickVariant = \"ripple\" | \"ring\" | \"crosshair\" | \"burst\" | \"particles\";\n\ntype Item = {\n  id: string;\n  x: number;\n  y: number;\n  variant: ClickVariant;\n  color: string;\n  size: number;\n  duration: number;\n};\n\ntype ClickCommonProps = {\n  children?: React.ReactNode;\n  color?: string;\n  size?: number;\n  duration?: number;\n  scope?: React.RefObject<HTMLElement | null>;\n  disabled?: boolean;\n};\n\ntype RippleClickProps = HTMLMotionProps<\"div\"> &\n  ClickCommonProps & {\n    variant: \"ripple\";\n  };\n\ntype SvgClickProps = SVGMotionProps<\"svg\"> &\n  ClickCommonProps & {\n    variant?: Exclude<ClickVariant, \"ripple\">;\n  };\n\ntype ClickProps = RippleClickProps | SvgClickProps;\n\nfunction Click(props: RippleClickProps): React.ReactElement | null;\nfunction Click(props: SvgClickProps): React.ReactElement | null;\nfunction Click(props: ClickProps): React.ReactElement | null {\n  const {\n    children,\n    variant = \"ring\",\n    color = \"currentColor\",\n    size = 100,\n    duration = 400,\n    scope,\n    disabled = false,\n  } = props as ClickProps;\n  const [items, setItems] = React.useState<Item[]>([]);\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n\n  React.useEffect(() => {\n    if (disabled) return;\n    const target = scope?.current ?? document;\n    const isDoc = target instanceof Document;\n    // eslint-disable-next-line @typescript-eslint/no-explicit-any\n    const el: any = isDoc ? document : target;\n    if (!el) return;\n    function onPointerUp(e: PointerEvent) {\n      if (window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches) return;\n      if (!isDoc) {\n        const rect = (el as HTMLElement).getBoundingClientRect();\n        if (\n          e.clientX < rect.left ||\n          e.clientX > rect.right ||\n          e.clientY < rect.top ||\n          e.clientY > rect.bottom\n        ) {\n          return;\n        }\n      }\n      const x = e.clientX;\n      const y = e.clientY;\n      const id = crypto.randomUUID();\n      const item: Item = {\n        id,\n        x,\n        y,\n        variant,\n        color,\n        size,\n        duration,\n      };\n      setItems((prev) => [...prev, item]);\n      window.setTimeout(() => {\n        setItems((prev) => prev.filter((it) => it.id !== id));\n      }, duration + 300);\n    }\n    el.addEventListener(\"pointerup\", onPointerUp, { passive: true });\n    return () => el.removeEventListener(\"pointerup\", onPointerUp);\n  }, [scope, variant, color, size, duration, disabled]);\n\n  const portal = typeof window !== \"undefined\" ? document.body : null;\n\n  return (\n    <>\n      {children}\n      {portal &&\n        createPortal(\n          <div\n            ref={containerRef}\n            style={{\n              position: \"fixed\",\n              inset: 0,\n              pointerEvents: \"none\",\n              zIndex: 9999999999,\n            }}\n          >\n            <AnimatePresence initial={false}>\n              {items.map((it) => (\n                <EffectRenderer key={it.id} item={it} />\n              ))}\n            </AnimatePresence>\n          </div>,\n          portal,\n        )}\n    </>\n  );\n}\n\nfunction EffectRenderer({ item }: { item: Item }) {\n  if (item.variant === \"ripple\") return <Ripple item={item} />;\n  if (item.variant === \"ring\") return <Ring item={item} />;\n  if (item.variant === \"crosshair\") return <Crosshair item={item} />;\n  if (item.variant === \"burst\") return <Burst item={item} />;\n  if (item.variant === \"particles\") return <Particles item={item} />;\n  return null;\n}\n\nfunction Ripple({ item }: { item: Item }) {\n  const r = item.size;\n  const d = item.duration / 1000;\n\n  return (\n    <motion.div\n      initial={{ opacity: 1, scale: 0.2 }}\n      animate={{ opacity: 0, scale: 1 }}\n      exit={{ opacity: 0 }}\n      transition={{ duration: d, ease: \"easeOut\" }}\n      style={{\n        position: \"fixed\",\n        left: item.x - r / 2,\n        top: item.y - r / 2,\n        width: r,\n        height: r,\n        borderRadius: \"9999px\",\n        background: item.color,\n        boxShadow: `0 0 0 2px ${item.color} inset`,\n        willChange: \"transform, opacity\",\n      }}\n    />\n  );\n}\n\nfunction Ring({ item }: { item: Item }) {\n  const d = item.duration / 1000;\n\n  const rEnd = Math.max(item.size / 1.5, 50);\n  const rStart = Math.max(Math.floor(rEnd * 0.2), 12);\n\n  const strokeStart = Math.max(Math.floor(rEnd * 0.05), 2.5);\n  const strokeEnd = 0;\n\n  return (\n    <motion.svg\n      style={{\n        position: \"fixed\",\n        left: item.x - rEnd / 2,\n        top: item.y - rEnd / 2,\n        width: rEnd,\n        height: rEnd,\n        display: \"block\",\n        overflow: \"visible\",\n      }}\n      viewBox={`0 0 ${rEnd} ${rEnd}`}\n    >\n      <motion.circle\n        cx={rEnd / 2}\n        cy={rEnd / 2}\n        fill=\"none\"\n        stroke={item.color}\n        initial={{ r: rStart, strokeWidth: strokeStart }}\n        animate={{ r: rEnd, strokeWidth: strokeEnd }}\n        transition={{ duration: d, ease: \"easeOut\" }}\n        strokeLinecap=\"round\"\n        strokeLinejoin=\"round\"\n      />\n    </motion.svg>\n  );\n}\n\nfunction Crosshair({ item }: { item: Item }) {\n  const d = item.duration / 1000;\n\n  const distanceEnd = Math.max(item.size * 0.65, 56);\n  const gapStart = Math.max(item.size * 0.05, 3);\n  const lenStart = Math.max(item.size * 0.26, 16);\n  const lenEnd = Math.max(lenStart * 0.55, 6);\n\n  const strokeWidth = Math.max(item.size * 0.065, 2);\n\n  const half = distanceEnd + gapStart + lenStart;\n  const box = half * 2;\n  const cx = half,\n    cy = half;\n\n  const dirs = [\n    { dx: +1, dy: 0 },\n    { dx: -1, dy: 0 },\n    { dx: 0, dy: -1 },\n    { dx: 0, dy: +1 },\n  ] as const;\n\n  return (\n    <motion.svg\n      style={{\n        position: \"fixed\",\n        left: item.x - half,\n        top: item.y - half,\n        width: box,\n        height: box,\n        display: \"block\",\n        overflow: \"visible\",\n        pointerEvents: \"none\",\n        rotate: \"-25deg\",\n        strokeWidth,\n      }}\n      viewBox={`0 0 ${box} ${box}`}\n    >\n      {dirs.map((dir, i) => {\n        const x1_0 = cx + dir.dx * gapStart;\n        const y1_0 = cy + dir.dy * gapStart;\n        const x2_0 = cx + dir.dx * (gapStart + lenStart);\n        const y2_0 = cy + dir.dy * (gapStart + lenStart);\n\n        const x1_1 = cx + dir.dx * distanceEnd;\n        const y1_1 = cy + dir.dy * distanceEnd;\n        const x2_1 = cx + dir.dx * (distanceEnd + lenEnd);\n        const y2_1 = cy + dir.dy * (distanceEnd + lenEnd);\n\n        return (\n          <motion.line\n            key={i}\n            x1={x1_0}\n            y1={y1_0}\n            x2={x2_0}\n            y2={y2_0}\n            initial={{ pathLength: 1, scale: 1 }}\n            animate={{\n              x1: x1_1,\n              y1: y1_1,\n              x2: x2_1,\n              y2: y2_1,\n              pathLength: 0,\n              scale: 0,\n            }}\n            transition={{ duration: d, ease: [0, 0, 0.7, 1] }}\n            stroke={item.color}\n          />\n        );\n      })}\n    </motion.svg>\n  );\n}\n\nfunction Burst({ item }: { item: Item }) {\n  const d = item.duration / 1000;\n\n  const rStart = Math.max(item.size * 0.4, 36);\n  const rEnd = Math.max(item.size * 0.65, 56);\n\n  const segCount = 4;\n  const lenStart = Math.max(item.size * 0.2, 14);\n  const lenEnd = Math.max(lenStart * 0.5, 6);\n  const strokeWidth = Math.max(item.size * 0.05, 1.5);\n\n  const startDeg = 180;\n  const endDeg = 270;\n\n  const half = rEnd + lenStart;\n  const box = half * 2;\n  const cx = half,\n    cy = half;\n\n  const toRad = (deg: number) => (deg * Math.PI) / 180;\n\n  return (\n    <motion.svg\n      style={{\n        position: \"fixed\",\n        left: item.x - half,\n        top: item.y - half,\n        width: box,\n        height: box,\n        display: \"block\",\n        overflow: \"visible\",\n        pointerEvents: \"none\",\n        strokeWidth,\n      }}\n      viewBox={`0 0 ${box} ${box}`}\n    >\n      {Array.from({ length: segCount }).map((_, i) => {\n        const t = i / Math.max(1, segCount - 1);\n        const deg = startDeg + (endDeg - startDeg) * t;\n        const th = toRad(deg);\n\n        const ux = Math.cos(th);\n        const uy = Math.sin(th);\n\n        const c0x = cx + ux * rStart;\n        const c0y = cy + uy * rStart;\n        const c1x = cx + ux * rEnd;\n        const c1y = cy + uy * rEnd;\n\n        return (\n          <motion.line\n            key={i}\n            x1={c0x - ux * (lenStart / 2)}\n            y1={c0y - uy * (lenStart / 2)}\n            x2={c0x + ux * (lenStart / 2)}\n            y2={c0y + uy * (lenStart / 2)}\n            initial={{ pathLength: 1, scale: 1 }}\n            animate={{\n              x1: [\n                c0x - ux * (lenStart / 2),\n                c1x - ux * (lenEnd / 2),\n                c1x - ux * (lenEnd / 2),\n              ],\n              y1: [\n                c0y - uy * (lenStart / 2),\n                c1y - uy * (lenEnd / 2),\n                c1y - uy * (lenEnd / 2),\n              ],\n              x2: [\n                c0x + ux * (lenStart / 2),\n                c1x + ux * (lenEnd / 2),\n                c1x + ux * (lenEnd / 2),\n              ],\n              y2: [\n                c0y + uy * (lenStart / 2),\n                c1y + uy * (lenEnd / 2),\n                c1y + uy * (lenEnd / 2),\n              ],\n              pathLength: [1, 0.5, 0.5],\n              scale: [1, 1, 0],\n            }}\n            transition={{\n              duration: d,\n              ease: \"easeInOut\",\n              times: [0, 0.7, 1],\n            }}\n            stroke={item.color}\n          />\n        );\n      })}\n    </motion.svg>\n  );\n}\n\nfunction Particles({ item }: { item: Item }) {\n  const d = item.duration / 1000;\n\n  const count = 6;\n  const radius = item.size * 0.35;\n  const r0 = Math.max(item.size * 0.025, 2);\n\n  const half = radius + r0 * 2;\n  const box = half * 2;\n  const cx = half,\n    cy = half;\n\n  const angleStep = (2 * Math.PI) / count;\n\n  return (\n    <motion.svg\n      style={{\n        position: \"fixed\",\n        left: item.x - half,\n        top: item.y - half,\n        width: box,\n        height: box,\n        display: \"block\",\n        overflow: \"visible\",\n        pointerEvents: \"none\",\n      }}\n      viewBox={`0 0 ${box} ${box}`}\n    >\n      {Array.from({ length: count }).map((_, i) => {\n        const angle = i * angleStep;\n        const x = Math.cos(angle) * radius;\n        const y = Math.sin(angle) * radius;\n\n        return (\n          <motion.circle\n            key={i}\n            cx={cx}\n            cy={cy}\n            r={r0}\n            fill={item.color}\n            initial={{ scale: 0, opacity: 0 }}\n            animate={{\n              cx: cx + x,\n              cy: cy + y,\n              scale: [0, 1, 0],\n              opacity: [0, 1, 0],\n            }}\n            transition={{\n              duration: d,\n              delay: i * (d * 0.08),\n              ease: \"easeOut\",\n            }}\n          />\n        );\n      })}\n    </motion.svg>\n  );\n}\n\nexport { Click, type ClickProps, type ClickVariant };\n",
      "type": "registry:ui",
      "target": "components/unlumen-ui/primitives/effects/click.tsx"
    }
  ],
  "type": "registry:ui"
}