+ )
+}
+
+
+
===== COMPONENT: client-tweet-card =====
Title: Client Tweet Card
Description: A client-side version of the tweet card that displays a tweet with the author's name, handle, and profile picture.
diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt
index dd7e1290c..3bf58adcc 100644
--- a/apps/www/public/llms.txt
+++ b/apps/www/public/llms.txt
@@ -20,6 +20,7 @@ This file provides LLM-friendly entry points to documentation and examples.
- [Bento Grid](https://magicui.design/docs/components/bento-grid): Bento grid is a layout used to showcase the features of a product in a simple and elegant way.
- [Blur Fade](https://magicui.design/docs/components/blur-fade): Blur fade in and out animation. Used to smoothly fade in and out content.
- [Border Beam](https://magicui.design/docs/components/border-beam): An animated beam of light which travels along the border of its container.
+- [Click Spark](https://magicui.design/docs/components/click-spark): A component that renders sparks when clicked.
- [Client Tweet Card](https://magicui.design/docs/components/client-tweet-card): A client-side version of the tweet card that displays a tweet with the author's name, handle, and profile picture.
- [Code Comparison](https://magicui.design/docs/components/code-comparison): A component which compares two code snippets.
- [Comic Text](https://magicui.design/docs/components/comic-text): Comic text animation
@@ -85,6 +86,7 @@ This file provides LLM-friendly entry points to documentation and examples.
## Examples
+- [Click Spark Demo](https://github.com/magicuidesign/magicui/blob/main/example/click-spark-demo.tsx): Example usage
- [Magic Card Demo](https://github.com/magicuidesign/magicui/blob/main/example/magic-card-demo.tsx): Example usage
- [Magic Card Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/magic-card-demo2.tsx): Example usage
- [Android Demo](https://github.com/magicuidesign/magicui/blob/main/example/android-demo.tsx): Example usage
diff --git a/apps/www/public/r/click-spark-demo.json b/apps/www/public/r/click-spark-demo.json
new file mode 100644
index 000000000..47d3307ae
--- /dev/null
+++ b/apps/www/public/r/click-spark-demo.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "click-spark-demo",
+ "type": "registry:example",
+ "title": "Click Spark Demo",
+ "description": "Example showing a click spark component.",
+ "registryDependencies": [
+ "@magicui/click-spark"
+ ],
+ "files": [
+ {
+ "path": "registry/example/click-spark-demo.tsx",
+ "content": "import { ClickSpark } from \"@/registry/magicui/click-spark\"\n\nexport default function ClickSparkDemo() {\n return (\n
\n \n
\n
\n Interactive Spark\n
\n
\n Click anywhere to experience the effect\n
\n
\n \n
\n )\n}\n",
+ "type": "registry:example"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/apps/www/public/r/click-spark.json b/apps/www/public/r/click-spark.json
new file mode 100644
index 000000000..3ef0c02d5
--- /dev/null
+++ b/apps/www/public/r/click-spark.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
+ "name": "click-spark",
+ "type": "registry:ui",
+ "title": "Click Spark",
+ "description": "A component that renders sparks when clicked.",
+ "files": [
+ {
+ "path": "registry/magicui/click-spark.tsx",
+ "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useRef } from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface Spark {\n x: number\n y: number\n angle: number\n startTime: number\n}\n\ninterface ClickSparkProps {\n /**\n * The color of the sparks.\n * @default \"#fff\"\n */\n sparkColor?: string\n /**\n * The size of the sparks.\n * @default 10\n */\n sparkSize?: number\n /**\n * The radius of the spark explosion.\n * @default 15\n */\n sparkRadius?: number\n /**\n * The number of sparks per click.\n * @default 8\n */\n sparkCount?: number\n /**\n * The duration of the spark animation in milliseconds.\n * @default 400\n */\n duration?: number\n /**\n * The easing function for the spark animation.\n * @default \"ease-out\"\n */\n easing?: \"linear\" | \"ease-in\" | \"ease-out\" | \"ease-in-out\"\n /**\n * Extra scale factor for the spark distance.\n * @default 1.0\n */\n extraScale?: number\n /**\n * The content to wrap.\n */\n children?: React.ReactNode\n /**\n * Additional class names for the wrapper.\n */\n className?: string\n}\n\nexport function ClickSpark({\n sparkColor = \"#fff\",\n sparkSize = 10,\n sparkRadius = 15,\n sparkCount = 8,\n duration = 400,\n easing = \"ease-out\",\n extraScale = 1.0,\n children,\n className,\n}: ClickSparkProps) {\n const canvasRef = useRef(null)\n const sparksRef = useRef([])\n const animationIdRef = useRef(null)\n\n // Store animation settings in a ref to keep the draw loop stable\n // and avoid exhaustive-deps warnings.\n const settingsRef = useRef({\n sparkColor,\n sparkSize,\n sparkRadius,\n duration,\n extraScale,\n })\n\n // Sync refs when props change\n useEffect(() => {\n settingsRef.current = {\n sparkColor,\n sparkSize,\n sparkRadius,\n duration,\n extraScale,\n }\n }, [sparkColor, sparkSize, sparkRadius, duration, extraScale])\n\n useEffect(() => {\n const canvas = canvasRef.current\n if (!canvas) return\n\n const parent = canvas.parentElement\n if (!parent) return\n\n let resizeTimeout: ReturnType\n\n const resizeCanvas = () => {\n const { width, height } = parent.getBoundingClientRect()\n if (canvas.width !== width || canvas.height !== height) {\n canvas.width = width\n canvas.height = height\n }\n }\n\n const handleResize = () => {\n clearTimeout(resizeTimeout)\n resizeTimeout = setTimeout(resizeCanvas, 100)\n }\n\n const ro = new ResizeObserver(handleResize)\n ro.observe(parent)\n\n resizeCanvas()\n\n return () => {\n ro.disconnect()\n clearTimeout(resizeTimeout)\n }\n }, [])\n\n const easeFunc = useCallback(\n (t: number) => {\n switch (easing) {\n case \"linear\":\n return t\n case \"ease-in\":\n return t * t\n case \"ease-in-out\":\n return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t\n default:\n return t * (2 - t)\n }\n },\n [easing]\n )\n\n useEffect(() => {\n return () => {\n if (animationIdRef.current) {\n cancelAnimationFrame(animationIdRef.current)\n }\n }\n }, [])\n\n const draw = useCallback(\n (timestamp: number) => {\n const canvas = canvasRef.current\n if (!canvas) return\n const ctx = canvas.getContext(\"2d\")\n if (!ctx) return\n\n ctx.clearRect(0, 0, canvas.width, canvas.height)\n\n // Use settings from Ref to avoid dependency loop\n const {\n sparkColor: color,\n sparkSize: size,\n sparkRadius: radius,\n duration: drn,\n extraScale: scale,\n } = settingsRef.current\n\n sparksRef.current = sparksRef.current.filter((spark: Spark) => {\n const elapsed = timestamp - spark.startTime\n if (elapsed >= drn) {\n return false\n }\n\n const progress = elapsed / drn\n const eased = easeFunc(progress)\n\n const distance = eased * radius * scale\n const lineLength = size * (1 - eased)\n\n const x1 = spark.x + distance * Math.cos(spark.angle)\n const y1 = spark.y + distance * Math.sin(spark.angle)\n const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle)\n const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle)\n\n ctx.strokeStyle = color\n ctx.lineWidth = 2\n ctx.beginPath()\n ctx.moveTo(x1, y1)\n ctx.lineTo(x2, y2)\n ctx.stroke()\n\n return true\n })\n\n if (sparksRef.current.length > 0) {\n animationIdRef.current = requestAnimationFrame(draw)\n } else {\n animationIdRef.current = null\n }\n },\n [easeFunc]\n )\n\n const handleClick = (e: React.MouseEvent): void => {\n const canvas = canvasRef.current\n if (!canvas) return\n const rect = canvas.getBoundingClientRect()\n const x = e.clientX - rect.left\n const y = e.clientY - rect.top\n\n const now = performance.now()\n // sparkCount is only used here to initialize the objects\n const newSparks: Spark[] = Array.from({ length: sparkCount }, (_, i) => ({\n x,\n y,\n angle: (2 * Math.PI * i) / sparkCount,\n startTime: now,\n }))\n\n sparksRef.current.push(...newSparks)\n\n if (animationIdRef.current === null) {\n animationIdRef.current = requestAnimationFrame(draw)\n }\n }\n\n return (\n