Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/www/config/docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,12 @@ export const docsConfig: DocsConfig = {
{
title: "Animations",
items: [
{
title: "Click Spark",
href: `/docs/components/click-spark`,
items: [],
label: "New",
},
{
title: "Blur Fade",
href: `/docs/components/blur-fade`,
Expand Down
53 changes: 53 additions & 0 deletions apps/www/content/docs/components/click-spark.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
title: Click Spark
date: 2026-05-17
description: A component that renders sparks when clicked.
author: rishabhmishra
published: true
---

<ComponentPreview name="click-spark-demo" />

## Installation

<Tabs defaultValue="cli">

<TabsList>
<TabsTrigger value="cli">CLI</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">

```bash
npx shadcn@latest add @magicui/click-spark
```

</TabsContent>

<TabsContent value="manual">

<Steps>

<Step>Copy and paste the following code into your project.</Step>

<ComponentSource name="click-spark" />

<Step>Update the import paths to match your project setup.</Step>

</Steps>

</TabsContent>

</Tabs>

## Props

| Prop | Type | Default | Description |
| ------------- | -------- | ---------- | --------------------------------- |
| `sparkColor` | `string` | `#fff` | Color of the sparks. |
| `sparkSize` | `number` | `10` | Size of the sparks. |
| `sparkRadius` | `number` | `15` | Radius of the sparks. |
| `sparkCount` | `number` | `8` | Number of sparks. |
| `duration` | `number` | `400` | Duration of the animation. |
| `easing` | `string` | `ease-out` | Easing function of the animation. |
| `extraScale` | `number` | `1.0` | Extra scale of the sparks. |
281 changes: 281 additions & 0 deletions apps/www/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4709,6 +4709,287 @@ export default function Component() {



===== COMPONENT: click-spark =====
Title: Click Spark
Description: A component that renders sparks when clicked.

--- file: magicui/click-spark.tsx ---
"use client"

import React, { useCallback, useEffect, useRef } from "react"

import { cn } from "@/lib/utils"

interface Spark {
x: number
y: number
angle: number
startTime: number
}

interface ClickSparkProps {
/**
* The color of the sparks.
* @default "#fff"
*/
sparkColor?: string
/**
* The size of the sparks.
* @default 10
*/
sparkSize?: number
/**
* The radius of the spark explosion.
* @default 15
*/
sparkRadius?: number
/**
* The number of sparks per click.
* @default 8
*/
sparkCount?: number
/**
* The duration of the spark animation in milliseconds.
* @default 400
*/
duration?: number
/**
* The easing function for the spark animation.
* @default "ease-out"
*/
easing?: "linear" | "ease-in" | "ease-out" | "ease-in-out"
/**
* Extra scale factor for the spark distance.
* @default 1.0
*/
extraScale?: number
/**
* The content to wrap.
*/
children?: React.ReactNode
/**
* Additional class names for the wrapper.
*/
className?: string
}

export function ClickSpark({
sparkColor = "#fff",
sparkSize = 10,
sparkRadius = 15,
sparkCount = 8,
duration = 400,
easing = "ease-out",
extraScale = 1.0,
children,
className,
}: ClickSparkProps) {
const canvasRef = useRef<HTMLCanvasElement>(null)
const sparksRef = useRef<Spark[]>([])
const animationIdRef = useRef<number | null>(null)

// Store animation settings in a ref to keep the draw loop stable
// and avoid exhaustive-deps warnings.
const settingsRef = useRef({
sparkColor,
sparkSize,
sparkRadius,
duration,
extraScale,
})

// Sync refs when props change
useEffect(() => {
settingsRef.current = {
sparkColor,
sparkSize,
sparkRadius,
duration,
extraScale,
}
}, [sparkColor, sparkSize, sparkRadius, duration, extraScale])

useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return

const parent = canvas.parentElement
if (!parent) return

let resizeTimeout: ReturnType<typeof setTimeout>

const resizeCanvas = () => {
const { width, height } = parent.getBoundingClientRect()
if (canvas.width !== width || canvas.height !== height) {
canvas.width = width
canvas.height = height
}
}

const handleResize = () => {
clearTimeout(resizeTimeout)
resizeTimeout = setTimeout(resizeCanvas, 100)
}

const ro = new ResizeObserver(handleResize)
ro.observe(parent)

resizeCanvas()

return () => {
ro.disconnect()
clearTimeout(resizeTimeout)
}
}, [])

const easeFunc = useCallback(
(t: number) => {
switch (easing) {
case "linear":
return t
case "ease-in":
return t * t
case "ease-in-out":
return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
default:
return t * (2 - t)
}
},
[easing]
)

useEffect(() => {
return () => {
if (animationIdRef.current) {
cancelAnimationFrame(animationIdRef.current)
}
}
}, [])

const draw = useCallback(
(timestamp: number) => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext("2d")
if (!ctx) return

ctx.clearRect(0, 0, canvas.width, canvas.height)

// Use settings from Ref to avoid dependency loop
const {
sparkColor: color,
sparkSize: size,
sparkRadius: radius,
duration: drn,
extraScale: scale,
} = settingsRef.current

sparksRef.current = sparksRef.current.filter((spark: Spark) => {
const elapsed = timestamp - spark.startTime
if (elapsed >= drn) {
return false
}

const progress = elapsed / drn
const eased = easeFunc(progress)

const distance = eased * radius * scale
const lineLength = size * (1 - eased)

const x1 = spark.x + distance * Math.cos(spark.angle)
const y1 = spark.y + distance * Math.sin(spark.angle)
const x2 = spark.x + (distance + lineLength) * Math.cos(spark.angle)
const y2 = spark.y + (distance + lineLength) * Math.sin(spark.angle)

ctx.strokeStyle = color
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(x1, y1)
ctx.lineTo(x2, y2)
ctx.stroke()

return true
})

if (sparksRef.current.length > 0) {
animationIdRef.current = requestAnimationFrame(draw)
} else {
animationIdRef.current = null
}
},
[easeFunc]
)

const handleClick = (e: React.MouseEvent<HTMLDivElement>): void => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const x = e.clientX - rect.left
const y = e.clientY - rect.top

const now = performance.now()
// sparkCount is only used here to initialize the objects
const newSparks: Spark[] = Array.from({ length: sparkCount }, (_, i) => ({
x,
y,
angle: (2 * Math.PI * i) / sparkCount,
startTime: now,
}))

sparksRef.current.push(...newSparks)

if (animationIdRef.current === null) {
animationIdRef.current = requestAnimationFrame(draw)
}
}

return (
<div
className={cn("relative h-full w-full", className)}
onClick={handleClick}
>
<canvas
ref={canvasRef}
className="pointer-events-none absolute inset-0"
/>
{children}
</div>
)
}


===== EXAMPLE: click-spark-demo =====
Title: Click Spark Demo

--- file: example/click-spark-demo.tsx ---
import { ClickSpark } from "@/registry/magicui/click-spark"

export default function ClickSparkDemo() {
return (
<div className="bg-background relative flex h-[400px] w-full flex-col items-center justify-center overflow-hidden rounded-xl">
<ClickSpark
sparkColor="#a1a1aa"
sparkSize={10}
sparkRadius={25}
sparkCount={8}
duration={500}
easing="ease-in-out"
>
<div className="flex h-full w-full flex-col items-center justify-center space-y-4">
<p className="z-10 bg-gradient-to-b from-zinc-400 to-zinc-900 bg-clip-text text-center text-5xl font-semibold tracking-tighter text-transparent sm:text-7xl dark:from-zinc-100 dark:to-zinc-600">
Interactive Spark
</p>
<p className="z-10 text-center text-base font-light tracking-wide text-zinc-500 dark:text-zinc-400">
Click anywhere to experience the effect
</p>
</div>
</ClickSpark>
</div>
)
}



===== 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.
Expand Down
2 changes: 2 additions & 0 deletions apps/www/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions apps/www/public/r/click-spark-demo.json
Original file line number Diff line number Diff line change
@@ -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 <div className=\"bg-background relative flex h-[400px] w-full flex-col items-center justify-center overflow-hidden rounded-xl\">\n <ClickSpark\n sparkColor=\"#a1a1aa\"\n sparkSize={10}\n sparkRadius={25}\n sparkCount={8}\n duration={500}\n easing=\"ease-in-out\"\n >\n <div className=\"flex h-full w-full flex-col items-center justify-center space-y-4\">\n <p className=\"z-10 bg-gradient-to-b from-zinc-400 to-zinc-900 bg-clip-text text-center text-5xl font-semibold tracking-tighter text-transparent sm:text-7xl dark:from-zinc-100 dark:to-zinc-600\">\n Interactive Spark\n </p>\n <p className=\"z-10 text-center text-base font-light tracking-wide text-zinc-500 dark:text-zinc-400\">\n Click anywhere to experience the effect\n </p>\n </div>\n </ClickSpark>\n </div>\n )\n}\n",
"type": "registry:example"
}
]
}
Loading
Loading