
magicui
by Alanlee0323
SKILL.md
name: magicui description: A collection of high-performance React UI components using Tailwind CSS and Framer Motion. tags: [react, tailwind, framer-motion, ui-components]
===== COMPONENT: android ===== Title: Android Description: A mockup of an Android device.
--- file: magicui/android.tsx --- import { SVGProps } from "react"
export interface AndroidProps extends SVGProps { width?: number height?: number src?: string videoSrc?: string }
export function Android({
width = 433,
height = 882,
src,
videoSrc,
...props
}: AndroidProps) {
return (
<svg
width={width}
height={height}
viewBox={0 0 ${width} ${height}}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<g clipPath="url(#clip0_514_20855)">
<path
d="M9.25 48C9.25 29.3604 24.3604 14.25 43 14.25H335C353.64 14.25 368.75 29.3604 368.75 48V780C368.75 798.64 353.64 813.75 335 813.75H43C24.3604 813.75 9.25 798.64 9.25 780V48Z"
className="fill-[#E5E5E5] stroke-[#E5E5E5] stroke-[0.5] dark:fill-[#404040] dark:stroke-[#404040]"
/>
</g>
<circle
cx="189"
cy="28"
r="9"
className="fill-white dark:fill-[#262626]"
/>
<circle
cx="189"
cy="28"
r="4"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
{src && (
<image
href={src}
width="360"
height="800"
className="size-full object-cover"
preserveAspectRatio="xMidYMid slice"
clipPath="url(#clip0_514_20855)"
/>
)}
{videoSrc && (
<foreignObject
width="380"
height="820"
clipPath="url(#clip0_514_20855)"
preserveAspectRatio="xMidYMid slice"
>
<video
className="size-full object-cover"
src={videoSrc}
autoPlay
loop
muted
playsInline
/>
</foreignObject>
)}
<defs>
<clipPath id="clip0_514_20855">
<rect
width="360"
height="800"
rx="33"
ry="25"
className="fill-white dark:fill-[#262626]"
transform="translate(9 14)"
/>
</clipPath>
</defs>
</svg>
) }
===== EXAMPLE: android-demo ===== Title: Android Demo
--- file: example/android-demo.tsx --- import { Android } from "@/registry/magicui/android"
export default function AndroidDemo() { return ( ) }
===== EXAMPLE: android-demo-2 ===== Title: Android Demo 2
--- file: example/android-demo-2.tsx --- import { Android } from "@/registry/magicui/android"
export default function AndroidDemo() { return ( ) }
===== EXAMPLE: android-demo-3 ===== Title: Android Demo 3
--- file: example/android-demo-3.tsx --- import { Android } from "@/registry/magicui/android"
export default function AndroidDemo() { return ( ) }
===== COMPONENT: animated-beam ===== Title: Animated Beam Description: An animated beam of light which travels along a path. Useful for showcasing the integration features of a website.
--- file: magicui/animated-beam.tsx --- "use client"
import { RefObject, useEffect, useId, useState } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
export interface AnimatedBeamProps { className?: string containerRef: RefObject<HTMLElement | null> // Container ref fromRef: RefObject<HTMLElement | null> toRef: RefObject<HTMLElement | null> curvature?: number reverse?: boolean pathColor?: string pathWidth?: number pathOpacity?: number gradientStartColor?: string gradientStopColor?: string delay?: number duration?: number startXOffset?: number startYOffset?: number endXOffset?: number endYOffset?: number }
export const AnimatedBeam: React.FC = ({ className, containerRef, fromRef, toRef, curvature = 0, reverse = false, // Include the reverse prop duration = Math.random() * 3 + 4, delay = 0, pathColor = "gray", pathWidth = 2, pathOpacity = 0.2, gradientStartColor = "#ffaa40", gradientStopColor = "#9c40ff", startXOffset = 0, startYOffset = 0, endXOffset = 0, endYOffset = 0, }) => { const id = useId() const [pathD, setPathD] = useState("") const [svgDimensions, setSvgDimensions] = useState({ width: 0, height: 0 })
// Calculate the gradient coordinates based on the reverse prop const gradientCoordinates = reverse ? { x1: ["90%", "-10%"], x2: ["100%", "0%"], y1: ["0%", "0%"], y2: ["0%", "0%"], } : { x1: ["10%", "110%"], x2: ["0%", "100%"], y1: ["0%", "0%"], y2: ["0%", "0%"], }
useEffect(() => { const updatePath = () => { if (containerRef.current && fromRef.current && toRef.current) { const containerRect = containerRef.current.getBoundingClientRect() const rectA = fromRef.current.getBoundingClientRect() const rectB = toRef.current.getBoundingClientRect()
const svgWidth = containerRect.width
const svgHeight = containerRect.height
setSvgDimensions({ width: svgWidth, height: svgHeight })
const startX =
rectA.left - containerRect.left + rectA.width / 2 + startXOffset
const startY =
rectA.top - containerRect.top + rectA.height / 2 + startYOffset
const endX =
rectB.left - containerRect.left + rectB.width / 2 + endXOffset
const endY =
rectB.top - containerRect.top + rectB.height / 2 + endYOffset
const controlY = startY - curvature
const d = `M ${startX},${startY} Q ${
(startX + endX) / 2
},${controlY} ${endX},${endY}`
setPathD(d)
}
}
// Initialize ResizeObserver
const resizeObserver = new ResizeObserver(() => {
updatePath()
})
// Observe the container element
if (containerRef.current) {
resizeObserver.observe(containerRef.current)
}
// Call the updatePath initially to set the initial path
updatePath()
// Clean up the observer on component unmount
return () => {
resizeObserver.disconnect()
}
}, [ containerRef, fromRef, toRef, curvature, startXOffset, startYOffset, endXOffset, endYOffset, ])
return (
<svg
fill="none"
width={svgDimensions.width}
height={svgDimensions.height}
xmlns="http://www.w3.org/2000/svg"
className={cn(
"pointer-events-none absolute top-0 left-0 transform-gpu stroke-2",
className
)}
viewBox={0 0 ${svgDimensions.width} ${svgDimensions.height}}
>
<path
d={pathD}
strokeWidth={pathWidth}
stroke={url(#${id})}
strokeOpacity="1"
strokeLinecap="round"
/>
<motion.linearGradient
className="transform-gpu"
id={id}
gradientUnits={"userSpaceOnUse"}
initial={{
x1: "0%",
x2: "0%",
y1: "0%",
y2: "0%",
}}
animate={{
x1: gradientCoordinates.x1,
x2: gradientCoordinates.x2,
y1: gradientCoordinates.y1,
y2: gradientCoordinates.y2,
}}
transition={{
delay,
duration,
ease: [0.16, 1, 0.3, 1], // https://easings.net/#easeOutExpo
repeat: Infinity,
repeatDelay: 0,
}}
>
</motion.linearGradient>
)
}
===== EXAMPLE: animated-beam-demo ===== Title: Animated Beam Demo
--- file: example/animated-beam-demo.tsx --- "use client"
import React, { forwardRef, useRef } from "react"
import { cn } from "@/lib/utils" import { AnimatedBeam } from "@/registry/magicui/animated-beam"
const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }
(({ className, children }, ref) => { return ( <div ref={ref} className={cn( "z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]", className )} > {children} ) })
Circle.displayName = "Circle"
export default function AnimatedBeamDemo() { const containerRef = useRef(null) const div1Ref = useRef(null) const div2Ref = useRef(null) const div3Ref = useRef(null) const div4Ref = useRef(null) const div5Ref = useRef(null) const div6Ref = useRef(null) const div7Ref = useRef(null)
return ( <Icons.googleDrive /> <Icons.googleDocs /> <Icons.notion /> <Icons.openai /> <Icons.zapier /> <Icons.whatsapp /> <Icons.messenger />
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div4Ref}
curvature={-75}
endYOffset={-10}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div2Ref}
toRef={div4Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div3Ref}
toRef={div4Ref}
curvature={75}
endYOffset={10}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div5Ref}
toRef={div4Ref}
curvature={-75}
endYOffset={-10}
reverse
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div6Ref}
toRef={div4Ref}
reverse
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div7Ref}
toRef={div4Ref}
curvature={75}
endYOffset={10}
reverse
/>
</div>
) }
const Icons = { notion: () => ( ), openai: () => ( ), googleDrive: () => ( ), whatsapp: () => ( ), googleDocs: () => ( ), zapier: () => ( ), messenger: () => ( ), }
===== EXAMPLE: animated-beam-unidirectional ===== Title: Animated Beam Unidirectional
--- file: example/animated-beam-unidirectional.tsx --- "use client"
import React, { forwardRef, useRef } from "react"
import { cn } from "@/lib/utils" import { AnimatedBeam } from "@/registry/magicui/animated-beam"
const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }
(({ className, children }, ref) => { return ( <div ref={ref} className={cn( "z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]", className )} > {children} ) })
Circle.displayName = "Circle"
export default function AnimatedBeamDemo() { const containerRef = useRef(null) const div1Ref = useRef(null) const div2Ref = useRef(null)
return ( <Icons.user /> <Icons.openai />
<AnimatedBeam
duration={3}
containerRef={containerRef}
fromRef={div1Ref}
toRef={div2Ref}
/>
</div>
) }
const Icons = { openai: () => ( ), user: () => ( ), }
===== EXAMPLE: animated-beam-bidirectional ===== Title: Animated Beam Bidirectional
--- file: example/animated-beam-bidirectional.tsx --- "use client"
import React, { forwardRef, useRef } from "react"
import { cn } from "@/lib/utils" import { AnimatedBeam } from "@/registry/magicui/animated-beam"
const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }
(({ className, children }, ref) => { return ( <div ref={ref} className={cn( "z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]", className )} > {children} ) })
Circle.displayName = "Circle"
export default function AnimatedBeamDemo() { const containerRef = useRef(null) const div1Ref = useRef(null) const div2Ref = useRef(null)
return ( <Icons.user /> <Icons.openai />
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div2Ref}
startYOffset={10}
endYOffset={10}
curvature={-20}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div2Ref}
startYOffset={-10}
endYOffset={-10}
curvature={20}
reverse
/>
</div>
) }
const Icons = { openai: () => ( ), user: () => ( ), }
===== EXAMPLE: animated-beam-multiple-inputs ===== Title: Animated Beam Multiple Inputs
--- file: example/animated-beam-multiple-inputs.tsx --- "use client"
import React, { forwardRef, useRef } from "react"
import { cn } from "@/lib/utils" import { AnimatedBeam } from "@/registry/magicui/animated-beam"
const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }
(({ className, children }, ref) => { return ( <div ref={ref} className={cn( "border-border z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]", className )} > {children} ) })
Circle.displayName = "Circle"
export default function AnimatedBeamMultipleOutputDemo({ className, }: { className?: string }) { const containerRef = useRef(null) const div1Ref = useRef(null) const div2Ref = useRef(null) const div3Ref = useRef(null) const div4Ref = useRef(null) const div5Ref = useRef(null) const div6Ref = useRef(null) const div7Ref = useRef(null)
return ( <div className={cn( "relative flex h-[500px] w-full items-center justify-center overflow-hidden p-10", className )} ref={containerRef} > <Icons.googleDrive /> <Icons.googleDocs /> <Icons.whatsapp /> <Icons.messenger /> <Icons.notion /> <Icons.openai /> <Icons.user />
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div6Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div2Ref}
toRef={div6Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div3Ref}
toRef={div6Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div4Ref}
toRef={div6Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div5Ref}
toRef={div6Ref}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div6Ref}
toRef={div7Ref}
/>
</div>
) }
const Icons = { notion: () => ( ), openai: () => ( ), googleDrive: () => ( ), whatsapp: () => ( ), googleDocs: () => ( ), zapier: () => ( ), messenger: () => ( ), user: () => ( ), }
===== EXAMPLE: animated-beam-multiple-outputs ===== Title: Animated Beam Multiple Outputs
--- file: example/animated-beam-multiple-outputs.tsx --- "use client"
import React, { forwardRef, useRef } from "react"
import { cn } from "@/lib/utils" import { AnimatedBeam } from "@/registry/magicui/animated-beam"
const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }
(({ className, children }, ref) => { return ( <div ref={ref} className={cn( "z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white p-3 shadow-[0_0_20px_-12px_rgba(0,0,0,0.8)]", className )} > {children} ) })
Circle.displayName = "Circle"
export default function AnimatedBeamMultipleOutputDemo({ className, }: { className?: string }) { const containerRef = useRef(null) const div1Ref = useRef(null) const div2Ref = useRef(null) const div3Ref = useRef(null) const div4Ref = useRef(null) const div5Ref = useRef(null) const div6Ref = useRef(null) const div7Ref = useRef(null)
return ( <div className={cn( "relative flex h-[500px] w-full items-center justify-center overflow-hidden p-10", className )} ref={containerRef} > <Icons.user /> <Icons.openai /> <Icons.googleDrive /> <Icons.googleDocs /> <Icons.whatsapp /> <Icons.messenger /> <Icons.notion />
{/* AnimatedBeams */}
<AnimatedBeam
containerRef={containerRef}
fromRef={div1Ref}
toRef={div6Ref}
duration={3}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div2Ref}
toRef={div6Ref}
duration={3}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div3Ref}
toRef={div6Ref}
duration={3}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div4Ref}
toRef={div6Ref}
duration={3}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div5Ref}
toRef={div6Ref}
duration={3}
/>
<AnimatedBeam
containerRef={containerRef}
fromRef={div6Ref}
toRef={div7Ref}
duration={3}
/>
</div>
) }
const Icons = { notion: () => ( ), openai: () => ( ), googleDrive: () => ( ), whatsapp: () => ( ), googleDocs: () => ( ), zapier: () => ( ), messenger: () => ( ), user: () => ( ), }
===== COMPONENT: animated-circular-progress-bar ===== Title: Animated Circular Progress Bar Description: Animated Circular Progress Bar is a component that displays a circular gauge with a percentage value.
--- file: magicui/animated-circular-progress-bar.tsx --- import { cn } from "@/lib/utils"
interface AnimatedCircularProgressBarProps { max?: number min?: number value: number gaugePrimaryColor: string gaugeSecondaryColor: string className?: string }
export function AnimatedCircularProgressBar({ max = 100, min = 0, value = 0, gaugePrimaryColor, gaugeSecondaryColor, className, }: AnimatedCircularProgressBarProps) { const circumference = 2 * Math.PI * 45 const percentPx = circumference / 100 const currentPercent = Math.round(((value - min) / (max - min)) * 100)
return (
<div
className={cn("relative size-40 text-2xl font-semibold", className)}
style={
{
"--circle-size": "100px",
"--circumference": circumference,
"--percent-to-px": ${percentPx}px,
"--gap-percent": "5",
"--offset-factor": "0",
"--transition-length": "1s",
"--transition-step": "200ms",
"--delay": "0s",
"--percent-to-deg": "3.6deg",
transform: "translateZ(0)",
} as React.CSSProperties
}
>
{currentPercent <= 90 && currentPercent >= 0 && (
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-100"
style={
{
stroke: gaugeSecondaryColor,
"--stroke-percent": 90 - currentPercent,
"--offset-factor-secondary": "calc(1 - var(--offset-factor))",
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transform:
"rotate(calc(1turn - 90deg - (var(--gap-percent) * var(--percent-to-deg) * var(--offset-factor-secondary)))) scaleY(-1)",
transition: "all var(--transition-length) ease var(--delay)",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
)}
<circle
cx="50"
cy="50"
r="45"
strokeWidth="10"
strokeDashoffset="0"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-100"
style={
{
stroke: gaugePrimaryColor,
"--stroke-percent": currentPercent,
strokeDasharray:
"calc(var(--stroke-percent) * var(--percent-to-px)) var(--circumference)",
transition:
"var(--transition-length) ease var(--delay),stroke var(--transition-length) ease var(--delay)",
transitionProperty: "stroke-dasharray,transform",
transform:
"rotate(calc(-90deg + var(--gap-percent) * var(--offset-factor) * var(--percent-to-deg)))",
transformOrigin:
"calc(var(--circle-size) / 2) calc(var(--circle-size) / 2)",
} as React.CSSProperties
}
/>
{currentPercent}
)
}
===== EXAMPLE: animated-circular-progress-bar-demo ===== Title: Animated Circular Progress Bar Demo
--- file: example/animated-circular-progress-bar-demo.tsx --- "use client"
import { useEffect, useState } from "react"
import { AnimatedCircularProgressBar } from "@/registry/magicui/animated-circular-progress-bar"
export default function AnimatedCircularProgressBarDemo() { const [value, setValue] = useState(0)
useEffect(() => { const handleIncrement = (prev: number) => { if (prev === 100) { return 0 } return prev + 10 } setValue(handleIncrement) const interval = setInterval(() => setValue(handleIncrement), 2000) return () => clearInterval(interval) }, [])
return ( ) }
===== COMPONENT: animated-gradient-text ===== Title: Animated Gradient Text Description: An animated gradient background which transitions between colors for text.
--- file: magicui/animated-gradient-text.tsx --- import { ComponentPropsWithoutRef } from "react"
import { cn } from "@/lib/utils"
export interface AnimatedGradientTextProps extends ComponentPropsWithoutRef<"div"> { speed?: number colorFrom?: string colorTo?: string }
export function AnimatedGradientText({
children,
className,
speed = 1,
colorFrom = "#ffaa40",
colorTo = "#9c40ff",
...props
}: AnimatedGradientTextProps) {
return (
<span
style={
{
"--bg-size": ${speed * 300}%,
"--color-from": colorFrom,
"--color-to": colorTo,
} as React.CSSProperties
}
className={cn(
animate-gradient inline bg-gradient-to-r from-[var(--color-from)] via-[var(--color-to)] to-[var(--color-from)] bg-[length:var(--bg-size)_100%] bg-clip-text text-transparent,
className
)}
{...props}
>
{children}
)
}
===== EXAMPLE: animated-gradient-text-demo ===== Title: Animated Gradient Text Demo
--- file: example/animated-gradient-text-demo.tsx --- import { ChevronRight } from "lucide-react"
import { cn } from "@/lib/utils" import { AnimatedGradientText } from "@/registry/magicui/animated-gradient-text"
export default function AnimatedGradientTextDemo() { return ( <span className={cn( "animate-gradient absolute inset-0 block h-full w-full rounded-[inherit] bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:300%_100%] p-[1px]" )} style={{ WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)", WebkitMaskComposite: "destination-out", mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)", maskComposite: "subtract", WebkitClipPath: "padding-box", }} /> 🎉 Introducing Magic UI ) }
===== EXAMPLE: animated-gradient-text-demo-2 ===== Title: Animated Gradient Text Demo 2
--- file: example/animated-gradient-text-demo-2.tsx --- import { AnimatedGradientText } from "@/registry/magicui/animated-gradient-text"
export default function AnimatedGradientTextDemo() { return ( Fast Gradient ) }
===== COMPONENT: animated-grid-pattern ===== Title: Animated Grid Pattern Description: A animated background grid pattern made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/animated-grid-pattern.tsx --- "use client"
import { ComponentPropsWithoutRef, useCallback, useEffect, useId, useRef, useState, } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
export interface AnimatedGridPatternProps extends ComponentPropsWithoutRef<"svg"> { width?: number height?: number x?: number y?: number strokeDasharray?: number numSquares?: number maxOpacity?: number duration?: number repeatDelay?: number }
type Square = { id: number pos: [number, number] iteration: number }
export function AnimatedGridPattern({ width = 40, height = 40, x = -1, y = -1, strokeDasharray = 0, numSquares = 50, className, maxOpacity = 0.5, duration = 4, repeatDelay = 0.5, ...props }: AnimatedGridPatternProps) { const id = useId() const containerRef = useRef<SVGSVGElement | null>(null) const [dimensions, setDimensions] = useState({ width: 0, height: 0 }) const [squares, setSquares] = useState<Array>([])
const getPos = useCallback((): [number, number] => { return [ Math.floor((Math.random() * dimensions.width) / width), Math.floor((Math.random() * dimensions.height) / height), ] }, [dimensions.height, dimensions.width, height, width])
const generateSquares = useCallback( (count: number) => { return Array.from({ length: count }, (_, i) => ({ id: i, pos: getPos(), iteration: 0, })) }, [getPos] )
const updateSquarePosition = useCallback( (squareId: number) => { setSquares((currentSquares) => { const current = currentSquares[squareId] if (!current || current.id !== squareId) return currentSquares
const nextSquares = currentSquares.slice()
nextSquares[squareId] = {
...current,
pos: getPos(),
iteration: current.iteration + 1,
}
return nextSquares
})
},
[getPos]
)
useEffect(() => { if (dimensions.width && dimensions.height) { setSquares(generateSquares(numSquares)) } }, [dimensions.width, dimensions.height, generateSquares, numSquares])
useEffect(() => { const element = containerRef.current if (!element) return
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
setDimensions((currentDimensions) => {
const nextWidth = entry.contentRect.width
const nextHeight = entry.contentRect.height
if (
currentDimensions.width === nextWidth &&
currentDimensions.height === nextHeight
) {
return currentDimensions
}
return { width: nextWidth, height: nextHeight }
})
}
})
resizeObserver.observe(element)
return () => {
resizeObserver.disconnect()
}
}, [])
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className
)}
{...props}
>
<path
d={M.5 ${height}V.5H${width}}
fill="none"
strokeDasharray={strokeDasharray}
/>
<rect width="100%" height="100%" fill={url(#${id})} />
{squares.map(({ pos: [squareX, squareY], id, iteration }, index) => (
<motion.rect
initial={{ opacity: 0 }}
animate={{ opacity: maxOpacity }}
transition={{
duration,
repeat: 1,
delay: index * 0.1,
repeatType: "reverse",
repeatDelay,
}}
onAnimationComplete={() => updateSquarePosition(id)}
key={${id}-${iteration}}
width={width - 1}
height={height - 1}
x={squareX * width + 1}
y={squareY * height + 1}
fill="currentColor"
strokeWidth="0"
/>
))}
)
}
===== EXAMPLE: animated-grid-pattern-demo ===== Title: Animated Grid Pattern Demo
--- file: example/animated-grid-pattern-demo.tsx --- import { cn } from "@/lib/utils" import { AnimatedGridPattern } from "@/registry/magicui/animated-grid-pattern"
export default function AnimatedGridPatternDemo() { return ( <AnimatedGridPattern numSquares={30} maxOpacity={0.1} duration={3} repeatDelay={1} className={cn( "mask-[radial-gradient(500px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> ) }
===== COMPONENT: animated-list ===== Title: Animated List Description: A list that animates each item in sequence with a delay. Used to showcase notifications or events on your landing page.
--- file: magicui/animated-list.tsx --- "use client"
import React, { ComponentPropsWithoutRef, useEffect, useMemo, useState, } from "react" import { AnimatePresence, motion, MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
export function AnimatedListItem({ children }: { children: React.ReactNode }) { const animations: MotionProps = { initial: { scale: 0, opacity: 0 }, animate: { scale: 1, opacity: 1, originY: 0 }, exit: { scale: 0, opacity: 0 }, transition: { type: "spring", stiffness: 350, damping: 40 }, }
return ( <motion.div {...animations} layout className="mx-auto w-full"> {children} </motion.div> ) }
export interface AnimatedListProps extends ComponentPropsWithoutRef<"div"> { children: React.ReactNode delay?: number }
export const AnimatedList = React.memo( ({ children, className, delay = 1000, ...props }: AnimatedListProps) => { const [index, setIndex] = useState(0) const childrenArray = useMemo( () => React.Children.toArray(children), [children] )
useEffect(() => {
if (index < childrenArray.length - 1) {
const timeout = setTimeout(() => {
setIndex((prevIndex) => (prevIndex + 1) % childrenArray.length)
}, delay)
return () => clearTimeout(timeout)
}
}, [index, delay, childrenArray.length])
const itemsToShow = useMemo(() => {
const result = childrenArray.slice(0, index + 1).reverse()
return result
}, [index, childrenArray])
return (
<div
className={cn(`flex flex-col items-center gap-4`, className)}
{...props}
>
<AnimatePresence>
{itemsToShow.map((item) => (
<AnimatedListItem key={(item as React.ReactElement).key}>
{item}
</AnimatedListItem>
))}
</AnimatePresence>
</div>
)
} )
AnimatedList.displayName = "AnimatedList"
===== EXAMPLE: animated-list-demo ===== Title: Animated List Demo
--- file: example/animated-list-demo.tsx --- "use client"
import { cn } from "@/lib/utils" import { AnimatedList } from "@/registry/magicui/animated-list"
interface Item { name: string description: string icon: string color: string time: string }
let notifications = [ { name: "Payment received", description: "Magic UI", time: "15m ago",
icon: "💸",
color: "#00C9A7",
}, { name: "User signed up", description: "Magic UI", time: "10m ago", icon: "👤", color: "#FFB800", }, { name: "New message", description: "Magic UI", time: "5m ago", icon: "💬", color: "#FF3D71", }, { name: "New event", description: "Magic UI", time: "2m ago", icon: "🗞️", color: "#1E86FF", }, ]
notifications = Array.from({ length: 10 }, () => notifications).flat()
const Notification = ({ name, description, icon, color, time }: Item) => { return ( <figure className={cn( "relative mx-auto min-h-fit w-full max-w-[400px] cursor-pointer overflow-hidden rounded-2xl p-4", // animation styles "transition-all duration-200 ease-in-out hover:scale-[103%]", // light styles "bg-white [box-shadow:0_0_0_1px_rgba(0,0,0,.03),0_2px_4px_rgba(0,0,0,.05),0_12px_24px_rgba(0,0,0,.05)]", // dark styles "transform-gpu dark:bg-transparent dark:[box-shadow:0_-20px_80px_-20px_#ffffff1f_inset] dark:backdrop-blur-md dark:[border:1px_solid_rgba(255,255,255,.1)]" )} > <div className="flex size-10 items-center justify-center rounded-2xl" style={{ backgroundColor: color, }} > {icon} {name} · {time} {description} ) }
export default function AnimatedListDemo({ className, }: { className?: string }) { return ( <div className={cn( "relative flex h-[500px] w-full flex-col overflow-hidden p-2", className )} > {notifications.map((item, idx) => ( <Notification {...item} key={idx} /> ))}
<div className="from-background pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t"></div>
</div>
) }
===== COMPONENT: animated-shiny-text ===== Title: Animated Shiny Text Description: A light glare effect which pans across text making it appear as if it is shimmering.
--- file: magicui/animated-shiny-text.tsx --- import { ComponentPropsWithoutRef, CSSProperties, FC } from "react"
import { cn } from "@/lib/utils"
export interface AnimatedShinyTextProps extends ComponentPropsWithoutRef<"span"> { shimmerWidth?: number }
export const AnimatedShinyText: FC = ({
children,
className,
shimmerWidth = 100,
...props
}) => {
return (
<span
style={
{
"--shiny-width": ${shimmerWidth}px,
} as CSSProperties
}
className={cn(
"mx-auto max-w-md text-neutral-600/70 dark:text-neutral-400/70",
// Shine effect
"animate-shiny-text [background-size:var(--shiny-width)_100%] bg-clip-text [background-position:0_0] bg-no-repeat [transition:background-position_1s_cubic-bezier(.6,.6,0,1)_infinite]",
// Shine gradient
"bg-gradient-to-r from-transparent via-black/80 via-50% to-transparent dark:via-white/80",
className
)}
{...props}
>
{children}
</span>
) }
===== EXAMPLE: animated-shiny-text-demo ===== Title: Animated Shiny Text Demo
--- file: example/animated-shiny-text-demo.tsx --- import { ArrowRightIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils" import { AnimatedShinyText } from "@/registry/magicui/animated-shiny-text"
export default function AnimatedShinyTextDemo() { return ( <div className={cn( "group rounded-full border border-black/5 bg-neutral-100 text-base text-white transition-all ease-in hover:cursor-pointer hover:bg-neutral-200 dark:border-white/5 dark:bg-neutral-900 dark:hover:bg-neutral-800" )} > ✨ Introducing Magic UI ) }
===== COMPONENT: animated-theme-toggler ===== Title: Theme Toggler Description: A component for theme changing animation.
--- file: magicui/animated-theme-toggler.tsx --- "use client"
import { useCallback, useEffect, useRef, useState } from "react" import { Moon, Sun } from "lucide-react" import { flushSync } from "react-dom"
import { cn } from "@/lib/utils"
interface AnimatedThemeTogglerProps extends React.ComponentPropsWithoutRef<"button"> { duration?: number }
export const AnimatedThemeToggler = ({ className, duration = 400, ...props }: AnimatedThemeTogglerProps) => { const [isDark, setIsDark] = useState(false) const buttonRef = useRef(null)
useEffect(() => { const updateTheme = () => { setIsDark(document.documentElement.classList.contains("dark")) }
updateTheme()
const observer = new MutationObserver(updateTheme)
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["class"],
})
return () => observer.disconnect()
}, [])
const toggleTheme = useCallback(async () => { if (!buttonRef.current) return
await document.startViewTransition(() => {
flushSync(() => {
const newTheme = !isDark
setIsDark(newTheme)
document.documentElement.classList.toggle("dark")
localStorage.setItem("theme", newTheme ? "dark" : "light")
})
}).ready
const { top, left, width, height } =
buttonRef.current.getBoundingClientRect()
const x = left + width / 2
const y = top + height / 2
const maxRadius = Math.hypot(
Math.max(left, window.innerWidth - left),
Math.max(top, window.innerHeight - top)
)
document.documentElement.animate(
{
clipPath: [
`circle(0px at ${x}px ${y}px)`,
`circle(${maxRadius}px at ${x}px ${y}px)`,
],
},
{
duration,
easing: "ease-in-out",
pseudoElement: "::view-transition-new(root)",
}
)
}, [isDark, duration])
return ( <button ref={buttonRef} onClick={toggleTheme} className={cn(className)} {...props} > {isDark ? : } Toggle theme ) }
===== EXAMPLE: animated-theme-toggler-demo ===== Title: Animated Theme Toggler Demo
--- file: example/animated-theme-toggler-demo.tsx --- import { AnimatedThemeToggler } from "@/registry/magicui/animated-theme-toggler"
export default function AnimatedThemeTogglerDemo() { return }
===== COMPONENT: aurora-text ===== Title: Aurora Text Description: A beautiful aurora text effect
--- file: magicui/aurora-text.tsx --- "use client"
import React, { memo } from "react"
interface AuroraTextProps { children: React.ReactNode className?: string colors?: string[] speed?: number }
export const AuroraText = memo(
({
children,
className = "",
colors = ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"],
speed = 1,
}: AuroraTextProps) => {
const gradientStyle = {
backgroundImage: linear-gradient(135deg, ${colors.join(", ")}, ${ colors[0] }),
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
animationDuration: ${10 / speed}s,
}
return (
<span className={`relative inline-block ${className}`}>
<span className="sr-only">{children}</span>
<span
className="animate-aurora relative bg-size-[200%_auto] bg-clip-text text-transparent"
style={gradientStyle}
aria-hidden="true"
>
{children}
</span>
</span>
)
} )
AuroraText.displayName = "AuroraText"
===== EXAMPLE: aurora-text-demo ===== Title: Aurora Text Demo
--- file: example/aurora-text-demo.tsx --- import { AuroraText } from "@/registry/magicui/aurora-text"
export default function AuroraTextDemo() { return ( Ship beautiful ) }
===== COMPONENT: avatar-circles ===== Title: Avatar Circles Description: Overlapping circles of avatars.
--- file: magicui/avatar-circles.tsx --- /* eslint-disable @next/next/no-img-element */ "use client"
import { cn } from "@/lib/utils"
interface Avatar { imageUrl: string profileUrl: string } interface AvatarCirclesProps { className?: string numPeople?: number avatarUrls: Avatar[] }
export const AvatarCircles = ({
numPeople,
className,
avatarUrls,
}: AvatarCirclesProps) => {
return (
<div className={cn("z-10 flex -space-x-4 rtl:space-x-reverse", className)}>
{avatarUrls.map((url, index) => (
<img
key={index}
className="h-10 w-10 rounded-full border-2 border-white dark:border-gray-800"
src={url.imageUrl}
width={40}
height={40}
alt={Avatar ${index + 1}}
/>
))}
{(numPeople ?? 0) > 0 && (
+{numPeople}
)}
)
}
===== EXAMPLE: avatar-circles-demo ===== Title: Avatar Circles Demo
--- file: example/avatar-circles-demo.tsx --- import { AvatarCircles } from "@/registry/magicui/avatar-circles"
const avatars = [ { imageUrl: "https://avatars.githubusercontent.com/u/16860528", profileUrl: "https://github.com/dillionverma", }, { imageUrl: "https://avatars.githubusercontent.com/u/20110627", profileUrl: "https://github.com/tomonarifeehan", }, { imageUrl: "https://avatars.githubusercontent.com/u/106103625", profileUrl: "https://github.com/BankkRoll", }, { imageUrl: "https://avatars.githubusercontent.com/u/59228569", profileUrl: "https://github.com/safethecode", }, { imageUrl: "https://avatars.githubusercontent.com/u/59442788", profileUrl: "https://github.com/sanjay-mali", }, { imageUrl: "https://avatars.githubusercontent.com/u/89768406", profileUrl: "https://github.com/itsarghyadas", }, ]
export default function AvatarCirclesDemo() { return }
===== COMPONENT: bento-grid ===== Title: Bento Grid Description: Bento grid is a layout used to showcase the features of a product in a simple and elegant way.
--- file: magicui/bento-grid.tsx --- import { ComponentPropsWithoutRef, ReactNode } from "react" import { ArrowRightIcon } from "@radix-ui/react-icons"
import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button"
interface BentoGridProps extends ComponentPropsWithoutRef<"div"> { children: ReactNode className?: string }
interface BentoCardProps extends ComponentPropsWithoutRef<"div"> { name: string className: string background: ReactNode Icon: React.ElementType description: string href: string cta: string }
const BentoGrid = ({ children, className, ...props }: BentoGridProps) => { return ( <div className={cn( "grid w-full auto-rows-[22rem] grid-cols-3 gap-4", className )} {...props} > {children} ) }
const BentoCard = ({ name, className, background, Icon, description, href, cta, ...props }: BentoCardProps) => (
<div
className={cn(
"pointer-events-none flex w-full translate-y-0 transform-gpu flex-row items-center transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:hidden"
)}
>
<Button
variant="link"
asChild
size="sm"
className="pointer-events-auto p-0"
>
<a href={href}>
{cta}
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
</a>
</Button>
</div>
</div>
<div
className={cn(
"pointer-events-none absolute bottom-0 hidden w-full translate-y-10 transform-gpu flex-row items-center p-4 opacity-0 transition-all duration-300 group-hover:translate-y-0 group-hover:opacity-100 lg:flex"
)}
>
<Button
variant="link"
asChild
size="sm"
className="pointer-events-auto p-0"
>
<a href={href}>
{cta}
<ArrowRightIcon className="ms-2 h-4 w-4 rtl:rotate-180" />
</a>
</Button>
</div>
<div className="pointer-events-none absolute inset-0 transform-gpu transition-all duration-300 group-hover:bg-black/[.03] group-hover:dark:bg-neutral-800/10" />
export { BentoCard, BentoGrid }
===== EXAMPLE: bento-demo ===== Title: Bento Demo
--- file: example/bento-demo.tsx --- import { CalendarIcon, FileTextIcon } from "@radix-ui/react-icons" import { BellIcon, Share2Icon } from "lucide-react"
import { cn } from "@/lib/utils" import { Calendar } from "@/components/ui/calendar" import AnimatedBeamMultipleOutputDemo from "@/registry/example/animated-beam-multiple-outputs" import AnimatedListDemo from "@/registry/example/animated-list-demo" import { BentoCard, BentoGrid } from "@/registry/magicui/bento-grid" import { Marquee } from "@/registry/magicui/marquee"
const files = [ { name: "bitcoin.pdf", body: "Bitcoin is a cryptocurrency invented in 2008 by an unknown person or group of people using the name Satoshi Nakamoto.", }, { name: "finances.xlsx", body: "A spreadsheet or worksheet is a file made of rows and columns that help sort data, arrange data easily, and calculate numerical data.", }, { name: "logo.svg", body: "Scalable Vector Graphics is an Extensible Markup Language-based vector image format for two-dimensional graphics with support for interactivity and animation.", }, { name: "keys.gpg", body: "GPG keys are used to encrypt and decrypt email, files, directories, and whole disk partitions and to authenticate messages.", }, { name: "seed.txt", body: "A seed phrase, seed recovery phrase or backup seed phrase is a list of words which store all the information needed to recover Bitcoin funds on-chain.", }, ]
const features = [ { Icon: FileTextIcon, name: "Save your files", description: "We automatically save your files as you type.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-1", background: ( {files.map((f, idx) => ( <figure key={idx} className={cn( "relative w-32 cursor-pointer overflow-hidden rounded-xl border p-4", "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]", "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]", "transform-gpu blur-[1px] transition-all duration-300 ease-out hover:blur-none" )} > {f.name} {f.body} ))} ), }, { Icon: BellIcon, name: "Notifications", description: "Get notified when something happens.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: ( ), }, { Icon: Share2Icon, name: "Integrations", description: "Supports 100+ integrations and counting.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: ( ), }, { Icon: CalendarIcon, name: "Calendar", description: "Use the calendar to filter your files by date.", className: "col-span-3 lg:col-span-1", href: "#", cta: "Learn more", background: ( <Calendar mode="single" selected={new Date(2022, 4, 11, 0, 0, 0)} className="absolute top-10 right-0 origin-top scale-75 rounded-md border [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] transition-all duration-300 ease-out group-hover:scale-90" /> ), }, ]
export default function BentoDemo() { return ( {features.map((feature, idx) => ( <BentoCard key={idx} {...feature} /> ))} ) }
===== EXAMPLE: bento-demo-vertical ===== Title: Bento Vertical Demo
--- file: example/bento-demo-vertical.tsx --- import { BellIcon, CalendarIcon, FileTextIcon, GlobeIcon, InputIcon, } from "@radix-ui/react-icons"
import { BentoCard, BentoGrid } from "@/registry/magicui/bento-grid"
const features = [ { Icon: FileTextIcon, name: "Save your files", description: "We automatically save your files as you type.", href: "/", cta: "Learn more", background: , className: "lg:row-start-1 lg:row-end-4 lg:col-start-2 lg:col-end-3", }, { Icon: InputIcon, name: "Full text search", description: "Search through all your files in one place.", href: "/", cta: "Learn more", background: , className: "lg:col-start-1 lg:col-end-2 lg:row-start-1 lg:row-end-3", }, { Icon: GlobeIcon, name: "Multilingual", description: "Supports 100+ languages and counting.", href: "/", cta: "Learn more", background: , className: "lg:col-start-1 lg:col-end-2 lg:row-start-3 lg:row-end-4", }, { Icon: CalendarIcon, name: "Calendar", description: "Use the calendar to filter your files by date.", href: "/", cta: "Learn more", background: , className: "lg:col-start-3 lg:col-end-3 lg:row-start-1 lg:row-end-2", }, { Icon: BellIcon, name: "Notifications", description: "Get notified when someone shares a file or mentions you in a comment.", href: "/", cta: "Learn more", background: , className: "lg:col-start-3 lg:col-end-3 lg:row-start-2 lg:row-end-4", }, ]
export default function BentoDemo() { return ( {features.map((feature) => ( <BentoCard key={feature.name} {...feature} /> ))} ) }
===== COMPONENT: blur-fade ===== Title: Blur Fade Description: Blur fade in and out animation. Used to smoothly fade in and out content.
--- file: magicui/blur-fade.tsx --- "use client"
import { useRef } from "react" import { AnimatePresence, motion, MotionProps, useInView, UseInViewOptions, Variants, } from "motion/react"
type MarginType = UseInViewOptions["margin"]
interface BlurFadeProps extends MotionProps { children: React.ReactNode className?: string variant?: { hidden: { y: number } visible: { y: number } } duration?: number delay?: number offset?: number direction?: "up" | "down" | "left" | "right" inView?: boolean inViewMargin?: MarginType blur?: string }
export function BlurFade({
children,
className,
variant,
duration = 0.4,
delay = 0,
offset = 6,
direction = "down",
inView = false,
inViewMargin = "-50px",
blur = "6px",
...props
}: BlurFadeProps) {
const ref = useRef(null)
const inViewResult = useInView(ref, { once: true, margin: inViewMargin })
const isInView = !inView || inViewResult
const defaultVariants: Variants = {
hidden: {
[direction === "left" || direction === "right" ? "x" : "y"]:
direction === "right" || direction === "down" ? -offset : offset,
opacity: 0,
filter: blur(${blur}),
},
visible: {
[direction === "left" || direction === "right" ? "x" : "y"]: 0,
opacity: 1,
filter: blur(0px),
},
}
const combinedVariants = variant || defaultVariants
return (
<motion.div
ref={ref}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
exit="hidden"
variants={combinedVariants}
transition={{
delay: 0.04 + delay,
duration,
ease: "easeOut",
}}
className={className}
{...props}
>
{children}
</motion.div>
)
}
===== EXAMPLE: blur-fade-demo ===== Title: Blur Fade Demo
--- file: example/blur-fade-demo.tsx --- /* eslint-disable @next/next/no-img-element */ import { BlurFade } from "@/registry/magicui/blur-fade"
const images = Array.from({ length: 9 }, (_, i) => {
const isLandscape = i % 2 === 0
const width = isLandscape ? 800 : 600
const height = isLandscape ? 600 : 800
return https://picsum.photos/seed/${i + 1}/${width}/${height}
})
export default function BlurFadeDemo() {
return (
{images.map((imageUrl, idx) => (
<BlurFade key={imageUrl} delay={0.25 + idx * 0.05} inView>
<img
className="mb-4 size-full rounded-lg object-contain"
src={imageUrl}
alt={Random stock image ${idx + 1}}
/>
))}
)
}
===== EXAMPLE: blur-fade-text-demo ===== Title: Blur Fade Text Demo
--- file: example/blur-fade-text-demo.tsx --- import { BlurFade } from "@/registry/magicui/blur-fade"
export default function BlurFadeTextDemo() { return ( Hello World 👋 <BlurFade delay={0.25 * 2} inView> Nice to meet you ) }
===== COMPONENT: border-beam ===== Title: Border Beam Description: An animated beam of light which travels along the border of its container.
--- file: magicui/border-beam.tsx --- "use client"
import { motion, MotionStyle, Transition } from "motion/react"
import { cn } from "@/lib/utils"
interface BorderBeamProps { /**
- The size of the border beam. / size?: number /*
- The duration of the border beam. / duration?: number /*
- The delay of the border beam. / delay?: number /*
- The color of the border beam from. / colorFrom?: string /*
- The color of the border beam to. / colorTo?: string /*
- The motion transition of the border beam. / transition?: Transition /*
- The class name of the border beam. / className?: string /*
- The style of the border beam. / style?: React.CSSProperties /*
- Whether to reverse the animation direction. / reverse?: boolean /*
- The initial offset position (0-100). / initialOffset?: number /*
- The border width of the beam. */ borderWidth?: number }
export const BorderBeam = ({
className,
size = 50,
delay = 0,
duration = 6,
colorFrom = "#ffaa40",
colorTo = "#9c40ff",
transition,
style,
reverse = false,
initialOffset = 0,
borderWidth = 1,
}: BorderBeamProps) => {
return (
<div
className="pointer-events-none absolute inset-0 rounded-[inherit] border-(length:--border-beam-width) border-transparent mask-[linear-gradient(transparent,transparent),linear-gradient(#000,#000)] mask-intersect [mask-clip:padding-box,border-box]"
style={
{
"--border-beam-width": ${borderWidth}px,
} as React.CSSProperties
}
>
<motion.div
className={cn(
"absolute aspect-square",
"bg-linear-to-l from-(--color-from) via-(--color-to) to-transparent",
className
)}
style={
{
width: size,
offsetPath: rect(0 auto auto 0 round ${size}px),
"--color-from": colorFrom,
"--color-to": colorTo,
...style,
} as MotionStyle
}
initial={{ offsetDistance: ${initialOffset}% }}
animate={{
offsetDistance: reverse
? [${100 - initialOffset}%, ${-initialOffset}%]
: [${initialOffset}%, ${100 + initialOffset}%],
}}
transition={{
repeat: Infinity,
ease: "linear",
duration,
delay: -delay,
...transition,
}}
/>
)
}
===== EXAMPLE: border-beam-demo ===== Title: Border Beam Demo
--- file: example/border-beam-demo.tsx --- import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { BorderBeam } from "@/registry/magicui/border-beam"
export default function Component() { return ( Login Enter your credentials to access your account. Email Password Register Login ) }
===== EXAMPLE: border-beam-demo-2 ===== Title: Border Beam Demo
--- file: example/border-beam-demo-2.tsx --- import { Play, SkipBack, SkipForward } from "lucide-react"
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { BorderBeam } from "@/registry/magicui/border-beam"
export default function MusicPlayer() { return ( Now Playing Stairway to Heaven - Led Zeppelin 2:45 8:02 ) }
===== EXAMPLE: border-beam-demo-3 ===== Title: Border Beam Demo 3
--- file: example/border-beam-demo-3.tsx --- import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { BorderBeam } from "@/registry/magicui/border-beam"
export default function LoginForm() { return ( Login Enter your credentials to access your account. Email Password Register Login ) }
===== EXAMPLE: border-beam-demo-4 ===== Title: Border Beam Demo 4
--- file: example/border-beam-demo-4.tsx --- import { Button } from "@/components/ui/button" import { BorderBeam } from "@/registry/magicui/border-beam"
export default function Component() { return ( Buy Now <BorderBeam size={40} initialOffset={20} className="from-transparent via-yellow-500 to-transparent" transition={{ type: "spring", stiffness: 60, damping: 20, }} /> ) }
===== 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.
--- file: magicui/client-tweet-card.tsx --- "use client"
import { TweetProps, useTweet } from "react-tweet"
import { MagicTweet, TweetNotFound, TweetSkeleton, } from "@/registry/magicui/tweet-card"
export const ClientTweetCard = ({ id, apiUrl, fallback = , components, fetchOptions, onError, ...props }: TweetProps & { className?: string }) => { const { data, error, isLoading } = useTweet(id, apiUrl, fetchOptions)
if (isLoading) return fallback if (error || !data) { const NotFound = components?.TweetNotFound || TweetNotFound return <NotFound error={onError ? onError(error) : error} /> }
return <MagicTweet tweet={data} {...props} /> }
===== COMPONENT: code-comparison ===== Title: Code Comparison Description: A component which compares two code snippets.
--- file: magicui/code-comparison.tsx --- "use client"
import { useEffect, useMemo, useState } from "react" import { transformerNotationDiff, transformerNotationFocus, } from "@shikijs/transformers" import { FileIcon } from "lucide-react" import { useTheme } from "next-themes"
import { cn } from "@/lib/utils"
interface CodeComparisonProps { beforeCode: string afterCode: string language: string filename: string lightTheme: string darkTheme: string highlightColor?: string }
export function CodeComparison({ beforeCode, afterCode, language, filename, lightTheme, darkTheme, highlightColor = "#ff3333", }: CodeComparisonProps) { const { theme, systemTheme } = useTheme() const [highlightedBefore, setHighlightedBefore] = useState("") const [highlightedAfter, setHighlightedAfter] = useState("") const [hasLeftFocus, setHasLeftFocus] = useState(false) const [hasRightFocus, setHasRightFocus] = useState(false)
const selectedTheme = useMemo(() => { const currentTheme = theme === "system" ? systemTheme : theme return currentTheme === "dark" ? darkTheme : lightTheme }, [theme, systemTheme, darkTheme, lightTheme])
useEffect(() => { if (highlightedBefore || highlightedAfter) { setHasLeftFocus(highlightedBefore.includes('class="line focused"')) setHasRightFocus(highlightedAfter.includes('class="line focused"')) } }, [highlightedBefore, highlightedAfter])
useEffect(() => { async function highlightCode() { try { const { codeToHtml } = await import("shiki") const { transformerNotationHighlight } = await import("@shikijs/transformers")
const before = await codeToHtml(beforeCode, {
lang: language,
theme: selectedTheme,
transformers: [
transformerNotationHighlight({ matchAlgorithm: "v3" }),
transformerNotationDiff({ matchAlgorithm: "v3" }),
transformerNotationFocus({ matchAlgorithm: "v3" }),
],
})
const after = await codeToHtml(afterCode, {
lang: language,
theme: selectedTheme,
transformers: [
transformerNotationHighlight({ matchAlgorithm: "v3" }),
transformerNotationDiff({ matchAlgorithm: "v3" }),
transformerNotationFocus({ matchAlgorithm: "v3" }),
],
})
setHighlightedBefore(before)
setHighlightedAfter(after)
} catch (error) {
console.error("Error highlighting code:", error)
setHighlightedBefore(`<pre>${beforeCode}</pre>`)
setHighlightedAfter(`<pre>${afterCode}</pre>`)
}
}
highlightCode()
}, [beforeCode, afterCode, language, selectedTheme])
const renderCode = (code: string, highlighted: string) => { if (highlighted) { return ( <div style={{ "--highlight-color": highlightColor } as React.CSSProperties} className={cn( "bg-background h-full w-full overflow-auto font-mono text-xs", "[&>pre]:h-full [&>pre]:!w-screen [&>pre]:py-2", "[&>pre>code]:!inline-block [&>pre>code]:!w-full", "[&>pre>code>span]:!inline-block [&>pre>code>span]:w-full [&>pre>code>span]:px-4 [&>pre>code>span]:py-0.5", "[&>pre>code>.highlighted]:inline-block [&>pre>code>.highlighted]:w-full [&>pre>code>.highlighted]:!bg-[var(--highlight-color)]", "group-hover/left:[&>pre>code>:not(.focused)]:!opacity-100 group-hover/left:[&>pre>code>:not(.focused)]:!blur-none", "group-hover/right:[&>pre>code>:not(.focused)]:!opacity-100 group-hover/right:[&>pre>code>:not(.focused)]:!blur-none", "[&>pre>code>.add]:bg-[rgba(16,185,129,.16)] [&>pre>code>.remove]:bg-[rgba(244,63,94,.16)]", "group-hover/left:[&>pre>code>:not(.focused)]:transition-all group-hover/left:[&>pre>code>:not(.focused)]:duration-300", "group-hover/right:[&>pre>code>:not(.focused)]:transition-all group-hover/right:[&>pre>code>:not(.focused)]:duration-300" )} dangerouslySetInnerHTML={{ __html: highlighted }} /> ) } else { return ( {code} ) } }
return ( <div className={cn( "leftside group/left border-primary/20 md:border-r", hasLeftFocus && "[&>div>pre>code>:not(.focused)]:!opacity-50 [&>div>pre>code>:not(.focused)]:!blur-[0.095rem]", "[&>div>pre>code>:not(.focused)]:transition-all [&>div>pre>code>:not(.focused)]:duration-300" )} > {filename} before {renderCode(beforeCode, highlightedBefore)} <div className={cn( "rightside group/right border-primary/20 border-t md:border-t-0", hasRightFocus && "[&>div>pre>code>:not(.focused)]:!opacity-50 [&>div>pre>code>:not(.focused)]:!blur-[0.095rem]", "[&>div>pre>code>:not(.focused)]:transition-all [&>div>pre>code>:not(.focused)]:duration-300" )} > {filename} after {renderCode(afterCode, highlightedAfter)} VS ) }
===== EXAMPLE: code-comparison-demo ===== Title: Code Comparison Demo
--- file: example/code-comparison-demo.tsx --- import { CodeComparison } from "@/registry/magicui/code-comparison"
const beforeCode = `import { NextRequest } from 'next/server';
export const middleware = async (req: NextRequest) => { let user = undefined; let team = undefined; const token = req.headers.get('token');
if(req.nextUrl.pathname.startsWith('/auth')) { user = await getUserByToken(token);
if(!user) {
return NextResponse.redirect('/login');
}
}
if(req.nextUrl.pathname.startsWith('/team')) { user = await getUserByToken(token);
if(!user) {
return NextResponse.redirect('/login');
}
const slug = req.nextUrl.query.slug;
team = await getTeamBySlug(slug); // [!code highlight]
if(!team) { // [!code highlight]
return NextResponse.redirect('/'); // [!code highlight]
} // [!code highlight]
} // [!code highlight]
return NextResponse.next(); // [!code highlight] }
export const config = { matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'], // [!code highlight] };`
const afterCode = `import { createMiddleware, type MiddlewareFunctionProps } from '@app/(auth)/auth/_middleware'; import { auth } from '@/app/(auth)/auth/_middleware'; // [!code --] import { auth } from '@/app/(auth)/auth/_middleware'; // [!code ++] import { team } from '@/app/(team)/team/_middleware';
const middlewares = { '/auth{/:path?}': auth, '/team{/:slug?}': [ auth, team ], };
export const middleware = createMiddleware(middlewares); // [!code focus]
export const config = { matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'], };`
export default function CodeComparisonDemo() { return ( ) }
===== COMPONENT: comic-text ===== Title: Comic Text Description: Comic text animation
--- file: magicui/comic-text.tsx --- "use client"
import { CSSProperties } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
type ComicTextProps = { children: string className?: string style?: CSSProperties fontSize?: number }
export function ComicText({ children, className, style, fontSize = 5, }: ComicTextProps) { if (typeof children !== "string") { throw new Error("children must be a string") }
const dotColor = "#EF4444" const backgroundColor = "#FACC15"
return (
<motion.div
className={cn("text-center select-none", className)}
style={{
fontSize: ${fontSize}rem,
fontFamily: "'Bangers', 'Comic Sans MS', 'Impact', sans-serif",
fontWeight: "900",
WebkitTextStroke: ${fontSize * 0.35}px #000000, // Thick black outline
transform: "skewX(-10deg)",
textTransform: "uppercase",
filter: drop-shadow(5px 5px 0px #000000) drop-shadow(3px 3px 0px ${dotColor}) ,
backgroundColor: backgroundColor,
backgroundImage: radial-gradient(circle at 1px 1px, ${dotColor} 1px, transparent 0),
backgroundSize: "8px 8px",
backgroundClip: "text",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
...style,
}}
initial={{ opacity: 0, scale: 0.8, rotate: -2 }}
animate={{ opacity: 1, scale: 1, rotate: 0 }}
transition={{
duration: 0.6,
ease: [0.175, 0.885, 0.32, 1.275],
type: "spring",
}}
>
{children}
</motion.div>
)
}
===== EXAMPLE: comic-text-demo ===== Title: Comic Text Demo
--- file: example/comic-text-demo.tsx --- import { ComicText } from "@/registry/magicui/comic-text"
export default function ComicTextDemo() { return ( BOOM! ) }
===== COMPONENT: confetti ===== Title: Confetti Description: Confetti animations are best used to delight your users when something special happens
--- file: magicui/confetti.tsx --- "use client"
import type { ReactNode } from "react" import React, { createContext, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, } from "react" import type { GlobalOptions as ConfettiGlobalOptions, CreateTypes as ConfettiInstance, Options as ConfettiOptions, } from "canvas-confetti" import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
type Api = { fire: (options?: ConfettiOptions) => void }
type Props = React.ComponentPropsWithRef<"canvas"> & { options?: ConfettiOptions globalOptions?: ConfettiGlobalOptions manualstart?: boolean children?: ReactNode }
export type ConfettiRef = Api | null
const ConfettiContext = createContext({} as Api)
// Define component first const ConfettiComponent = forwardRef<ConfettiRef, Props>((props, ref) => { const { options, globalOptions = { resize: true, useWorker: true }, manualstart = false, children, ...rest } = props const instanceRef = useRef<ConfettiInstance | null>(null)
const canvasRef = useCallback( (node: HTMLCanvasElement) => { if (node !== null) { if (instanceRef.current) return instanceRef.current = confetti.create(node, { ...globalOptions, resize: true, }) } else { if (instanceRef.current) { instanceRef.current.reset() instanceRef.current = null } } }, [globalOptions] )
const fire = useCallback( async (opts = {}) => { try { await instanceRef.current?.({ ...options, ...opts }) } catch (error) { console.error("Confetti error:", error) } }, [options] )
const api = useMemo( () => ({ fire, }), [fire] )
useImperativeHandle(ref, () => api, [api])
useEffect(() => { if (!manualstart) { ;(async () => { try { await fire() } catch (error) { console.error("Confetti effect error:", error) } })() } }, [manualstart, fire])
return ( <ConfettiContext.Provider value={api}> <canvas ref={canvasRef} {...rest} /> {children} </ConfettiContext.Provider> ) })
// Set display name immediately ConfettiComponent.displayName = "Confetti"
// Export as Confetti export const Confetti = ConfettiComponent
interface ConfettiButtonProps extends React.ComponentProps<"button"> { options?: ConfettiOptions & ConfettiGlobalOptions & { canvas?: HTMLCanvasElement } }
const ConfettiButtonComponent = ({ options, children, ...props }: ConfettiButtonProps) => { const handleClick = async (event: React.MouseEvent) => { try { const rect = event.currentTarget.getBoundingClientRect() const x = rect.left + rect.width / 2 const y = rect.top + rect.height / 2 await confetti({ ...options, origin: { x: x / window.innerWidth, y: y / window.innerHeight, }, }) } catch (error) { console.error("Confetti button error:", error) } }
return ( <Button onClick={handleClick} {...props}> {children} ) }
ConfettiButtonComponent.displayName = "ConfettiButton"
export const ConfettiButton = ConfettiButtonComponent
===== EXAMPLE: confetti-demo ===== Title: Confetti Demo
--- file: example/confetti-demo.tsx --- "use client"
import { useRef } from "react"
import { Confetti, type ConfettiRef } from "@/registry/magicui/confetti"
export default function ConfettiDemo() { const confettiRef = useRef(null)
return ( Confetti
<Confetti
ref={confettiRef}
className="absolute top-0 left-0 z-0 size-full"
onMouseEnter={() => {
confettiRef.current?.fire({})
}}
/>
</div>
) }
===== EXAMPLE: confetti-basic-cannon ===== Title: Confetti Basic Cannon
--- file: example/confetti-basic-cannon.tsx --- import { ConfettiButton } from "@/registry/magicui/confetti"
export default function ConfettiButtonDemo() { return ( Confetti 🎉 ) }
===== EXAMPLE: confetti-random-direction ===== Title: Confetti Random Direction
--- file: example/confetti-random-direction.tsx --- import { ConfettiButton } from "@/registry/magicui/confetti"
export default function ConfettiButtonDemo() { return ( <ConfettiButton options={{ get angle() { return Math.random() * 360 }, }} > Random Confetti 🎉 ) }
===== EXAMPLE: confetti-fireworks ===== Title: Confetti Fireworks
--- file: example/confetti-fireworks.tsx --- "use client"
import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
export default function ConfettiFireworks() { const handleClick = () => { const duration = 5 * 1000 const animationEnd = Date.now() + duration const defaults = { startVelocity: 30, spread: 360, ticks: 60, zIndex: 0 }
const randomInRange = (min: number, max: number) =>
Math.random() * (max - min) + min
const interval = window.setInterval(() => {
const timeLeft = animationEnd - Date.now()
if (timeLeft <= 0) {
return clearInterval(interval)
}
const particleCount = 50 * (timeLeft / duration)
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.1, 0.3), y: Math.random() - 0.2 },
})
confetti({
...defaults,
particleCount,
origin: { x: randomInRange(0.7, 0.9), y: Math.random() - 0.2 },
})
}, 250)
}
return ( Trigger Fireworks ) }
===== EXAMPLE: confetti-stars ===== Title: Confetti Stars
--- file: example/confetti-stars.tsx --- "use client"
import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
export default function ConfettiStars() { const handleClick = () => { const defaults = { spread: 360, ticks: 50, gravity: 0, decay: 0.94, startVelocity: 30, colors: ["#FFE400", "#FFBD00", "#E89400", "#FFCA6C", "#FDFFB8"], }
const shoot = () => {
confetti({
...defaults,
particleCount: 40,
scalar: 1.2,
shapes: ["star"],
})
confetti({
...defaults,
particleCount: 10,
scalar: 0.75,
shapes: ["circle"],
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
}
return ( Trigger Stars ) }
===== EXAMPLE: confetti-side-cannons ===== Title: Confetti Side Cannons
--- file: example/confetti-side-cannons.tsx --- "use client"
import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
export default function ConfettiSideCannons() { const handleClick = () => { const end = Date.now() + 3 * 1000 // 3 seconds const colors = ["#a786ff", "#fd8bbc", "#eca184", "#f8deb1"]
const frame = () => {
if (Date.now() > end) return
confetti({
particleCount: 2,
angle: 60,
spread: 55,
startVelocity: 60,
origin: { x: 0, y: 0.5 },
colors: colors,
})
confetti({
particleCount: 2,
angle: 120,
spread: 55,
startVelocity: 60,
origin: { x: 1, y: 0.5 },
colors: colors,
})
requestAnimationFrame(frame)
}
frame()
}
return ( Trigger Side Cannons ) }
===== EXAMPLE: confetti-custom-shapes ===== Title: Confetti Custom Shapes
--- file: example/confetti-custom-shapes.tsx --- "use client"
import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
export default function ConfettiCustomShapes() { const handleClick = () => { const scalar = 2 const triangle = confetti.shapeFromPath({ path: "M0 10 L5 0 L10 10z", }) const square = confetti.shapeFromPath({ path: "M0 0 L10 0 L10 10 L0 10 Z", }) const coin = confetti.shapeFromPath({ path: "M5 0 A5 5 0 1 0 5 10 A5 5 0 1 0 5 0 Z", }) const tree = confetti.shapeFromPath({ path: "M5 0 L10 10 L0 10 Z", })
const defaults = {
spread: 360,
ticks: 60,
gravity: 0,
decay: 0.96,
startVelocity: 20,
shapes: [triangle, square, coin, tree],
scalar,
}
const shoot = () => {
confetti({
...defaults,
particleCount: 30,
})
confetti({
...defaults,
particleCount: 5,
})
confetti({
...defaults,
particleCount: 15,
scalar: scalar / 2,
shapes: ["circle"],
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
}
return ( Trigger Shapes ) }
===== EXAMPLE: confetti-emoji ===== Title: Confetti Emoji
--- file: example/confetti-emoji.tsx --- "use client"
import confetti from "canvas-confetti"
import { Button } from "@/components/ui/button"
export default function ConfettiEmoji() { const handleClick = () => { const scalar = 2 const unicorn = confetti.shapeFromText({ text: "🦄", scalar })
const defaults = {
spread: 360,
ticks: 60,
gravity: 0,
decay: 0.96,
startVelocity: 20,
shapes: [unicorn],
scalar,
}
const shoot = () => {
confetti({
...defaults,
particleCount: 30,
})
confetti({
...defaults,
particleCount: 5,
})
confetti({
...defaults,
particleCount: 15,
scalar: scalar / 2,
shapes: ["circle"],
})
}
setTimeout(shoot, 0)
setTimeout(shoot, 100)
setTimeout(shoot, 200)
}
return ( Trigger Emoji ) }
===== COMPONENT: cool-mode ===== Title: Cool Mode Description: Cool mode effect for buttons, links, and other DOMs
--- file: magicui/cool-mode.tsx --- "use client"
import React, { ReactNode, useEffect, useRef } from "react"
export interface BaseParticle { element: HTMLElement | SVGSVGElement left: number size: number top: number }
export interface BaseParticleOptions { particle?: string size?: number }
export interface CoolParticle extends BaseParticle { direction: number speedHorz: number speedUp: number spinSpeed: number spinVal: number }
export interface CoolParticleOptions extends BaseParticleOptions { particleCount?: number speedHorz?: number speedUp?: number }
const getContainer = () => { const id = "_coolMode_effect" const existingContainer = document.getElementById(id)
if (existingContainer) { return existingContainer }
const container = document.createElement("div") container.setAttribute("id", id) container.setAttribute( "style", "overflow:hidden; position:fixed; height:100%; top:0; left:0; right:0; bottom:0; pointer-events:none; z-index:2147483647" )
document.body.appendChild(container)
return container }
let instanceCounter = 0
const applyParticleEffect = ( element: HTMLElement, options?: CoolParticleOptions ): (() => void) => { instanceCounter++
const defaultParticle = "circle" const particleType = options?.particle || defaultParticle const sizes = [15, 20, 25, 35, 45] const limit = 45
let particles: CoolParticle[] = [] let autoAddParticle = false let mouseX = 0 let mouseY = 0
const container = getContainer()
function generateParticle() { const size = options?.size || sizes[Math.floor(Math.random() * sizes.length)] const speedHorz = options?.speedHorz || Math.random() * 10 const speedUp = options?.speedUp || Math.random() * 25 const spinVal = Math.random() * 360 const spinSpeed = Math.random() * 35 * (Math.random() <= 0.5 ? -1 : 1) const top = mouseY - size / 2 const left = mouseX - size / 2 const direction = Math.random() <= 0.5 ? -1 : 1
const particle = document.createElement("div")
if (particleType === "circle") {
const svgNS = "http://www.w3.org/2000/svg"
const circleSVG = document.createElementNS(svgNS, "svg")
const circle = document.createElementNS(svgNS, "circle")
circle.setAttributeNS(null, "cx", (size / 2).toString())
circle.setAttributeNS(null, "cy", (size / 2).toString())
circle.setAttributeNS(null, "r", (size / 2).toString())
circle.setAttributeNS(
null,
"fill",
`hsl(${Math.random() * 360}, 70%, 50%)`
)
circleSVG.appendChild(circle)
circleSVG.setAttribute("width", size.toString())
circleSVG.setAttribute("height", size.toString())
particle.appendChild(circleSVG)
} else if (
particleType.startsWith("http") ||
particleType.startsWith("/")
) {
// Handle URL-based images
particle.innerHTML = `<img src="${particleType}" width="${size}" height="${size}" style="border-radius: 50%">`
} else {
// Handle emoji or text characters
const fontSizeMultiplier = 3 // Make emojis 3x bigger
const emojiSize = size * fontSizeMultiplier
particle.innerHTML = `<div style="font-size: ${emojiSize}px; line-height: 1; text-align: center; width: ${size}px; height: ${size}px; display: flex; align-items: center; justify-content: center; transform: scale(${fontSizeMultiplier}); transform-origin: center;">${particleType}</div>`
}
particle.style.position = "absolute"
particle.style.transform = `translate3d(${left}px, ${top}px, 0px) rotate(${spinVal}deg)`
container.appendChild(particle)
particles.push({
direction,
element: particle,
left,
size,
speedHorz,
speedUp,
spinSpeed,
spinVal,
top,
})
}
function refreshParticles() { particles.forEach((p) => { p.left = p.left - p.speedHorz * p.direction p.top = p.top - p.speedUp p.speedUp = Math.min(p.size, p.speedUp - 1) p.spinVal = p.spinVal + p.spinSpeed
if (
p.top >=
Math.max(window.innerHeight, document.body.clientHeight) + p.size
) {
particles = particles.filter((o) => o !== p)
p.element.remove()
}
p.element.setAttribute(
"style",
[
"position:absolute",
"will-change:transform",
`top:${p.top}px`,
`left:${p.left}px`,
`transform:rotate(${p.spinVal}deg)`,
].join(";")
)
})
}
let animationFrame: number | undefined
let lastParticleTimestamp = 0 const particleGenerationDelay = 30
function loop() { const currentTime = performance.now() if ( autoAddParticle && particles.length < limit && currentTime - lastParticleTimestamp > particleGenerationDelay ) { generateParticle() lastParticleTimestamp = currentTime }
refreshParticles()
animationFrame = requestAnimationFrame(loop)
}
loop()
const isTouchInteraction = "ontouchstart" in window
const tap = isTouchInteraction ? "touchstart" : "mousedown" const tapEnd = isTouchInteraction ? "touchend" : "mouseup" const move = isTouchInteraction ? "touchmove" : "mousemove"
const updateMousePosition = (e: MouseEvent | TouchEvent) => { if ("touches" in e) { mouseX = e.touches?.[0].clientX mouseY = e.touches?.[0].clientY } else { mouseX = e.clientX mouseY = e.clientY } }
const tapHandler = (e: MouseEvent | TouchEvent) => { updateMousePosition(e) autoAddParticle = true }
const disableAutoAddParticle = () => { autoAddParticle = false }
element.addEventListener(move, updateMousePosition, { passive: true }) element.addEventListener(tap, tapHandler, { passive: true }) element.addEventListener(tapEnd, disableAutoAddParticle, { passive: true }) element.addEventListener("mouseleave", disableAutoAddParticle, { passive: true, })
return () => { element.removeEventListener(move, updateMousePosition) element.removeEventListener(tap, tapHandler) element.removeEventListener(tapEnd, disableAutoAddParticle) element.removeEventListener("mouseleave", disableAutoAddParticle)
const interval = setInterval(() => {
if (animationFrame && particles.length === 0) {
cancelAnimationFrame(animationFrame)
clearInterval(interval)
if (--instanceCounter === 0) {
container.remove()
}
}
}, 500)
} }
interface CoolModeProps { children: ReactNode options?: CoolParticleOptions }
export const CoolMode: React.FC = ({ children, options }) => { const ref = useRef(null)
useEffect(() => { if (ref.current) { return applyParticleEffect(ref.current, options) } }, [options])
return {children} }
===== EXAMPLE: cool-mode-demo ===== Title: Cool Mode Demo
--- file: example/cool-mode-demo.tsx --- import { Button } from "@/components/ui/button" import { CoolMode } from "@/registry/magicui/cool-mode"
export default function CoolModeDemo() { return ( Click Me! ) }
===== EXAMPLE: cool-mode-custom ===== Title: Cool Mode Custom
--- file: example/cool-mode-custom.tsx --- import { Button } from "@/components/ui/button" import { CoolMode } from "@/registry/magicui/cool-mode"
export default function CoolModeCustom() { return ( <CoolMode options={{ particle: "https://pbs.twimg.com/profile_images/1782811051504885763/YR5-kWOI_400x400.jpg", }} > Click Me! ) }
===== COMPONENT: dock ===== Title: Dock Description: An implementation of the MacOS dock using react + tailwindcss + motion
--- file: magicui/dock.tsx --- "use client"
import React, { PropsWithChildren, useRef } from "react" import { cva, type VariantProps } from "class-variance-authority" import { motion, MotionValue, useMotionValue, useSpring, useTransform, } from "motion/react" import type { MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
export interface DockProps extends VariantProps { className?: string iconSize?: number iconMagnification?: number disableMagnification?: boolean iconDistance?: number direction?: "top" | "middle" | "bottom" children: React.ReactNode }
const DEFAULT_SIZE = 40 const DEFAULT_MAGNIFICATION = 60 const DEFAULT_DISTANCE = 140 const DEFAULT_DISABLEMAGNIFICATION = false
const dockVariants = cva( "supports-backdrop-blur:bg-white/10 supports-backdrop-blur:dark:bg-black/10 mx-auto mt-8 flex h-[58px] w-max items-center justify-center gap-2 rounded-2xl border p-2 backdrop-blur-md" )
const Dock = React.forwardRef<HTMLDivElement, DockProps>( ( { className, children, iconSize = DEFAULT_SIZE, iconMagnification = DEFAULT_MAGNIFICATION, disableMagnification = DEFAULT_DISABLEMAGNIFICATION, iconDistance = DEFAULT_DISTANCE, direction = "middle", ...props }, ref ) => { const mouseX = useMotionValue(Infinity)
const renderChildren = () => {
return React.Children.map(children, (child) => {
if (
React.isValidElement<DockIconProps>(child) &&
child.type === DockIcon
) {
return React.cloneElement(child, {
...child.props,
mouseX: mouseX,
size: iconSize,
magnification: iconMagnification,
disableMagnification: disableMagnification,
distance: iconDistance,
})
}
return child
})
}
return (
<motion.div
ref={ref}
onMouseMove={(e) => mouseX.set(e.pageX)}
onMouseLeave={() => mouseX.set(Infinity)}
{...props}
className={cn(dockVariants({ className }), {
"items-start": direction === "top",
"items-center": direction === "middle",
"items-end": direction === "bottom",
})}
>
{renderChildren()}
</motion.div>
)
} )
Dock.displayName = "Dock"
export interface DockIconProps extends Omit< MotionProps & React.HTMLAttributes, "children"
{ size?: number magnification?: number disableMagnification?: boolean distance?: number mouseX?: MotionValue className?: string children?: React.ReactNode props?: PropsWithChildren }
const DockIcon = ({ size = DEFAULT_SIZE, magnification = DEFAULT_MAGNIFICATION, disableMagnification, distance = DEFAULT_DISTANCE, mouseX, className, children, ...props }: DockIconProps) => { const ref = useRef(null) const padding = Math.max(6, size * 0.2) const defaultMouseX = useMotionValue(Infinity)
const distanceCalc = useTransform(mouseX ?? defaultMouseX, (val: number) => { const bounds = ref.current?.getBoundingClientRect() ?? { x: 0, width: 0 } return val - bounds.x - bounds.width / 2 })
const targetSize = disableMagnification ? size : magnification
const sizeTransform = useTransform( distanceCalc, [-distance, 0, distance], [size, targetSize, size] )
const scaleSize = useSpring(sizeTransform, { mass: 0.1, stiffness: 150, damping: 12, })
return ( <motion.div ref={ref} style={{ width: scaleSize, height: scaleSize, padding }} className={cn( "flex aspect-square cursor-pointer items-center justify-center rounded-full", disableMagnification && "hover:bg-muted-foreground transition-colors", className )} {...props} > {children} </motion.div> ) }
DockIcon.displayName = "DockIcon"
export { Dock, DockIcon, dockVariants }
===== EXAMPLE: dock-demo ===== Title: Dock Demo
--- file: example/dock-demo.tsx --- "use client"
import React from "react" import Link from "next/link" import { CalendarIcon, HomeIcon, MailIcon, PencilIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" import { Separator } from "@/components/ui/separator" import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from "@/components/ui/tooltip" import { Dock, DockIcon } from "@/registry/magicui/dock"
export type IconProps = React.HTMLAttributes
const Icons = { calendar: (props: IconProps) => <CalendarIcon {...props} />, email: (props: IconProps) => <MailIcon {...props} />, linkedin: (props: IconProps) => ( <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}> LinkedIn ), x: (props: IconProps) => ( <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" {...props}> X ), youtube: (props: IconProps) => ( <svg width="32px" height="32px" viewBox="0 0 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props} > youtube ), github: (props: IconProps) => ( <svg viewBox="0 0 438.549 438.549" {...props}> ), }
const DATA = { navbar: [ { href: "#", icon: HomeIcon, label: "Home" }, { href: "#", icon: PencilIcon, label: "Blog" }, ], contact: { social: { GitHub: { name: "GitHub", url: "#", icon: Icons.github, }, LinkedIn: { name: "LinkedIn", url: "#", icon: Icons.linkedin, }, X: { name: "X", url: "#", icon: Icons.x, }, email: { name: "Send Email", url: "#", icon: Icons.email, }, }, }, }
export default function DockDemo() { return ( Dock {DATA.navbar.map((item) => ( <Link href={item.href} aria-label={item.label} className={cn( buttonVariants({ variant: "ghost", size: "icon" }), "size-12 rounded-full" )} > <item.icon className="size-4" /> {item.label} ))} {Object.entries(DATA.contact.social).map(([name, social]) => ( <Link href={social.url} aria-label={social.name} className={cn( buttonVariants({ variant: "ghost", size: "icon" }), "size-12 rounded-full" )} > <social.icon className="size-4" /> {name} ))} ) }
===== EXAMPLE: dock-demo-2 ===== Title: Dock Demo 2
--- file: example/dock-demo-2.tsx --- "use client"
import React from "react"
import { Dock, DockIcon } from "@/registry/magicui/dock"
export type IconProps = React.HTMLAttributes
export default function DockDemo() { return ( <Icons.gitHub className="size-6" /> <Icons.googleDrive className="size-6" /> <Icons.notion className="size-6" /> <Icons.whatsapp className="size-6" /> ) }
const Icons = { gitHub: (props: IconProps) => ( <svg viewBox="0 0 438.549 438.549" {...props}> ), notion: (props: IconProps) => ( <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} > ), googleDrive: (props: IconProps) => ( <svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg" {...props}> ), whatsapp: (props: IconProps) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.216 175.552" {...props} > ), }
===== EXAMPLE: dock-demo-3 ===== Title: Dock Demo 3
--- file: example/dock-demo-3.tsx --- "use client"
import React from "react"
import { Dock, DockIcon } from "@/registry/magicui/dock"
export type IconProps = React.HTMLAttributes
export default function DockDemo() { return ( <Icons.gitHub className="size-full" /> <Icons.googleDrive className="size-full" /> <Icons.notion className="size-full" /> <Icons.whatsapp className="size-full" /> ) }
const Icons = { gitHub: (props: IconProps) => ( <svg viewBox="0 0 438.549 438.549" {...props}> ), notion: (props: IconProps) => ( <svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg" {...props} > ), googleDrive: (props: IconProps) => ( <svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg" {...props}> ), whatsapp: (props: IconProps) => ( <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 175.216 175.552" {...props} > ), }
===== COMPONENT: dot-pattern ===== Title: Dot Pattern Description: A background dot pattern made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/dot-pattern.tsx --- "use client"
import React, { useEffect, useId, useRef, useState } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
/**
- DotPattern Component Props
- @param {number} [width=16] - The horizontal spacing between dots
- @param {number} [height=16] - The vertical spacing between dots
- @param {number} [x=0] - The x-offset of the entire pattern
- @param {number} [y=0] - The y-offset of the entire pattern
- @param {number} [cx=1] - The x-offset of individual dots
- @param {number} [cy=1] - The y-offset of individual dots
- @param {number} [cr=1] - The radius of each dot
- @param {string} [className] - Additional CSS classes to apply to the SVG container
- @param {boolean} [glow=false] - Whether dots should have a glowing animation effect */ interface DotPatternProps extends React.SVGProps { width?: number height?: number x?: number y?: number cx?: number cy?: number cr?: number className?: string glow?: boolean [key: string]: unknown }
/**
- DotPattern Component
- A React component that creates an animated or static dot pattern background using SVG.
- The pattern automatically adjusts to fill its container and can optionally display glowing dots.
- @component
- @see DotPatternProps for the props interface.
- @example
- // Basic usage
- // With glowing effect and custom spacing
- <DotPattern
- width={20}
- height={20}
- glow={true}
- className="opacity-50"
- />
- @notes
-
- The component is client-side only ("use client")
-
- Automatically responds to container size changes
-
- When glow is enabled, dots will animate with random delays and durations
-
- Uses Motion for animations
-
- Dots color can be controlled via the text color utility classes */
export function DotPattern({ width = 16, height = 16, x = 0, y = 0, cx = 1, cy = 1, cr = 1, className, glow = false, ...props }: DotPatternProps) { const id = useId() const containerRef = useRef(null) const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => { const updateDimensions = () => { if (containerRef.current) { const { width, height } = containerRef.current.getBoundingClientRect() setDimensions({ width, height }) } }
updateDimensions()
window.addEventListener("resize", updateDimensions)
return () => window.removeEventListener("resize", updateDimensions)
}, [])
const dots = Array.from( { length: Math.ceil(dimensions.width / width) * Math.ceil(dimensions.height / height), }, (_, i) => { const col = i % Math.ceil(dimensions.width / width) const row = Math.floor(i / Math.ceil(dimensions.width / width)) return { x: col * width + cx, y: row * height + cy, delay: Math.random() * 5, duration: Math.random() * 3 + 2, } } )
return (
<svg
ref={containerRef}
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full text-neutral-400/80",
className
)}
{...props}
>
<radialGradient id={${id}-gradient}>
{dots.map((dot, index) => (
<motion.circle
key={${dot.x}-${dot.y}}
cx={dot.x}
cy={dot.y}
r={cr}
fill={glow ? url(#${id}-gradient) : "currentColor"}
initial={glow ? { opacity: 0.4, scale: 1 } : {}}
animate={
glow
? {
opacity: [0.4, 1, 0.4],
scale: [1, 1.5, 1],
}
: {}
}
transition={
glow
? {
duration: dot.duration,
repeat: Infinity,
repeatType: "reverse",
delay: dot.delay,
ease: "easeInOut",
}
: {}
}
/>
))}
)
}
===== EXAMPLE: dot-pattern-demo ===== Title: Dot Pattern Demo
--- file: example/dot-pattern-demo.tsx --- "use client"
import { cn } from "@/lib/utils" import { DotPattern } from "@/registry/magicui/dot-pattern"
export default function DotPatternDemo() { return ( <DotPattern className={cn( "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]" )} /> ) }
===== EXAMPLE: dot-pattern-linear-gradient ===== Title: Dot Pattern Linear Gradient
--- file: example/dot-pattern-linear-gradient.tsx --- "use client"
import { cn } from "@/lib/utils" import { DotPattern } from "@/registry/magicui/dot-pattern"
export default function DotPatternLinearGradient() { return ( <DotPattern width={20} height={20} cx={1} cy={1} cr={1} className={cn( "[mask-image:linear-gradient(to_bottom_right,white,transparent,transparent)]" )} /> ) }
===== EXAMPLE: dot-pattern-with-glow-effect ===== Title: Dot Pattern with glow effect
--- file: example/dot-pattern-with-glow-effect.tsx --- "use client"
import { cn } from "@/lib/utils" import { DotPattern } from "@/registry/magicui/dot-pattern"
export default function DotPatternWithGlowEffectDemo() { return ( <DotPattern glow={true} className={cn( "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]" )} /> ) }
===== COMPONENT: dotted-map ===== Title: Dotted Map Description: A component with a dotted map.
--- file: magicui/dotted-map.tsx --- import * as React from "react" import { createMap } from "svg-dotted-map"
import { cn } from "@/lib/utils"
interface Marker { lat: number lng: number size?: number }
export interface DottedMapProps extends React.SVGProps { width?: number height?: number mapSamples?: number markers?: Marker[] dotColor?: string markerColor?: string dotRadius?: number stagger?: boolean }
export function DottedMap({ width = 150, height = 75, mapSamples = 5000, markers = [], markerColor = "#FF6900", dotRadius = 0.2, stagger = true, className, style, }: DottedMapProps) { const { points, addMarkers } = createMap({ width, height, mapSamples, })
const processedMarkers = addMarkers(markers)
// Compute stagger helpers in a single, simple pass const { xStep, yToRowIndex } = React.useMemo(() => { const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x) const rowMap = new Map<number, number>() let step = 0 let prevY = Number.NaN let prevXInRow = Number.NaN
for (const p of sorted) {
if (p.y !== prevY) {
// new row
prevY = p.y
prevXInRow = Number.NaN
if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size)
}
if (!Number.isNaN(prevXInRow)) {
const delta = p.x - prevXInRow
if (delta > 0) step = step === 0 ? delta : Math.min(step, delta)
}
prevXInRow = p.x
}
return { xStep: step || 1, yToRowIndex: rowMap }
}, [points])
return (
<svg
viewBox={0 0 ${width} ${height}}
className={cn("text-gray-500 dark:text-gray-500", className)}
style={{ width: "100%", height: "100%", ...style }}
>
{points.map((point, index) => {
const rowIndex = yToRowIndex.get(point.y) ?? 0
const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0
return (
<circle
cx={point.x + offsetX}
cy={point.y}
r={dotRadius}
fill="currentColor"
key={${point.x}-${point.y}-${index}}
/>
)
})}
{processedMarkers.map((marker, index) => {
const rowIndex = yToRowIndex.get(marker.y) ?? 0
const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0
return (
<circle
cx={marker.x + offsetX}
cy={marker.y}
r={marker.size ?? dotRadius}
fill={markerColor}
key={${marker.x}-${marker.y}-${index}}
/>
)
})}
)
}
===== EXAMPLE: dotted-map-demo ===== Title: Dotted Map Demo
--- file: example/dotted-map-demo.tsx --- import { DottedMap } from "@/registry/magicui/dotted-map"
const markers = [ { lat: 40.7128, lng: -74.006, size: 0.3, }, // New York { lat: 34.0522, lng: -118.2437, size: 0.3, }, // Los Angeles { lat: 51.5074, lng: -0.1278, size: 0.3, }, // London { lat: -33.8688, lng: 151.2093, size: 0.3, }, // Sydney { lat: 48.8566, lng: 2.3522, size: 0.3, }, // Paris { lat: 35.6762, lng: 139.6503, size: 0.3, }, // Tokyo { lat: 55.7558, lng: 37.6176, size: 0.3, }, // Moscow { lat: 39.9042, lng: 116.4074, size: 0.3, }, // Beijing { lat: 28.6139, lng: 77.209, size: 0.3, }, // New Delhi { lat: -23.5505, lng: -46.6333, size: 0.3, }, // São Paulo { lat: 1.3521, lng: 103.8198, size: 0.3, }, // Singapore { lat: 25.2048, lng: 55.2708, size: 0.3, }, // Dubai { lat: 52.52, lng: 13.405, size: 0.3, }, // Berlin { lat: 19.4326, lng: -99.1332, size: 0.3, }, // Mexico City { lat: -26.2041, lng: 28.0473, size: 0.3, }, // Johannesburg ]
export default function Component() { return ( ) }
===== EXAMPLE: dotted-map-demo-2 ===== Title: Dotted Map Demo 2
--- file: example/dotted-map-demo-2.tsx --- import { DottedMap } from "@/registry/magicui/dotted-map"
export default function Component() { return ( ) }
===== COMPONENT: file-tree ===== Title: File Tree Description: A component used to showcase the folder and file structure of a directory.
--- file: magicui/file-tree.tsx --- "use client"
import React, { createContext, forwardRef, useCallback, useContext, useEffect, useState, } from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion" import { FileIcon, FolderIcon, FolderOpenIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { Button } from "@/components/ui/button" import { ScrollArea } from "@/components/ui/scroll-area"
type TreeViewElement = { id: string name: string isSelectable?: boolean children?: TreeViewElement[] }
type TreeContextProps = { selectedId: string | undefined expandedItems: string[] | undefined indicator: boolean handleExpand: (id: string) => void selectItem: (id: string) => void setExpandedItems?: React.Dispatch<React.SetStateAction<string[] | undefined>> openIcon?: React.ReactNode closeIcon?: React.ReactNode direction: "rtl" | "ltr" }
const TreeContext = createContext<TreeContextProps | null>(null)
const useTree = () => { const context = useContext(TreeContext) if (!context) { throw new Error("useTree must be used within a TreeProvider") } return context }
type Direction = "rtl" | "ltr" | undefined
type TreeViewProps = { initialSelectedId?: string indicator?: boolean elements?: TreeViewElement[] initialExpandedItems?: string[] openIcon?: React.ReactNode closeIcon?: React.ReactNode } & React.HTMLAttributes
const Tree = forwardRef<HTMLDivElement, TreeViewProps>( ( { className, elements, initialSelectedId, initialExpandedItems, children, indicator = true, openIcon, closeIcon, dir, ...props }, ref ) => { const [selectedId, setSelectedId] = useState<string | undefined>( initialSelectedId ) const [expandedItems, setExpandedItems] = useState<string[] | undefined>( initialExpandedItems )
const selectItem = useCallback((id: string) => {
setSelectedId(id)
}, [])
const handleExpand = useCallback((id: string) => {
setExpandedItems((prev) => {
if (prev?.includes(id)) {
return prev.filter((item) => item !== id)
}
return [...(prev ?? []), id]
})
}, [])
const expandSpecificTargetedElements = useCallback(
(elements?: TreeViewElement[], selectId?: string) => {
if (!elements || !selectId) return
const findParent = (
currentElement: TreeViewElement,
currentPath: string[] = []
) => {
const isSelectable = currentElement.isSelectable ?? true
const newPath = [...currentPath, currentElement.id]
if (currentElement.id === selectId) {
if (isSelectable) {
setExpandedItems((prev) => [...(prev ?? []), ...newPath])
} else {
if (newPath.includes(currentElement.id)) {
newPath.pop()
setExpandedItems((prev) => [...(prev ?? []), ...newPath])
}
}
return
}
if (
isSelectable &&
currentElement.children &&
currentElement.children.length > 0
) {
currentElement.children.forEach((child) => {
findParent(child, newPath)
})
}
}
elements.forEach((element) => {
findParent(element)
})
},
[]
)
useEffect(() => {
if (initialSelectedId) {
expandSpecificTargetedElements(elements, initialSelectedId)
}
}, [initialSelectedId, elements])
const direction = dir === "rtl" ? "rtl" : "ltr"
return (
<TreeContext.Provider
value={{
selectedId,
expandedItems,
handleExpand,
selectItem,
setExpandedItems,
indicator,
openIcon,
closeIcon,
direction,
}}
>
<div className={cn("size-full", className)}>
<ScrollArea
ref={ref}
className="relative h-full px-2"
dir={dir as Direction}
>
<AccordionPrimitive.Root
{...props}
type="multiple"
defaultValue={expandedItems}
value={expandedItems}
className="flex flex-col gap-1"
onValueChange={(value) =>
setExpandedItems((prev) => [...(prev ?? []), value[0]])
}
dir={dir as Direction}
>
{children}
</AccordionPrimitive.Root>
</ScrollArea>
</div>
</TreeContext.Provider>
)
} )
Tree.displayName = "Tree"
const TreeIndicator = forwardRef< HTMLDivElement, React.HTMLAttributes
(({ className, ...props }, ref) => { const { direction } = useTree()
return ( <div dir={direction} ref={ref} className={cn( "bg-muted absolute left-1.5 h-full w-px rounded-md py-3 duration-300 ease-in-out hover:bg-slate-300 rtl:right-1.5", className )} {...props} /> ) })
TreeIndicator.displayName = "TreeIndicator"
type FolderProps = { expandedItems?: string[] element: string isSelectable?: boolean isSelect?: boolean } & React.ComponentPropsWithoutRef
const Folder = forwardRef< HTMLDivElement, FolderProps & React.HTMLAttributes
( ( { className, element, value, isSelectable = true, isSelect, children, ...props }, ref ) => { const { direction, handleExpand, expandedItems, indicator, setExpandedItems, openIcon, closeIcon, } = useTree()
return (
<AccordionPrimitive.Item
{...props}
value={value}
className="relative h-full overflow-hidden"
>
<AccordionPrimitive.Trigger
className={cn(
`flex items-center gap-1 rounded-md text-sm`,
className,
{
"bg-muted rounded-md": isSelect && isSelectable,
"cursor-pointer": isSelectable,
"cursor-not-allowed opacity-50": !isSelectable,
}
)}
disabled={!isSelectable}
onClick={() => handleExpand(value)}
>
{expandedItems?.includes(value)
? (openIcon ?? <FolderOpenIcon className="size-4" />)
: (closeIcon ?? <FolderIcon className="size-4" />)}
<span>{element}</span>
</AccordionPrimitive.Trigger>
<AccordionPrimitive.Content className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down relative h-full overflow-hidden text-sm">
{element && indicator && <TreeIndicator aria-hidden="true" />}
<AccordionPrimitive.Root
dir={direction}
type="multiple"
className="ml-5 flex flex-col gap-1 py-1 rtl:mr-5"
defaultValue={expandedItems}
value={expandedItems}
onValueChange={(value) => {
setExpandedItems?.((prev) => [...(prev ?? []), value[0]])
}}
>
{children}
</AccordionPrimitive.Root>
</AccordionPrimitive.Content>
</AccordionPrimitive.Item>
)
} )
Folder.displayName = "Folder"
const File = forwardRef< HTMLButtonElement, { value: string handleSelect?: (id: string) => void isSelectable?: boolean isSelect?: boolean fileIcon?: React.ReactNode } & React.ButtonHTMLAttributes
( ( { value, className, handleSelect, isSelectable = true, isSelect, fileIcon, children, ...props }, ref ) => { const { direction, selectedId, selectItem } = useTree() const isSelected = isSelect ?? selectedId === value return ( <button ref={ref} type="button" disabled={!isSelectable} className={cn( "flex w-fit items-center gap-1 rounded-md pr-1 text-sm duration-200 ease-in-out rtl:pr-0 rtl:pl-1", { "bg-muted": isSelected && isSelectable, }, isSelectable ? "cursor-pointer" : "cursor-not-allowed opacity-50", direction === "rtl" ? "rtl" : "ltr", className )} onClick={() => selectItem(value)} {...props} > {fileIcon ?? } {children} ) } )
File.displayName = "File"
const CollapseButton = forwardRef< HTMLButtonElement, { elements: TreeViewElement[] expandAll?: boolean } & React.HTMLAttributes
(({ className, elements, expandAll = false, children, ...props }, ref) => { const { expandedItems, setExpandedItems } = useTree()
const expendAllTree = useCallback((elements: TreeViewElement[]) => { const expandTree = (element: TreeViewElement) => { const isSelectable = element.isSelectable ?? true if (isSelectable && element.children && element.children.length > 0) { setExpandedItems?.((prev) => [...(prev ?? []), element.id]) element.children.forEach(expandTree) } }
elements.forEach(expandTree)
}, [])
const closeAll = useCallback(() => { setExpandedItems?.([]) }, [])
useEffect(() => { console.log(expandAll) if (expandAll) { expendAllTree(elements) } }, [expandAll])
return ( <Button variant={"ghost"} className="absolute right-2 bottom-1 h-8 w-fit p-1" onClick={ expandedItems && expandedItems.length > 0 ? closeAll : () => expendAllTree(elements) } ref={ref} {...props} > {children} Toggle ) })
CollapseButton.displayName = "CollapseButton"
export { CollapseButton, File, Folder, Tree, type TreeViewElement }
===== EXAMPLE: file-tree-demo ===== Title: File Tree Demo
--- file: example/file-tree-demo.tsx --- import { File, Folder, Tree } from "@/registry/magicui/file-tree"
export default function FileTreeDemo() { return ( <Tree className="bg-background overflow-hidden rounded-md p-2" initialSelectedId="7" initialExpandedItems={[ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", ]} elements={ELEMENTS} > layout.tsx page.tsx button.tsx header.tsx footer.tsx utils.ts ) }
const ELEMENTS = [ { id: "1", isSelectable: true, name: "src", children: [ { id: "2", isSelectable: true, name: "app", children: [ { id: "3", isSelectable: true, name: "layout.tsx", }, { id: "4", isSelectable: true, name: "page.tsx", }, ], }, { id: "5", isSelectable: true, name: "components", children: [ { id: "6", isSelectable: true, name: "header.tsx", }, { id: "7", isSelectable: true, name: "footer.tsx", }, ], }, { id: "8", isSelectable: true, name: "lib", children: [ { id: "9", isSelectable: true, name: "utils.ts", }, ], }, ], }, ]
===== COMPONENT: flickering-grid ===== Title: Flickering Grid Description: A flickering grid background made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/flickering-grid.tsx --- "use client"
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import { cn } from "@/lib/utils"
interface FlickeringGridProps extends React.HTMLAttributes { squareSize?: number gridGap?: number flickerChance?: number color?: string width?: number height?: number className?: string maxOpacity?: number }
export const FlickeringGrid: React.FC = ({ squareSize = 4, gridGap = 6, flickerChance = 0.3, color = "rgb(0, 0, 0)", width, height, className, maxOpacity = 0.3, ...props }) => { const canvasRef = useRef(null) const containerRef = useRef(null) const [isInView, setIsInView] = useState(false) const [canvasSize, setCanvasSize] = useState({ width: 0, height: 0 })
const memoizedColor = useMemo(() => {
const toRGBA = (color: string) => {
if (typeof window === "undefined") {
return rgba(0, 0, 0,
}
const canvas = document.createElement("canvas")
canvas.width = canvas.height = 1
const ctx = canvas.getContext("2d")
if (!ctx) return "rgba(255, 0, 0,"
ctx.fillStyle = color
ctx.fillRect(0, 0, 1, 1)
const [r, g, b] = Array.from(ctx.getImageData(0, 0, 1, 1).data)
return rgba(${r}, ${g}, ${b},
}
return toRGBA(color)
}, [color])
const setupCanvas = useCallback(
(canvas: HTMLCanvasElement, width: number, height: number) => {
const dpr = window.devicePixelRatio || 1
canvas.width = width * dpr
canvas.height = height * dpr
canvas.style.width = ${width}px
canvas.style.height = ${height}px
const cols = Math.floor(width / (squareSize + gridGap))
const rows = Math.floor(height / (squareSize + gridGap))
const squares = new Float32Array(cols * rows)
for (let i = 0; i < squares.length; i++) {
squares[i] = Math.random() * maxOpacity
}
return { cols, rows, squares, dpr }
},
[squareSize, gridGap, maxOpacity]
)
const updateSquares = useCallback( (squares: Float32Array, deltaTime: number) => { for (let i = 0; i < squares.length; i++) { if (Math.random() < flickerChance * deltaTime) { squares[i] = Math.random() * maxOpacity } } }, [flickerChance, maxOpacity] )
const drawGrid = useCallback( ( ctx: CanvasRenderingContext2D, width: number, height: number, cols: number, rows: number, squares: Float32Array, dpr: number ) => { ctx.clearRect(0, 0, width, height) ctx.fillStyle = "transparent" ctx.fillRect(0, 0, width, height)
for (let i = 0; i < cols; i++) {
for (let j = 0; j < rows; j++) {
const opacity = squares[i * rows + j]
ctx.fillStyle = `${memoizedColor}${opacity})`
ctx.fillRect(
i * (squareSize + gridGap) * dpr,
j * (squareSize + gridGap) * dpr,
squareSize * dpr,
squareSize * dpr
)
}
}
},
[memoizedColor, squareSize, gridGap]
)
useEffect(() => { const canvas = canvasRef.current const container = containerRef.current if (!canvas || !container) return
const ctx = canvas.getContext("2d")
if (!ctx) return
let animationFrameId: number
let gridParams: ReturnType<typeof setupCanvas>
const updateCanvasSize = () => {
const newWidth = width || container.clientWidth
const newHeight = height || container.clientHeight
setCanvasSize({ width: newWidth, height: newHeight })
gridParams = setupCanvas(canvas, newWidth, newHeight)
}
updateCanvasSize()
let lastTime = 0
const animate = (time: number) => {
if (!isInView) return
const deltaTime = (time - lastTime) / 1000
lastTime = time
updateSquares(gridParams.squares, deltaTime)
drawGrid(
ctx,
canvas.width,
canvas.height,
gridParams.cols,
gridParams.rows,
gridParams.squares,
gridParams.dpr
)
animationFrameId = requestAnimationFrame(animate)
}
const resizeObserver = new ResizeObserver(() => {
updateCanvasSize()
})
resizeObserver.observe(container)
const intersectionObserver = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting)
},
{ threshold: 0 }
)
intersectionObserver.observe(canvas)
if (isInView) {
animationFrameId = requestAnimationFrame(animate)
}
return () => {
cancelAnimationFrame(animationFrameId)
resizeObserver.disconnect()
intersectionObserver.disconnect()
}
}, [setupCanvas, updateSquares, drawGrid, width, height, isInView])
return (
<div
ref={containerRef}
className={cn(h-full w-full ${className})}
{...props}
>
<canvas
ref={canvasRef}
className="pointer-events-none"
style={{
width: canvasSize.width,
height: canvasSize.height,
}}
/>
)
}
===== EXAMPLE: flickering-grid-demo ===== Title: Flickering Grid Demo
--- file: example/flickering-grid-demo.tsx --- import { FlickeringGrid } from "@/registry/magicui/flickering-grid"
export default function FlickeringGridDemo() { return ( ) }
===== EXAMPLE: flickering-grid-rounded-demo ===== Title: Flickering Grid Rounded Demo
--- file: example/flickering-grid-rounded-demo.tsx --- import { FlickeringGrid } from "@/registry/magicui/flickering-grid"
export default function FlickeringGridRoundedDemo() { return ( ) }
===== COMPONENT: globe ===== Title: Globe Description: An autorotating, interactive, and highly performant globe made using WebGL.
--- file: magicui/globe.tsx --- "use client"
import { useEffect, useRef } from "react" import createGlobe, { COBEOptions } from "cobe" import { useMotionValue, useSpring } from "motion/react"
import { cn } from "@/lib/utils"
const MOVEMENT_DAMPING = 1400
const GLOBE_CONFIG: COBEOptions = { width: 800, height: 800, onRender: () => {}, devicePixelRatio: 2, phi: 0, theta: 0.3, dark: 0, diffuse: 0.4, mapSamples: 16000, mapBrightness: 1.2, baseColor: [1, 1, 1], markerColor: [251 / 255, 100 / 255, 21 / 255], glowColor: [1, 1, 1], markers: [ { location: [14.5995, 120.9842], size: 0.03 }, { location: [19.076, 72.8777], size: 0.1 }, { location: [23.8103, 90.4125], size: 0.05 }, { location: [30.0444, 31.2357], size: 0.07 }, { location: [39.9042, 116.4074], size: 0.08 }, { location: [-23.5505, -46.6333], size: 0.1 }, { location: [19.4326, -99.1332], size: 0.1 }, { location: [40.7128, -74.006], size: 0.1 }, { location: [34.6937, 135.5022], size: 0.05 }, { location: [41.0082, 28.9784], size: 0.06 }, ], }
export function Globe({ className, config = GLOBE_CONFIG, }: { className?: string config?: COBEOptions }) { let phi = 0 let width = 0 const canvasRef = useRef(null) const pointerInteracting = useRef<number | null>(null) const pointerInteractionMovement = useRef(0)
const r = useMotionValue(0) const rs = useSpring(r, { mass: 1, damping: 30, stiffness: 100, })
const updatePointerInteraction = (value: number | null) => { pointerInteracting.current = value if (canvasRef.current) { canvasRef.current.style.cursor = value !== null ? "grabbing" : "grab" } }
const updateMovement = (clientX: number) => { if (pointerInteracting.current !== null) { const delta = clientX - pointerInteracting.current pointerInteractionMovement.current = delta r.set(r.get() + delta / MOVEMENT_DAMPING) } }
useEffect(() => { const onResize = () => { if (canvasRef.current) { width = canvasRef.current.offsetWidth } }
window.addEventListener("resize", onResize)
onResize()
const globe = createGlobe(canvasRef.current!, {
...config,
width: width * 2,
height: width * 2,
onRender: (state) => {
if (!pointerInteracting.current) phi += 0.005
state.phi = phi + rs.get()
state.width = width * 2
state.height = width * 2
},
})
setTimeout(() => (canvasRef.current!.style.opacity = "1"), 0)
return () => {
globe.destroy()
window.removeEventListener("resize", onResize)
}
}, [rs, config])
return ( <div className={cn( "absolute inset-0 mx-auto aspect-[1/1] w-full max-w-[600px]", className )} > <canvas className={cn( "size-full opacity-0 transition-opacity duration-500 [contain:layout_paint_size]" )} ref={canvasRef} onPointerDown={(e) => { pointerInteracting.current = e.clientX updatePointerInteraction(e.clientX) }} onPointerUp={() => updatePointerInteraction(null)} onPointerOut={() => updatePointerInteraction(null)} onMouseMove={(e) => updateMovement(e.clientX)} onTouchMove={(e) => e.touches[0] && updateMovement(e.touches[0].clientX) } /> ) }
===== EXAMPLE: globe-demo ===== Title: Globe Demo
--- file: example/globe-demo.tsx --- import { Globe } from "@/registry/magicui/globe"
export default function GlobeDemo() { return ( Globe ) }
===== COMPONENT: grid-pattern ===== Title: Grid Pattern Description: A background grid pattern made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/grid-pattern.tsx --- import { useId } from "react"
import { cn } from "@/lib/utils"
interface GridPatternProps extends React.SVGProps { width?: number height?: number x?: number y?: number squares?: Array<[x: number, y: number]> strokeDasharray?: string className?: string [key: string]: unknown }
export function GridPattern({ width = 40, height = 40, x = -1, y = -1, strokeDasharray = "0", squares, className, ...props }: GridPatternProps) { const id = useId()
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-gray-400/30 stroke-gray-400/30",
className
)}
{...props}
>
<path
d={M.5 ${height}V.5H${width}}
fill="none"
strokeDasharray={strokeDasharray}
/>
<rect width="100%" height="100%" strokeWidth={0} fill={url(#${id})} />
{squares && (
{squares.map(([x, y]) => (
<rect
strokeWidth="0"
key={${x}-${y}}
width={width - 1}
height={height - 1}
x={x * width + 1}
y={y * height + 1}
/>
))}
)}
)
}
===== EXAMPLE: grid-pattern-demo ===== Title: Grid Pattern Demo
--- file: example/grid-pattern-demo.tsx --- "use client"
import { cn } from "@/lib/utils" import { GridPattern } from "@/registry/magicui/grid-pattern"
export default function GridPatternDemo() { return ( <GridPattern squares={[ [4, 4], [5, 1], [8, 2], [5, 3], [5, 5], [10, 10], [12, 15], [15, 10], [10, 15], [15, 10], [10, 15], [15, 10], ]} className={cn( "[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> ) }
===== EXAMPLE: grid-pattern-linear-gradient ===== Title: Grid Pattern Linear Gradient
--- file: example/grid-pattern-linear-gradient.tsx --- "use client"
import { cn } from "@/lib/utils" import { GridPattern } from "@/registry/magicui/grid-pattern"
export default function GridPatternLinearGradient() { return ( <GridPattern width={20} height={20} x={-1} y={-1} className={cn( "[mask-image:linear-gradient(to_bottom_right,white,transparent,transparent)]" )} /> ) }
===== EXAMPLE: grid-pattern-dashed ===== Title: Grid Pattern Dashed
--- file: example/grid-pattern-dashed.tsx --- "use client"
import { cn } from "@/lib/utils" import { GridPattern } from "@/registry/magicui/grid-pattern"
export default function GridPatternDashed() { return ( <GridPattern width={30} height={30} x={-1} y={-1} strokeDasharray={"4 2"} className={cn( "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]" )} /> ) }
===== COMPONENT: hero-video-dialog ===== Title: Hero Video Dialog Description: A hero video dialog component.
--- file: magicui/hero-video-dialog.tsx --- /* eslint-disable @next/next/no-img-element */ "use client"
import { useState } from "react" import { Play, XIcon } from "lucide-react" import { AnimatePresence, motion } from "motion/react"
import { cn } from "@/lib/utils"
type AnimationStyle = | "from-bottom" | "from-center" | "from-top" | "from-left" | "from-right" | "fade" | "top-in-bottom-out" | "left-in-right-out"
interface HeroVideoProps { animationStyle?: AnimationStyle videoSrc: string thumbnailSrc: string thumbnailAlt?: string className?: string }
const animationVariants = { "from-bottom": { initial: { y: "100%", opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: "100%", opacity: 0 }, }, "from-center": { initial: { scale: 0.5, opacity: 0 }, animate: { scale: 1, opacity: 1 }, exit: { scale: 0.5, opacity: 0 }, }, "from-top": { initial: { y: "-100%", opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: "-100%", opacity: 0 }, }, "from-left": { initial: { x: "-100%", opacity: 0 }, animate: { x: 0, opacity: 1 }, exit: { x: "-100%", opacity: 0 }, }, "from-right": { initial: { x: "100%", opacity: 0 }, animate: { x: 0, opacity: 1 }, exit: { x: "100%", opacity: 0 }, }, fade: { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, }, "top-in-bottom-out": { initial: { y: "-100%", opacity: 0 }, animate: { y: 0, opacity: 1 }, exit: { y: "100%", opacity: 0 }, }, "left-in-right-out": { initial: { x: "-100%", opacity: 0 }, animate: { x: 0, opacity: 1 }, exit: { x: "100%", opacity: 0 }, }, }
export function HeroVideoDialog({ animationStyle = "from-center", videoSrc, thumbnailSrc, thumbnailAlt = "Video thumbnail", className, }: HeroVideoProps) { const [isVideoOpen, setIsVideoOpen] = useState(false) const selectedAnimation = animationVariants[animationStyle]
return (
<div className={cn("relative", className)}>
<button
type="button"
aria-label="Play video"
className="group relative cursor-pointer border-0 bg-transparent p-0"
onClick={() => setIsVideoOpen(true)}
>
<div
className={from-primary/30 to-primary relative flex size-20 scale-100 items-center justify-center rounded-full bg-gradient-to-b shadow-md transition-all duration-200 ease-out group-hover:scale-[1.2]}
>
<Play
className="size-8 scale-100 fill-white text-white transition-transform duration-200 ease-out group-hover:scale-105"
style={{
filter:
"drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06))",
}}
/>
{isVideoOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Escape" || e.key === "Enter" || e.key === " ") {
setIsVideoOpen(false)
}
}}
onClick={() => setIsVideoOpen(false)}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md"
>
<motion.div
{...selectedAnimation}
transition={{ type: "spring", damping: 30, stiffness: 300 }}
className="relative mx-4 aspect-video w-full max-w-4xl md:mx-0"
>
<motion.button className="absolute -top-16 right-0 rounded-full bg-neutral-900/50 p-2 text-xl text-white ring-1 backdrop-blur-md dark:bg-neutral-100/50 dark:text-black">
</motion.button>
</motion.div>
</motion.div>
)}
)
}
===== EXAMPLE: hero-video-dialog-demo ===== Title: Hero Video Dialog Demo
--- file: example/hero-video-dialog-demo.tsx --- import { HeroVideoDialog } from "@/registry/magicui/hero-video-dialog"
export default function HeroVideoDialogDemo() { return ( ) }
===== EXAMPLE: hero-video-dialog-demo-top-in-bottom-out ===== Title: Hero Video Dialog Top In Bottom Out Demo
--- file: example/hero-video-dialog-demo-top-in-bottom-out.tsx --- import { HeroVideoDialog } from "@/registry/magicui/hero-video-dialog"
export default function HeroVideoDialogDemoTopInBottomOut() { return ( ) }
===== COMPONENT: highlighter ===== Title: Highlighter Description: A text highlighter that mimics the effect of a human-drawn marker stroke.
--- file: magicui/highlighter.tsx --- "use client"
import { useEffect, useRef } from "react" import type React from "react" import { useInView } from "motion/react" import { annotate } from "rough-notation" import { type RoughAnnotation } from "rough-notation/lib/model"
type AnnotationAction = | "highlight" | "underline" | "box" | "circle" | "strike-through" | "crossed-off" | "bracket"
interface HighlighterProps { children: React.ReactNode action?: AnnotationAction color?: string strokeWidth?: number animationDuration?: number iterations?: number padding?: number multiline?: boolean isView?: boolean }
export function Highlighter({ children, action = "highlight", color = "#ffd1dc", strokeWidth = 1.5, animationDuration = 600, iterations = 2, padding = 2, multiline = true, isView = false, }: HighlighterProps) { const elementRef = useRef(null) const annotationRef = useRef<RoughAnnotation | null>(null)
const isInView = useInView(elementRef, { once: true, margin: "-10%", })
// If isView is false, always show. If isView is true, wait for inView const shouldShow = !isView || isInView
useEffect(() => { if (!shouldShow) return
const element = elementRef.current
if (!element) return
const annotationConfig = {
type: action,
color,
strokeWidth,
animationDuration,
iterations,
padding,
multiline,
}
const annotation = annotate(element, annotationConfig)
annotationRef.current = annotation
annotationRef.current.show()
const resizeObserver = new ResizeObserver(() => {
annotation.hide()
annotation.show()
})
resizeObserver.observe(element)
resizeObserver.observe(document.body)
return () => {
if (element) {
annotate(element, { type: action }).remove()
resizeObserver.disconnect()
}
}
}, [ shouldShow, action, color, strokeWidth, animationDuration, iterations, padding, multiline, ])
return ( {children} ) }
===== EXAMPLE: highlighter-demo ===== Title: Highlighter Demo
--- file: example/highlighter-demo.tsx --- import { Highlighter } from "@/registry/magicui/highlighter"
export default function HighlighterDemo() { return ( The{" "} Magic UI Highlighter {" "} makes important{" "} text stand out {" "} effortlessly. ) }
--- file: magicui/highlighter.tsx --- "use client"
import { useEffect, useRef } from "react" import type React from "react" import { useInView } from "motion/react" import { annotate } from "rough-notation" import { type RoughAnnotation } from "rough-notation/lib/model"
type AnnotationAction = | "highlight" | "underline" | "box" | "circle" | "strike-through" | "crossed-off" | "bracket"
interface HighlighterProps { children: React.ReactNode action?: AnnotationAction color?: string strokeWidth?: number animationDuration?: number iterations?: number padding?: number multiline?: boolean isView?: boolean }
export function Highlighter({ children, action = "highlight", color = "#ffd1dc", strokeWidth = 1.5, animationDuration = 600, iterations = 2, padding = 2, multiline = true, isView = false, }: HighlighterProps) { const elementRef = useRef(null) const annotationRef = useRef<RoughAnnotation | null>(null)
const isInView = useInView(elementRef, { once: true, margin: "-10%", })
// If isView is false, always show. If isView is true, wait for inView const shouldShow = !isView || isInView
useEffect(() => { if (!shouldShow) return
const element = elementRef.current
if (!element) return
const annotationConfig = {
type: action,
color,
strokeWidth,
animationDuration,
iterations,
padding,
multiline,
}
const annotation = annotate(element, annotationConfig)
annotationRef.current = annotation
annotationRef.current.show()
const resizeObserver = new ResizeObserver(() => {
annotation.hide()
annotation.show()
})
resizeObserver.observe(element)
resizeObserver.observe(document.body)
return () => {
if (element) {
annotate(element, { type: action }).remove()
resizeObserver.disconnect()
}
}
}, [ shouldShow, action, color, strokeWidth, animationDuration, iterations, padding, multiline, ])
return ( {children} ) }
===== COMPONENT: hyper-text ===== Title: Hyper Text Description: A text animation that scrambles letters before revealing the final text.
--- file: magicui/hyper-text.tsx --- "use client"
import { useEffect, useRef, useState } from "react" import { AnimatePresence, motion, MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
type CharacterSet = string[] | readonly string[]
interface HyperTextProps extends MotionProps { /** The text content to be animated / children: string /* Optional className for styling / className?: string /* Duration of the animation in milliseconds / duration?: number /* Delay before animation starts in milliseconds / delay?: number /* Component to render as - defaults to div / as?: React.ElementType /* Whether to start animation when element comes into view / startOnView?: boolean /* Whether to trigger animation on hover / animateOnHover?: boolean /* Custom character set for scramble effect. Defaults to uppercase alphabet */ characterSet?: CharacterSet }
const DEFAULT_CHARACTER_SET = Object.freeze( "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("") ) as readonly string[]
const getRandomInt = (max: number): number => Math.floor(Math.random() * max)
export function HyperText({ children, className, duration = 800, delay = 0, as: Component = "div", startOnView = false, animateOnHover = true, characterSet = DEFAULT_CHARACTER_SET, ...props }: HyperTextProps) { const MotionComponent = motion.create(Component, { forwardMotionProps: true, })
const [displayText, setDisplayText] = useState<string[]>(() => children.split("") ) const [isAnimating, setIsAnimating] = useState(false) const iterationCount = useRef(0) const elementRef = useRef(null)
const handleAnimationTrigger = () => { if (animateOnHover && !isAnimating) { iterationCount.current = 0 setIsAnimating(true) } }
// Handle animation start based on view or delay useEffect(() => { if (!startOnView) { const startTimeout = setTimeout(() => { setIsAnimating(true) }, delay) return () => clearTimeout(startTimeout) }
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setTimeout(() => {
setIsAnimating(true)
}, delay)
observer.disconnect()
}
},
{ threshold: 0.1, rootMargin: "-30% 0px -30% 0px" }
)
if (elementRef.current) {
observer.observe(elementRef.current)
}
return () => observer.disconnect()
}, [delay, startOnView])
// Handle scramble animation useEffect(() => { if (!isAnimating) return
const maxIterations = children.length
const startTime = performance.now()
let animationFrameId: number
const animate = (currentTime: number) => {
const elapsed = currentTime - startTime
const progress = Math.min(elapsed / duration, 1)
iterationCount.current = progress * maxIterations
setDisplayText((currentText) =>
currentText.map((letter, index) =>
letter === " "
? letter
: index <= iterationCount.current
? children[index]
: characterSet[getRandomInt(characterSet.length)]
)
)
if (progress < 1) {
animationFrameId = requestAnimationFrame(animate)
} else {
setIsAnimating(false)
}
}
animationFrameId = requestAnimationFrame(animate)
return () => cancelAnimationFrame(animationFrameId)
}, [children, duration, isAnimating, characterSet])
return ( <MotionComponent ref={elementRef} className={cn("overflow-hidden py-2 text-4xl font-bold", className)} onMouseEnter={handleAnimationTrigger} {...props} > {displayText.map((letter, index) => ( <motion.span key={index} className={cn("font-mono", letter === " " ? "w-3" : "")} > {letter.toUpperCase()} </motion.span> ))} ) }
===== EXAMPLE: hyper-text-demo ===== Title: Hyper Text Demo
--- file: example/hyper-text-demo.tsx --- import { HyperText } from "@/registry/magicui/hyper-text"
export default function HyperTextDemo() { return Hover Me! }
===== COMPONENT: icon-cloud ===== Title: Icon Cloud Description: An interactive 3D tag cloud component
--- file: magicui/icon-cloud.tsx --- "use client"
import React, { useEffect, useRef, useState } from "react" import { renderToString } from "react-dom/server"
interface Icon { x: number y: number z: number scale: number opacity: number id: number }
interface IconCloudProps { icons?: React.ReactNode[] images?: string[] }
function easeOutCubic(t: number): number { return 1 - Math.pow(1 - t, 3) }
export function IconCloud({ icons, images }: IconCloudProps) { const canvasRef = useRef(null) const [iconPositions, setIconPositions] = useState<Icon[]>([]) const [rotation, setRotation] = useState({ x: 0, y: 0 }) const [isDragging, setIsDragging] = useState(false) const [lastMousePos, setLastMousePos] = useState({ x: 0, y: 0 }) const [mousePos, setMousePos] = useState({ x: 0, y: 0 }) const [targetRotation, setTargetRotation] = useState<{ x: number y: number startX: number startY: number distance: number startTime: number duration: number } | null>(null) const animationFrameRef = useRef(0) const rotationRef = useRef(rotation) const iconCanvasesRef = useRef<HTMLCanvasElement[]>([]) const imagesLoadedRef = useRef<boolean[]>([])
// Create icon canvases once when icons/images change useEffect(() => { if (!icons && !images) return
const items = icons || images || []
imagesLoadedRef.current = new Array(items.length).fill(false)
const newIconCanvases = items.map((item, index) => {
const offscreen = document.createElement("canvas")
offscreen.width = 40
offscreen.height = 40
const offCtx = offscreen.getContext("2d")
if (offCtx) {
if (images) {
// Handle image URLs directly
const img = new Image()
img.crossOrigin = "anonymous"
img.src = items[index] as string
img.onload = () => {
offCtx.clearRect(0, 0, offscreen.width, offscreen.height)
// Create circular clipping path
offCtx.beginPath()
offCtx.arc(20, 20, 20, 0, Math.PI * 2)
offCtx.closePath()
offCtx.clip()
// Draw the image
offCtx.drawImage(img, 0, 0, 40, 40)
imagesLoadedRef.current[index] = true
}
} else {
// Handle SVG icons
offCtx.scale(0.4, 0.4)
const svgString = renderToString(item as React.ReactElement)
const img = new Image()
img.src = "data:image/svg+xml;base64," + btoa(svgString)
img.onload = () => {
offCtx.clearRect(0, 0, offscreen.width, offscreen.height)
offCtx.drawImage(img, 0, 0)
imagesLoadedRef.current[index] = true
}
}
}
return offscreen
})
iconCanvasesRef.current = newIconCanvases
}, [icons, images])
// Generate initial icon positions on a sphere useEffect(() => { const items = icons || images || [] const newIcons: Icon[] = [] const numIcons = items.length || 20
// Fibonacci sphere parameters
const offset = 2 / numIcons
const increment = Math.PI * (3 - Math.sqrt(5))
for (let i = 0; i < numIcons; i++) {
const y = i * offset - 1 + offset / 2
const r = Math.sqrt(1 - y * y)
const phi = i * increment
const x = Math.cos(phi) * r
const z = Math.sin(phi) * r
newIcons.push({
x: x * 100,
y: y * 100,
z: z * 100,
scale: 1,
opacity: 1,
id: i,
})
}
setIconPositions(newIcons)
}, [icons, images])
// Handle mouse events const handleMouseDown = (e: React.MouseEvent) => { const rect = canvasRef.current?.getBoundingClientRect() if (!rect || !canvasRef.current) return
const x = e.clientX - rect.left
const y = e.clientY - rect.top
const ctx = canvasRef.current.getContext("2d")
if (!ctx) return
iconPositions.forEach((icon) => {
const cosX = Math.cos(rotationRef.current.x)
const sinX = Math.sin(rotationRef.current.x)
const cosY = Math.cos(rotationRef.current.y)
const sinY = Math.sin(rotationRef.current.y)
const rotatedX = icon.x * cosY - icon.z * sinY
const rotatedZ = icon.x * sinY + icon.z * cosY
const rotatedY = icon.y * cosX + rotatedZ * sinX
const screenX = canvasRef.current!.width / 2 + rotatedX
const screenY = canvasRef.current!.height / 2 + rotatedY
const scale = (rotatedZ + 200) / 300
const radius = 20 * scale
const dx = x - screenX
const dy = y - screenY
if (dx * dx + dy * dy < radius * radius) {
const targetX = -Math.atan2(
icon.y,
Math.sqrt(icon.x * icon.x + icon.z * icon.z)
)
const targetY = Math.atan2(icon.x, icon.z)
const currentX = rotationRef.current.x
const currentY = rotationRef.current.y
const distance = Math.sqrt(
Math.pow(targetX - currentX, 2) + Math.pow(targetY - currentY, 2)
)
const duration = Math.min(2000, Math.max(800, distance * 1000))
setTargetRotation({
x: targetX,
y: targetY,
startX: currentX,
startY: currentY,
distance,
startTime: performance.now(),
duration,
})
return
}
})
setIsDragging(true)
setLastMousePos({ x: e.clientX, y: e.clientY })
}
const handleMouseMove = (e: React.MouseEvent) => { const rect = canvasRef.current?.getBoundingClientRect() if (rect) { const x = e.clientX - rect.left const y = e.clientY - rect.top setMousePos({ x, y }) }
if (isDragging) {
const deltaX = e.clientX - lastMousePos.x
const deltaY = e.clientY - lastMousePos.y
rotationRef.current = {
x: rotationRef.current.x + deltaY * 0.002,
y: rotationRef.current.y + deltaX * 0.002,
}
setLastMousePos({ x: e.clientX, y: e.clientY })
}
}
const handleMouseUp = () => { setIsDragging(false) }
// Animation and rendering useEffect(() => { const canvas = canvasRef.current const ctx = canvas?.getContext("2d") if (!canvas || !ctx) return
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height)
const centerX = canvas.width / 2
const centerY = canvas.height / 2
const maxDistance = Math.sqrt(centerX * centerX + centerY * centerY)
const dx = mousePos.x - centerX
const dy = mousePos.y - centerY
const distance = Math.sqrt(dx * dx + dy * dy)
const speed = 0.003 + (distance / maxDistance) * 0.01
if (targetRotation) {
const elapsed = performance.now() - targetRotation.startTime
const progress = Math.min(1, elapsed / targetRotation.duration)
const easedProgress = easeOutCubic(progress)
rotationRef.current = {
x:
targetRotation.startX +
(targetRotation.x - targetRotation.startX) * easedProgress,
y:
targetRotation.startY +
(targetRotation.y - targetRotation.startY) * easedProgress,
}
if (progress >= 1) {
setTargetRotation(null)
}
} else if (!isDragging) {
rotationRef.current = {
x: rotationRef.current.x + (dy / canvas.height) * speed,
y: rotationRef.current.y + (dx / canvas.width) * speed,
}
}
iconPositions.forEach((icon, index) => {
const cosX = Math.cos(rotationRef.current.x)
const sinX = Math.sin(rotationRef.current.x)
const cosY = Math.cos(rotationRef.current.y)
const sinY = Math.sin(rotationRef.current.y)
const rotatedX = icon.x * cosY - icon.z * sinY
const rotatedZ = icon.x * sinY + icon.z * cosY
const rotatedY = icon.y * cosX + rotatedZ * sinX
const scale = (rotatedZ + 200) / 300
const opacity = Math.max(0.2, Math.min(1, (rotatedZ + 150) / 200))
ctx.save()
ctx.translate(canvas.width / 2 + rotatedX, canvas.height / 2 + rotatedY)
ctx.scale(scale, scale)
ctx.globalAlpha = opacity
if (icons || images) {
// Only try to render icons/images if they exist
if (
iconCanvasesRef.current[index] &&
imagesLoadedRef.current[index]
) {
ctx.drawImage(iconCanvasesRef.current[index], -20, -20, 40, 40)
}
} else {
// Show numbered circles if no icons/images are provided
ctx.beginPath()
ctx.arc(0, 0, 20, 0, Math.PI * 2)
ctx.fillStyle = "#4444ff"
ctx.fill()
ctx.fillStyle = "white"
ctx.textAlign = "center"
ctx.textBaseline = "middle"
ctx.font = "16px Arial"
ctx.fillText(`${icon.id + 1}`, 0, 0)
}
ctx.restore()
})
animationFrameRef.current = requestAnimationFrame(animate)
}
animate()
return () => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
}
}, [icons, images, iconPositions, isDragging, mousePos, targetRotation])
return ( ) }
===== EXAMPLE: icon-cloud-demo ===== Title: Icon Cloud Demo
--- file: example/icon-cloud-demo.tsx --- import { IconCloud } from "@/registry/magicui/icon-cloud"
const slugs = [ "typescript", "javascript", "dart", "java", "react", "flutter", "android", "html5", "css3", "nodedotjs", "express", "nextdotjs", "prisma", "amazonaws", "postgresql", "firebase", "nginx", "vercel", "testinglibrary", "jest", "cypress", "docker", "git", "jira", "github", "gitlab", "visualstudiocode", "androidstudio", "sonarqube", "figma", ]
export default function IconCloudDemo() {
const images = slugs.map(
(slug) => https://cdn.simpleicons.org/${slug}/${slug}
)
return ( ) }
===== EXAMPLE: icon-cloud-demo-2 ===== Title: Icon Cloud Demo 2
--- file: example/icon-cloud-demo-2.tsx --- import { IconCloud } from "@/registry/magicui/icon-cloud"
const images = [ "https://images.unsplash.com/photo-1720048171230-c60d162f93a0?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675553988173-a5249b5815fe?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675297844586-534b030564e0?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675555581018-7f1a352ff9a6?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://images.unsplash.com/photo-1719937050517-68d4e2a1702e?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://images.unsplash.com/photo-1720048171230-c60d162f93a0?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675553988173-a5249b5815fe?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675297844586-534b030564e0?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675555581018-7f1a352ff9a6?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://images.unsplash.com/photo-1719937050517-68d4e2a1702e?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://images.unsplash.com/photo-1720048171230-c60d162f93a0?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675553988173-a5249b5815fe?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675297844586-534b030564e0?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1675555581018-7f1a352ff9a6?q=80&w=1964&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://images.unsplash.com/photo-1719937050517-68d4e2a1702e?q=80&w=1974&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", ]
export default function IconCloudDemo() { return ( ) }
===== EXAMPLE: icon-cloud-demo-3 ===== Title: Icon Cloud Demo 3
--- file: example/icon-cloud-demo-3.tsx --- import { IconCloud } from "@/registry/magicui/icon-cloud"
const Icons = { gitHub: () => ( ), notion: () => ( ), openai: () => ( ), googleDrive: () => ( ), whatsapp: () => ( ), }
export default function IconCloudDemo() { return ( <IconCloud icons={[ <Icons.gitHub key="github" />, <Icons.notion key="notion" />, <Icons.openai key="openai" />, <Icons.googleDrive key="gdrive" />, <Icons.whatsapp key="whatsapp" />, <Icons.gitHub key="github2" />, <Icons.notion key="notion2" />, <Icons.openai key="openai2" />, <Icons.googleDrive key="gdrive2" />, <Icons.whatsapp key="whatsapp2" />, <Icons.gitHub key="github3" />, <Icons.notion key="notion3" />, <Icons.openai key="openai3" />, <Icons.googleDrive key="gdrive3" />, <Icons.whatsapp key="whatsapp3" />, <Icons.gitHub key="github4" />, <Icons.notion key="notion4" />, <Icons.openai key="openai4" />, <Icons.googleDrive key="gdrive4" />, <Icons.whatsapp key="whatsapp4" />, <Icons.notion key="notion4" />, <Icons.openai key="openai4" />, <Icons.googleDrive key="gdrive4" />, <Icons.whatsapp key="whatsapp4" />, ]} /> ) }
===== COMPONENT: interactive-grid-pattern ===== Title: Interactive Grid Pattern Description: A interactive background grid pattern made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/interactive-grid-pattern.tsx --- "use client"
import React, { useState } from "react"
import { cn } from "@/lib/utils"
/**
- InteractiveGridPattern is a component that renders a grid pattern with interactive squares.
- @param width - The width of each square.
- @param height - The height of each square.
- @param squares - The number of squares in the grid. The first element is the number of horizontal squares, and the second element is the number of vertical squares.
- @param className - The class name of the grid.
- @param squaresClassName - The class name of the squares. */ interface InteractiveGridPatternProps extends React.SVGProps { width?: number height?: number squares?: [number, number] // [horizontal, vertical] className?: string squaresClassName?: string }
/**
- The InteractiveGridPattern component.
- @see InteractiveGridPatternProps for the props interface.
- @returns A React component. */ export function InteractiveGridPattern({ width = 40, height = 40, squares = [24, 24], className, squaresClassName, ...props }: InteractiveGridPatternProps) { const [horizontal, vertical] = squares const [hoveredSquare, setHoveredSquare] = useState<number | null>(null)
return ( <svg width={width * horizontal} height={height * vertical} className={cn( "absolute inset-0 h-full w-full border border-gray-400/30", className )} {...props} > {Array.from({ length: horizontal * vertical }).map((_, index) => { const x = (index % horizontal) * width const y = Math.floor(index / horizontal) * height return ( <rect key={index} x={x} y={y} width={width} height={height} className={cn( "stroke-gray-400/30 transition-all duration-100 ease-in-out [&:not(:hover)]:duration-1000", hoveredSquare === index ? "fill-gray-300/30" : "fill-transparent", squaresClassName )} onMouseEnter={() => setHoveredSquare(index)} onMouseLeave={() => setHoveredSquare(null)} /> ) })} ) }
===== EXAMPLE: interactive-grid-pattern-demo ===== Title: Interactive Grid Pattern Demo
--- file: example/interactive-grid-pattern-demo.tsx --- "use client"
import { cn } from "@/lib/utils" import { InteractiveGridPattern } from "@/registry/magicui/interactive-grid-pattern"
export default function InteractiveGridPatternDemo() { return ( <InteractiveGridPattern className={cn( "[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> ) }
===== EXAMPLE: interactive-grid-pattern-demo-2 ===== Title: Interactive Grid Pattern Demo 2
--- file: example/interactive-grid-pattern-demo-2.tsx --- "use client"
import { cn } from "@/lib/utils" import { InteractiveGridPattern } from "@/registry/magicui/interactive-grid-pattern"
export default function InteractiveGridPatternDemo() { return ( <InteractiveGridPattern className={cn( "[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]" )} width={20} height={20} squares={[80, 80]} squaresClassName="hover:fill-blue-500" /> ) }
===== COMPONENT: interactive-hover-button ===== Title: interactive-hover-button Description: The interactive-hover-button component.
--- file: magicui/interactive-hover-button.tsx --- import { ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
export function InteractiveHoverButton({ children, className, ...props }: React.ButtonHTMLAttributes) { return ( <button className={cn( "group bg-background relative w-auto cursor-pointer overflow-hidden rounded-full border p-2 px-6 text-center font-semibold", className )} {...props} > {children} {children} ) }
===== EXAMPLE: interactive-hover-button-demo ===== Title: Interactive Hover Button Demo
--- file: example/interactive-hover-button-demo.tsx --- import { InteractiveHoverButton } from "@/registry/magicui/interactive-hover-button"
export default function InteractiveHoverButtonDemo() { return Hover Me }
===== COMPONENT: iphone ===== Title: iPhone Description: A mockup of the iPhone
--- file: magicui/iphone.tsx --- import type { HTMLAttributes } from "react"
const PHONE_WIDTH = 433 const PHONE_HEIGHT = 882 const SCREEN_X = 21.25 const SCREEN_Y = 19.25 const SCREEN_WIDTH = 389.5 const SCREEN_HEIGHT = 843.5 const SCREEN_RADIUS = 55.75
// Calculated percentages const LEFT_PCT = (SCREEN_X / PHONE_WIDTH) * 100 const TOP_PCT = (SCREEN_Y / PHONE_HEIGHT) * 100 const WIDTH_PCT = (SCREEN_WIDTH / PHONE_WIDTH) * 100 const HEIGHT_PCT = (SCREEN_HEIGHT / PHONE_HEIGHT) * 100 const RADIUS_H = (SCREEN_RADIUS / SCREEN_WIDTH) * 100 const RADIUS_V = (SCREEN_RADIUS / SCREEN_HEIGHT) * 100
export interface IphoneProps extends HTMLAttributes { src?: string videoSrc?: string }
export function Iphone({ src, videoSrc, className, style, ...props }: IphoneProps) { const hasVideo = !!videoSrc const hasMedia = hasVideo || !!src
return (
<div
className={relative inline-block w-full align-middle leading-none ${className}}
style={{
aspectRatio: ${PHONE_WIDTH}/${PHONE_HEIGHT},
...style,
}}
{...props}
>
{hasVideo && (
<div
className="pointer-events-none absolute z-0 overflow-hidden"
style={{
left: ${LEFT_PCT}%,
top: ${TOP_PCT}%,
width: ${WIDTH_PCT}%,
height: ${HEIGHT_PCT}%,
borderRadius: ${RADIUS_H}% / ${RADIUS_V}%,
}}
>
)}
{!hasVideo && src && (
<div
className="pointer-events-none absolute z-0 overflow-hidden"
style={{
left: `${LEFT_PCT}%`,
top: `${TOP_PCT}%`,
width: `${WIDTH_PCT}%`,
height: `${HEIGHT_PCT}%`,
borderRadius: `${RADIUS_H}% / ${RADIUS_V}%`,
}}
>
<img
src={src}
alt=""
className="block size-full object-cover object-top"
/>
</div>
)}
<svg
viewBox={`0 0 ${PHONE_WIDTH} ${PHONE_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute inset-0 size-full"
style={{ transform: "translateZ(0)" }}
>
<g mask={hasMedia ? "url(#screenPunch)" : undefined}>
<path
d="M2 73C2 32.6832 34.6832 0 75 0H357C397.317 0 430 32.6832 430 73V809C430 849.317 397.317 882 357 882H75C34.6832 882 2 849.317 2 809V73Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M0 171C0 170.448 0.447715 170 1 170H3V204H1C0.447715 204 0 203.552 0 203V171Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M1 234C1 233.448 1.44772 233 2 233H3.5V300H2C1.44772 300 1 299.552 1 299V234Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M1 319C1 318.448 1.44772 318 2 318H3.5V385H2C1.44772 385 1 384.552 1 384V319Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M430 279H432C432.552 279 433 279.448 433 280V384C433 384.552 432.552 385 432 385H430V279Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M6 74C6 35.3401 37.3401 4 76 4H356C394.66 4 426 35.3401 426 74V808C426 846.66 394.66 878 356 878H76C37.3401 878 6 846.66 6 808V74Z"
className="fill-white dark:fill-[#262626]"
/>
</g>
<path
opacity="0.5"
d="M174 5H258V5.5C258 6.60457 257.105 7.5 256 7.5H176C174.895 7.5 174 6.60457 174 5.5V5Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d={`M${SCREEN_X} 75C${SCREEN_X} 44.2101 46.2101 ${SCREEN_Y} 77 ${SCREEN_Y}H355C385.79 ${SCREEN_Y} 410.75 44.2101 410.75 75V807C410.75 837.79 385.79 862.75 355 862.75H77C46.2101 862.75 ${SCREEN_X} 837.79 ${SCREEN_X} 807V75Z`}
className="fill-[#E5E5E5] stroke-[#E5E5E5] stroke-[0.5] dark:fill-[#404040] dark:stroke-[#404040]"
mask={hasMedia ? "url(#screenPunch)" : undefined}
/>
<path
d="M154 48.5C154 38.2827 162.283 30 172.5 30H259.5C269.717 30 278 38.2827 278 48.5C278 58.7173 269.717 67 259.5 67H172.5C162.283 67 154 58.7173 154 48.5Z"
className="fill-[#F5F5F5] dark:fill-[#262626]"
/>
<path
d="M249 48.5C249 42.701 253.701 38 259.5 38C265.299 38 270 42.701 270 48.5C270 54.299 265.299 59 259.5 59C253.701 59 249 54.299 249 48.5Z"
className="fill-[#F5F5F5] dark:fill-[#262626]"
/>
<path
d="M254 48.5C254 45.4624 256.462 43 259.5 43C262.538 43 265 45.4624 265 48.5C265 51.5376 262.538 54 259.5 54C256.462 54 254 51.5376 254 48.5Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<defs>
<mask id="screenPunch" maskUnits="userSpaceOnUse">
<rect
x="0"
y="0"
width={PHONE_WIDTH}
height={PHONE_HEIGHT}
fill="white"
/>
<rect
x={SCREEN_X}
y={SCREEN_Y}
width={SCREEN_WIDTH}
height={SCREEN_HEIGHT}
rx={SCREEN_RADIUS}
ry={SCREEN_RADIUS}
fill="black"
/>
</mask>
<clipPath id="roundedCorners">
<rect
x={SCREEN_X}
y={SCREEN_Y}
width={SCREEN_WIDTH}
height={SCREEN_HEIGHT}
rx={SCREEN_RADIUS}
ry={SCREEN_RADIUS}
/>
</clipPath>
</defs>
</svg>
</div>
) }
===== EXAMPLE: iphone-demo ===== Title: iPhone Demo
--- file: example/iphone-demo.tsx --- import { Iphone } from "@/registry/magicui/iphone"
export default function Demo() { return ( ) }
===== EXAMPLE: iphone-demo-2 ===== Title: iPhone Demo 2
--- file: example/iphone-demo-2.tsx --- import { Iphone } from "@/registry/magicui/iphone"
export default function Demo() { return ( ) }
===== EXAMPLE: iphone-demo-3 ===== Title: iPhone Demo 3
--- file: example/iphone-demo-3.tsx --- import { Iphone } from "@/registry/magicui/iphone"
export default function Demo() { return ( ) }
===== COMPONENT: lens ===== Title: Lens Description: A interactive component that enables zooming into images, videos and other elements.
--- file: magicui/lens.tsx --- "use client"
import React, { useCallback, useMemo, useRef, useState } from "react" import { AnimatePresence, motion, useMotionTemplate } from "motion/react"
interface Position { /** The x coordinate of the lens / x: number /* The y coordinate of the lens */ y: number }
interface LensProps { /** The children of the lens / children: React.ReactNode /* The zoom factor of the lens / zoomFactor?: number /* The size of the lens / lensSize?: number /* The position of the lens / position?: Position /* The default position of the lens / defaultPosition?: Position /* Whether the lens is static / isStatic?: boolean /* The duration of the animation / duration?: number /* The color of the lens / lensColor?: string /* The aria label of the lens */ ariaLabel?: string }
export function Lens({ children, zoomFactor = 1.3, lensSize = 170, isStatic = false, position = { x: 0, y: 0 }, defaultPosition, duration = 0.1, lensColor = "black", ariaLabel = "Zoom Area", }: LensProps) { if (zoomFactor < 1) { throw new Error("zoomFactor must be greater than 1") } if (lensSize < 0) { throw new Error("lensSize must be greater than 0") }
const [isHovering, setIsHovering] = useState(false) const [mousePosition, setMousePosition] = useState(position) const containerRef = useRef(null)
const currentPosition = useMemo(() => { if (isStatic) return position if (defaultPosition && !isHovering) return defaultPosition return mousePosition }, [isStatic, position, defaultPosition, isHovering, mousePosition])
const handleMouseMove = useCallback((e: React.MouseEvent) => { const rect = e.currentTarget.getBoundingClientRect() setMousePosition({ x: e.clientX - rect.left, y: e.clientY - rect.top, }) }, [])
const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === "Escape") setIsHovering(false) }, [])
const maskImage = useMotionTemplateradial-gradient(circle ${ lensSize / 2 }px at ${currentPosition.x}px ${ currentPosition.y }px, ${lensColor} 100%, transparent 100%)
const LensContent = useMemo(() => { const { x, y } = currentPosition
return (
<motion.div
initial={{ opacity: 0, scale: 0.58 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
transition={{ duration }}
className="absolute inset-0 overflow-hidden"
style={{
maskImage,
WebkitMaskImage: maskImage,
transformOrigin: `${x}px ${y}px`,
zIndex: 50,
}}
>
<div
className="absolute inset-0"
style={{
transform: `scale(${zoomFactor})`,
transformOrigin: `${x}px ${y}px`,
}}
>
{children}
</div>
</motion.div>
)
}, [currentPosition, lensSize, lensColor, zoomFactor, children, duration])
return ( <div ref={containerRef} className="relative z-20 overflow-hidden rounded-xl" onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)} onMouseMove={handleMouseMove} onKeyDown={handleKeyDown} role="region" aria-label={ariaLabel} tabIndex={0} > {children} {isStatic || defaultPosition ? ( LensContent ) : ( {isHovering && LensContent} )} ) }
===== EXAMPLE: lens-demo ===== Title: Lens Demo
--- file: example/lens-demo.tsx --- /* eslint-disable @next/next/no-img-element */
"use client"
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Lens } from "@/registry/magicui/lens"
export default function LensDemo() { return ( Your next camp See our latest and best camp destinations all across the five continents of the globe. Let's go Another time ) }
===== EXAMPLE: lens-demo-2 ===== Title: Lens Demo 2
--- file: example/lens-demo-2.tsx --- /* eslint-disable @next/next/no-img-element */
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Lens } from "@/registry/magicui/lens"
export default function LensDemo() { return ( <Lens isStatic position={{ x: 260, y: 150 }}> Your next camp See our latest and best camp destinations all across the five continents of the globe. Let's go Another time ) }
===== EXAMPLE: lens-demo-3 ===== Title: Lens Demo 3
--- file: example/lens-demo-3.tsx --- /* eslint-disable @next/next/no-img-element */
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Lens } from "@/registry/magicui/lens"
export default function LensDemo() { return ( <Lens defaultPosition={{ x: 260, y: 150 }}> Your next camp See our latest and best camp destinations all across the five continents of the globe. Let's go Another time ) }
===== COMPONENT: light-rays ===== Title: Light Rays Description: A component with animated light rays which shine down from above.
--- file: magicui/light-rays.tsx --- "use client"
import { useEffect, useState, type CSSProperties } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
interface LightRaysProps extends React.HTMLAttributes { ref?: React.Ref count?: number color?: string blur?: number speed?: number length?: string }
type LightRay = { id: string left: number rotate: number width: number swing: number delay: number duration: number intensity: number }
const createRays = (count: number, cycle: number): LightRay[] => { if (count <= 0) return []
return Array.from({ length: count }, (_, index) => { const left = 8 + Math.random() * 84 const rotate = -28 + Math.random() * 56 const width = 160 + Math.random() * 160 const swing = 0.8 + Math.random() * 1.8 const delay = Math.random() * cycle const duration = cycle * (0.75 + Math.random() * 0.5) const intensity = 0.6 + Math.random() * 0.5
return {
id: `${index}-${Math.round(left * 10)}`,
left,
rotate,
width,
swing,
delay,
duration,
intensity,
}
}) }
const Ray = ({
left,
rotate,
width,
swing,
delay,
duration,
intensity,
}: LightRay) => {
return (
<motion.div
className="pointer-events-none absolute -top-[12%] left-[var(--ray-left)] h-[var(--light-rays-length)] w-[var(--ray-width)] origin-top -translate-x-1/2 rounded-full bg-gradient-to-b from-[color-mix(in_srgb,var(--light-rays-color)_70%,transparent)] to-transparent opacity-0 mix-blend-screen blur-[var(--light-rays-blur)]"
style={
{
"--ray-left": ${left}%,
"--ray-width": ${width}px,
} as CSSProperties
}
initial={{ rotate: rotate }}
animate={{
opacity: [0, intensity, 0],
rotate: [rotate - swing, rotate + swing, rotate - swing],
}}
transition={{
duration: duration,
repeat: Infinity,
ease: "easeInOut",
delay: delay,
repeatDelay: duration * 0.1,
}}
/>
)
}
export function LightRays({ className, style, count = 7, color = "rgba(160, 210, 255, 0.2)", blur = 36, speed = 14, length = "70vh", ref, ...props }: LightRaysProps) { const [rays, setRays] = useState<LightRay[]>([]) const cycleDuration = Math.max(speed, 0.1)
useEffect(() => { setRays(createRays(count, cycleDuration)) }, [count, cycleDuration])
return (
<div
ref={ref}
className={cn(
"pointer-events-none absolute inset-0 isolate overflow-hidden rounded-[inherit]",
className
)}
style={
{
"--light-rays-color": color,
"--light-rays-blur": ${blur}px,
"--light-rays-length": length,
...style,
} as CSSProperties
}
{...props}
>
<div
aria-hidden
className="absolute inset-0 opacity-60"
style={
{
background:
"radial-gradient(circle at 20% 15%, color-mix(in srgb, var(--light-rays-color) 45%, transparent), transparent 70%)",
} as CSSProperties
}
/>
<div
aria-hidden
className="absolute inset-0 opacity-60"
style={
{
background:
"radial-gradient(circle at 80% 10%, color-mix(in srgb, var(--light-rays-color) 35%, transparent), transparent 75%)",
} as CSSProperties
}
/>
{rays.map((ray) => (
<Ray key={ray.id} {...ray} />
))}
)
}
===== EXAMPLE: light-rays-demo ===== Title: light-rays-demo
--- file: example/light-rays-demo.tsx --- import { LightRays } from "@/registry/magicui/light-rays"
export default function Component() { return ( Ambient glow Light Rays Drop this component into any container and it will fill the space with softly animated light rays shining from above. ) }
===== COMPONENT: line-shadow-text ===== Title: Line Shadow Text Description: A text component with a moving line shadow.
--- file: magicui/line-shadow-text.tsx --- "use client"
import { motion, MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
interface LineShadowTextProps extends Omit<React.HTMLAttributes, keyof MotionProps>, MotionProps { shadowColor?: string as?: React.ElementType }
export function LineShadowText({ children, shadowColor = "black", className, as: Component = "span", ...props }: LineShadowTextProps) { const MotionComponent = motion.create(Component) const content = typeof children === "string" ? children : null
if (!content) { throw new Error("LineShadowText only accepts string content") }
return ( <MotionComponent style={{ "--shadow-color": shadowColor } as React.CSSProperties} className={cn( "relative z-0 inline-flex", "after:absolute after:top-[0.04em] after:left-[0.04em] after:content-[attr(data-text)]", "after:bg-[linear-gradient(45deg,transparent_45%,var(--shadow-color)_45%,var(--shadow-color)_55%,transparent_0)]", "after:-z-10 after:bg-[length:0.06em_0.06em] after:bg-clip-text after:text-transparent", "after:animate-line-shadow", className )} data-text={content} {...props} > {content} ) }
===== EXAMPLE: line-shadow-text-demo ===== Title: Line Shadow Text Demo
--- file: example/line-shadow-text-demo.tsx --- "use client"
import { useTheme } from "next-themes"
import { LineShadowText } from "@/registry/magicui/line-shadow-text"
export default function LineShadowTextDemo() { const theme = useTheme() const shadowColor = theme.resolvedTheme === "dark" ? "white" : "black" return ( Ship Fast ) }
===== COMPONENT: magic-card ===== Title: Magic Card Description: A spotlight effect that follows your mouse cursor and highlights borders on hover.
--- file: magicui/magic-card.tsx --- "use client"
import React, { useCallback, useEffect } from "react" import { motion, useMotionTemplate, useMotionValue } from "motion/react"
import { cn } from "@/lib/utils"
interface MagicCardProps { children?: React.ReactNode className?: string gradientSize?: number gradientColor?: string gradientOpacity?: number gradientFrom?: string gradientTo?: string }
export function MagicCard({ children, className, gradientSize = 200, gradientColor = "#262626", gradientOpacity = 0.8, gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", }: MagicCardProps) { const mouseX = useMotionValue(-gradientSize) const mouseY = useMotionValue(-gradientSize) const reset = useCallback(() => { mouseX.set(-gradientSize) mouseY.set(-gradientSize) }, [gradientSize, mouseX, mouseY])
const handlePointerMove = useCallback( (e: React.PointerEvent) => { const rect = e.currentTarget.getBoundingClientRect() mouseX.set(e.clientX - rect.left) mouseY.set(e.clientY - rect.top) }, [mouseX, mouseY] )
useEffect(() => { reset() }, [reset])
useEffect(() => { const handleGlobalPointerOut = (e: PointerEvent) => { if (!e.relatedTarget) { reset() } }
const handleVisibility = () => {
if (document.visibilityState !== "visible") {
reset()
}
}
window.addEventListener("pointerout", handleGlobalPointerOut)
window.addEventListener("blur", reset)
document.addEventListener("visibilitychange", handleVisibility)
return () => {
window.removeEventListener("pointerout", handleGlobalPointerOut)
window.removeEventListener("blur", reset)
document.removeEventListener("visibilitychange", handleVisibility)
}
}, [reset])
return (
<div
className={cn("group relative rounded-[inherit]", className)}
onPointerMove={handlePointerMove}
onPointerLeave={reset}
onPointerEnter={reset}
>
<motion.div
className="bg-border pointer-events-none absolute inset-0 rounded-[inherit] duration-300 group-hover:opacity-100"
style={{
background: useMotionTemplate radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientFrom}, ${gradientTo}, var(--border) 100% ) ,
}}
/>
<motion.div
className="pointer-events-none absolute inset-px rounded-[inherit] opacity-0 transition-opacity duration-300 group-hover:opacity-100"
style={{
background: useMotionTemplate radial-gradient(${gradientSize}px circle at ${mouseX}px ${mouseY}px, ${gradientColor}, transparent 100%) ,
opacity: gradientOpacity,
}}
/>
{children}
)
}
===== EXAMPLE: magic-card-demo ===== Title: Magic Card Demo
--- file: example/magic-card-demo.tsx --- "use client"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { MagicCard } from "@/registry/magicui/magic-card"
export default function MagicCardDemo() { const { theme } = useTheme() return ( <MagicCard gradientColor={theme === "dark" ? "#262626" : "#D9D9D955"} className="p-0" > Login Enter your credentials to access your account Email Password Sign In ) }
===== COMPONENT: marquee ===== Title: Marquee Description: An infinite scrolling component that can be used to display text, images, or videos.
--- file: magicui/marquee.tsx --- import { ComponentPropsWithoutRef } from "react"
import { cn } from "@/lib/utils"
interface MarqueeProps extends ComponentPropsWithoutRef<"div"> { /**
- Optional CSS class name to apply custom styles / className?: string /*
- Whether to reverse the animation direction
- @default false / reverse?: boolean /*
- Whether to pause the animation on hover
- @default false / pauseOnHover?: boolean /*
- Content to be displayed in the marquee / children: React.ReactNode /*
- Whether to animate vertically instead of horizontally
- @default false / vertical?: boolean /*
- Number of times to repeat the content
- @default 4 */ repeat?: number }
export function Marquee({ className, reverse = false, pauseOnHover = false, children, vertical = false, repeat = 4, ...props }: MarqueeProps) { return ( <div {...props} className={cn( "group flex [gap:var(--gap)] overflow-hidden p-2 [--duration:40s] [--gap:1rem]", { "flex-row": !vertical, "flex-col": vertical, }, className )} > {Array(repeat) .fill(0) .map((_, i) => ( <div key={i} className={cn("flex shrink-0 justify-around [gap:var(--gap)]", { "animate-marquee flex-row": !vertical, "animate-marquee-vertical flex-col": vertical, "group-hover:[animation-play-state:paused]": pauseOnHover, "[animation-direction:reverse]": reverse, })} > {children} ))} ) }
===== EXAMPLE: marquee-demo ===== Title: Marquee Demo
--- file: example/marquee-demo.tsx --- import { cn } from "@/lib/utils" import { Marquee } from "@/registry/magicui/marquee"
const reviews = [ { name: "Jack", username: "@jack", body: "I've never seen anything like this before. It's amazing. I love it.", img: "https://avatar.vercel.sh/jack", }, { name: "Jill", username: "@jill", body: "I don't know what to say. I'm speechless. This is amazing.", img: "https://avatar.vercel.sh/jill", }, { name: "John", username: "@john", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/john", }, { name: "Jane", username: "@jane", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/jane", }, { name: "Jenny", username: "@jenny", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/jenny", }, { name: "James", username: "@james", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/james", }, ]
const firstRow = reviews.slice(0, reviews.length / 2) const secondRow = reviews.slice(reviews.length / 2)
const ReviewCard = ({ img, name, username, body, }: { img: string name: string username: string body: string }) => { return ( <figure className={cn( "relative h-full w-64 cursor-pointer overflow-hidden rounded-xl border p-4", // light styles "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]", // dark styles "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]" )} > {name} {username} {body} ) }
export default function MarqueeDemo() { return ( {firstRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} {secondRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} ) }
===== EXAMPLE: marquee-demo-vertical ===== Title: Marquee Vertical Demo
--- file: example/marquee-demo-vertical.tsx --- /* eslint-disable @next/next/no-img-element */ import { cn } from "@/lib/utils" import { Marquee } from "@/registry/magicui/marquee"
const reviews = [ { name: "Jack", username: "@jack", body: "I've never seen anything like this before. It's amazing. I love it.", img: "https://avatar.vercel.sh/jack", }, { name: "Jill", username: "@jill", body: "I don't know what to say. I'm speechless. This is amazing.", img: "https://avatar.vercel.sh/jill", }, { name: "John", username: "@john", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/john", }, ]
const firstRow = reviews.slice(0, reviews.length / 2) const secondRow = reviews.slice(reviews.length / 2)
const ReviewCard = ({ img, name, username, body, }: { img: string name: string username: string body: string }) => { return ( <figure className={cn( "relative h-full w-fit cursor-pointer overflow-hidden rounded-xl border p-4 sm:w-36", // light styles "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]", // dark styles "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]" )} > {name} {username} {body} ) }
export default function MarqueeDemoVertical() { return ( {firstRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} {secondRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} ) }
===== EXAMPLE: marquee-logos ===== Title: Marquee Logos
--- file: example/marquee-logos.tsx --- import { cn } from "@/lib/utils" import { Marquee } from "@/registry/magicui/marquee"
const logos = [ { name: "Microsoft", img: "https://cdn.simpleicons.org/microsoft/000/fff", }, { name: "Apple", img: "https://cdn.simpleicons.org/apple/000/fff", }, { name: "Google", img: "https://cdn.simpleicons.org/google/000/fff", }, { name: "Facebook", img: "https://cdn.simpleicons.org/facebook/000/fff", }, { name: "LinkedIn", img: "https://cdn.simpleicons.org/linkedin/000/fff", }, { name: "Twitter", img: "https://cdn.simpleicons.org/twitter/000/fff", }, ]
const Logo = ({ name, img }: { name: string; img: string }) => { return ( <div className={cn("size-12 cursor-pointer")}> ) }
export default function MarqueeLogos() { return ( {logos.map((logo, idx) => ( <Logo key={idx} {...logo} /> ))} ) }
===== EXAMPLE: marquee-3d ===== Title: Marquee 3D
--- file: example/marquee-3d.tsx --- /* eslint-disable @next/next/no-img-element */ import { cn } from "@/lib/utils" import { Marquee } from "@/registry/magicui/marquee"
const reviews = [ { name: "Jack", username: "@jack", body: "I've never seen anything like this before. It's amazing. I love it.", img: "https://avatar.vercel.sh/jack", }, { name: "Jill", username: "@jill", body: "I don't know what to say. I'm speechless. This is amazing.", img: "https://avatar.vercel.sh/jill", }, { name: "John", username: "@john", body: "I'm at a loss for words. This is amazing. I love it.", img: "https://avatar.vercel.sh/john", }, ]
const firstRow = reviews.slice(0, reviews.length / 2) const secondRow = reviews.slice(reviews.length / 2) const thirdRow = reviews.slice(0, reviews.length / 2) const fourthRow = reviews.slice(reviews.length / 2)
const ReviewCard = ({ img, name, username, body, }: { img: string name: string username: string body: string }) => { return ( <figure className={cn( "relative h-full w-fit cursor-pointer overflow-hidden rounded-xl border p-4 sm:w-36", // light styles "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]", // dark styles "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]" )} > {name} {username} {body} ) }
export default function Marquee3D() { return ( <div className="flex flex-row items-center gap-4" style={{ transform: "translateX(-100px) translateY(0px) translateZ(-100px) rotateX(20deg) rotateY(-10deg) rotateZ(20deg)", }} > {firstRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} {secondRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} {thirdRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} {fourthRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))}
<div className="from-background pointer-events-none absolute inset-x-0 top-0 h-1/4 bg-gradient-to-b"></div>
<div className="from-background pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t"></div>
<div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div>
<div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div>
</div>
) }
===== EXAMPLE: bento-demo ===== Title: Bento Demo
--- file: example/bento-demo.tsx --- import { CalendarIcon, FileTextIcon } from "@radix-ui/react-icons" import { BellIcon, Share2Icon } from "lucide-react"
import { cn } from "@/lib/utils" import { Calendar } from "@/components/ui/calendar" import AnimatedBeamMultipleOutputDemo from "@/registry/example/animated-beam-multiple-outputs" import AnimatedListDemo from "@/registry/example/animated-list-demo" import { BentoCard, BentoGrid } from "@/registry/magicui/bento-grid" import { Marquee } from "@/registry/magicui/marquee"
const files = [ { name: "bitcoin.pdf", body: "Bitcoin is a cryptocurrency invented in 2008 by an unknown person or group of people using the name Satoshi Nakamoto.", }, { name: "finances.xlsx", body: "A spreadsheet or worksheet is a file made of rows and columns that help sort data, arrange data easily, and calculate numerical data.", }, { name: "logo.svg", body: "Scalable Vector Graphics is an Extensible Markup Language-based vector image format for two-dimensional graphics with support for interactivity and animation.", }, { name: "keys.gpg", body: "GPG keys are used to encrypt and decrypt email, files, directories, and whole disk partitions and to authenticate messages.", }, { name: "seed.txt", body: "A seed phrase, seed recovery phrase or backup seed phrase is a list of words which store all the information needed to recover Bitcoin funds on-chain.", }, ]
const features = [ { Icon: FileTextIcon, name: "Save your files", description: "We automatically save your files as you type.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-1", background: ( {files.map((f, idx) => ( <figure key={idx} className={cn( "relative w-32 cursor-pointer overflow-hidden rounded-xl border p-4", "border-gray-950/[.1] bg-gray-950/[.01] hover:bg-gray-950/[.05]", "dark:border-gray-50/[.1] dark:bg-gray-50/[.10] dark:hover:bg-gray-50/[.15]", "transform-gpu blur-[1px] transition-all duration-300 ease-out hover:blur-none" )} > {f.name} {f.body} ))} ), }, { Icon: BellIcon, name: "Notifications", description: "Get notified when something happens.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: ( ), }, { Icon: Share2Icon, name: "Integrations", description: "Supports 100+ integrations and counting.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: ( ), }, { Icon: CalendarIcon, name: "Calendar", description: "Use the calendar to filter your files by date.", className: "col-span-3 lg:col-span-1", href: "#", cta: "Learn more", background: ( <Calendar mode="single" selected={new Date(2022, 4, 11, 0, 0, 0)} className="absolute top-10 right-0 origin-top scale-75 rounded-md border [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] transition-all duration-300 ease-out group-hover:scale-90" /> ), }, ]
export default function BentoDemo() { return ( {features.map((feature, idx) => ( <BentoCard key={idx} {...feature} /> ))} ) }
===== COMPONENT: meteors ===== Title: Meteors Description: A meteor shower effect.
--- file: magicui/meteors.tsx --- "use client"
import React, { useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface MeteorsProps { number?: number minDelay?: number maxDelay?: number minDuration?: number maxDuration?: number angle?: number className?: string }
export const Meteors = ({ number = 20, minDelay = 0.2, maxDelay = 1.2, minDuration = 2, maxDuration = 10, angle = 215, className, }: MeteorsProps) => { const [meteorStyles, setMeteorStyles] = useState<Array<React.CSSProperties>>( [] )
useEffect(() => {
const styles = [...new Array(number)].map(() => ({
"--angle": -angle + "deg",
top: "-5%",
left: calc(0% + ${Math.floor(Math.random() * window.innerWidth)}px),
animationDelay: Math.random() * (maxDelay - minDelay) + minDelay + "s",
animationDuration:
Math.floor(Math.random() * (maxDuration - minDuration) + minDuration) +
"s",
}))
setMeteorStyles(styles)
}, [number, minDelay, maxDelay, minDuration, maxDuration, angle])
return ( <> {[...meteorStyles].map((style, idx) => ( // Meteor Head <span key={idx} style={{ ...style }} className={cn( "animate-meteor pointer-events-none absolute size-0.5 rotate-[var(--angle)] rounded-full bg-zinc-500 shadow-[0_0_0_1px_#ffffff10]", className )} > {/* Meteor Tail */} ))} </> ) }
===== EXAMPLE: meteors-demo ===== Title: Meteors Demo
--- file: example/meteors-demo.tsx --- import { Meteors } from "@/registry/magicui/meteors"
export default function MeteorDemo() { return ( Meteors ) }
===== COMPONENT: morphing-text ===== Title: Morphing Text Description: A dynamic text morphing component for Magic UI.
--- file: magicui/morphing-text.tsx --- "use client"
import { useCallback, useEffect, useRef } from "react"
import { cn } from "@/lib/utils"
const morphTime = 1.5 const cooldownTime = 0.5
const useMorphingText = (texts: string[]) => { const textIndexRef = useRef(0) const morphRef = useRef(0) const cooldownRef = useRef(0) const timeRef = useRef(new Date())
const text1Ref = useRef(null) const text2Ref = useRef(null)
const setStyles = useCallback( (fraction: number) => { const [current1, current2] = [text1Ref.current, text2Ref.current] if (!current1 || !current2) return
current2.style.filter = `blur(${Math.min(8 / fraction - 8, 100)}px)`
current2.style.opacity = `${Math.pow(fraction, 0.4) * 100}%`
const invertedFraction = 1 - fraction
current1.style.filter = `blur(${Math.min(
8 / invertedFraction - 8,
100
)}px)`
current1.style.opacity = `${Math.pow(invertedFraction, 0.4) * 100}%`
current1.textContent = texts[textIndexRef.current % texts.length]
current2.textContent = texts[(textIndexRef.current + 1) % texts.length]
},
[texts]
)
const doMorph = useCallback(() => { morphRef.current -= cooldownRef.current cooldownRef.current = 0
let fraction = morphRef.current / morphTime
if (fraction > 1) {
cooldownRef.current = cooldownTime
fraction = 1
}
setStyles(fraction)
if (fraction === 1) {
textIndexRef.current++
}
}, [setStyles])
const doCooldown = useCallback(() => { morphRef.current = 0 const [current1, current2] = [text1Ref.current, text2Ref.current] if (current1 && current2) { current2.style.filter = "none" current2.style.opacity = "100%" current1.style.filter = "none" current1.style.opacity = "0%" } }, [])
useEffect(() => { let animationFrameId: number
const animate = () => {
animationFrameId = requestAnimationFrame(animate)
const newTime = new Date()
const dt = (newTime.getTime() - timeRef.current.getTime()) / 1000
timeRef.current = newTime
cooldownRef.current -= dt
if (cooldownRef.current <= 0) doMorph()
else doCooldown()
}
animate()
return () => {
cancelAnimationFrame(animationFrameId)
}
}, [doMorph, doCooldown])
return { text1Ref, text2Ref } }
interface MorphingTextProps { className?: string texts: string[] }
const Texts: React.FC<Pick<MorphingTextProps, "texts">> = ({ texts }) => { const { text1Ref, text2Ref } = useMorphingText(texts) return ( <> </> ) }
const SvgFilters: React.FC = () => ( <svg id="filters" className="fixed h-0 w-0" preserveAspectRatio="xMidYMid slice"
<defs>
<filter id="threshold">
<feColorMatrix
in="SourceGraphic"
type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 255 -140"
/>
</filter>
</defs>
export const MorphingText: React.FC = ({ texts, className, }) => (
===== EXAMPLE: morphing-text-demo ===== Title: Morphing Text Demo
--- file: example/morphing-text-demo.tsx --- import { MorphingText } from "@/registry/magicui/morphing-text"
const texts = [ "Hello", "Morphing", "Text", "Animation", "React", "Component", "Smooth", "Transition", "Engaging", ]
export default function MorphingTextDemo() { return }
===== COMPONENT: neon-gradient-card ===== Title: Neon Gradient Card Description: A beautiful neon card effect
--- file: magicui/neon-gradient-card.tsx --- "use client"
import { CSSProperties, ReactElement, ReactNode, useEffect, useRef, useState, } from "react"
import { cn } from "@/lib/utils"
interface NeonColorsProps { firstColor: string secondColor: string }
interface NeonGradientCardProps extends React.HTMLAttributes { /**
- @default
- @type ReactElement
- @description
- The component to be rendered as the card
- / as?: ReactElement /*
- @default ""
- @type string
- @description
- The className of the card */ className?: string
/**
- @default ""
- @type ReactNode
- @description
- The children of the card
- */ children?: ReactNode
/**
- @default 5
- @type number
- @description
- The size of the border in pixels
- */ borderSize?: number
/**
- @default 20
- @type number
- @description
- The size of the radius in pixels
- */ borderRadius?: number
/**
- @default "{ firstColor: '#ff00aa', secondColor: '#00FFF1' }"
- @type string
- @description
- The colors of the neon gradient
- */ neonColors?: NeonColorsProps }
export const NeonGradientCard: React.FC = ({ className, children, borderSize = 2, borderRadius = 20, neonColors = { firstColor: "#ff00aa", secondColor: "#00FFF1", }, ...props }) => { const containerRef = useRef(null) const [dimensions, setDimensions] = useState({ width: 0, height: 0 })
useEffect(() => { const updateDimensions = () => { if (containerRef.current) { const { offsetWidth, offsetHeight } = containerRef.current setDimensions({ width: offsetWidth, height: offsetHeight }) } }
updateDimensions()
window.addEventListener("resize", updateDimensions)
return () => {
window.removeEventListener("resize", updateDimensions)
}
}, [])
useEffect(() => { if (containerRef.current) { const { offsetWidth, offsetHeight } = containerRef.current setDimensions({ width: offsetWidth, height: offsetHeight }) } }, [children])
return (
<div
ref={containerRef}
style={
{
"--border-size": ${borderSize}px,
"--border-radius": ${borderRadius}px,
"--neon-first-color": neonColors.firstColor,
"--neon-second-color": neonColors.secondColor,
"--card-width": ${dimensions.width}px,
"--card-height": ${dimensions.height}px,
"--card-content-radius": ${borderRadius - borderSize}px,
"--pseudo-element-background-image": linear-gradient(0deg, ${neonColors.firstColor}, ${neonColors.secondColor}),
"--pseudo-element-width": ${dimensions.width + borderSize * 2}px,
"--pseudo-element-height": ${dimensions.height + borderSize * 2}px,
"--after-blur": ${dimensions.width / 3}px,
} as CSSProperties
}
className={cn(
"relative z-10 size-full rounded-[var(--border-radius)]",
className
)}
{...props}
>
<div
className={cn(
"relative size-full min-h-[inherit] rounded-[var(--card-content-radius)] bg-gray-100 p-6",
"before:absolute before:-top-[var(--border-size)] before:-left-[var(--border-size)] before:-z-10 before:block",
"before:h-[var(--pseudo-element-height)] before:w-[var(--pseudo-element-width)] before:rounded-[var(--border-radius)] before:content-['']",
"before:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] before:bg-[length:100%_200%]",
"before:animate-background-position-spin",
"after:absolute after:-top-[var(--border-size)] after:-left-[var(--border-size)] after:-z-10 after:block",
"after:h-[var(--pseudo-element-height)] after:w-[var(--pseudo-element-width)] after:rounded-[var(--border-radius)] after:blur-[var(--after-blur)] after:content-['']",
"after:bg-[linear-gradient(0deg,var(--neon-first-color),var(--neon-second-color))] after:bg-[length:100%_200%] after:opacity-80",
"after:animate-background-position-spin",
"dark:bg-neutral-900",
"break-words"
)}
>
{children}
)
}
===== EXAMPLE: neon-gradient-card-demo ===== Title: Neon Gradient Card Demo
--- file: example/neon-gradient-card-demo.tsx --- import { NeonGradientCard } from "@/registry/magicui/neon-gradient-card"
export default function NeonGradientCardDemo() { return ( Neon Gradient Card ) }
===== COMPONENT: number-ticker ===== Title: Number Ticker Description: Animate numbers to count up or down to a target number
--- file: magicui/number-ticker.tsx --- "use client"
import { ComponentPropsWithoutRef, useEffect, useRef } from "react" import { useInView, useMotionValue, useSpring } from "motion/react"
import { cn } from "@/lib/utils"
interface NumberTickerProps extends ComponentPropsWithoutRef<"span"> { value: number startValue?: number direction?: "up" | "down" delay?: number decimalPlaces?: number }
export function NumberTicker({ value, startValue = 0, direction = "up", delay = 0, className, decimalPlaces = 0, ...props }: NumberTickerProps) { const ref = useRef(null) const motionValue = useMotionValue(direction === "down" ? value : startValue) const springValue = useSpring(motionValue, { damping: 60, stiffness: 100, }) const isInView = useInView(ref, { once: true, margin: "0px" })
useEffect(() => { if (isInView) { const timer = setTimeout(() => { motionValue.set(direction === "down" ? startValue : value) }, delay * 1000) return () => clearTimeout(timer) } }, [motionValue, isInView, delay, value, direction, startValue])
useEffect( () => springValue.on("change", (latest) => { if (ref.current) { ref.current.textContent = Intl.NumberFormat("en-US", { minimumFractionDigits: decimalPlaces, maximumFractionDigits: decimalPlaces, }).format(Number(latest.toFixed(decimalPlaces))) } }), [springValue, decimalPlaces] )
return ( <span ref={ref} className={cn( "inline-block tracking-wider text-black tabular-nums dark:text-white", className )} {...props} > {startValue} ) }
===== EXAMPLE: number-ticker-demo ===== Title: Number Ticker Demo
--- file: example/number-ticker-demo.tsx --- import { NumberTicker } from "@/registry/magicui/number-ticker"
export default function NumberTickerDemo() { return ( ) }
===== EXAMPLE: number-ticker-demo-2 ===== Title: Number Ticker Demo 2
--- file: example/number-ticker-demo-2.tsx --- import { NumberTicker } from "@/registry/magicui/number-ticker"
export default function NumberTickerDemo() { return ( ) }
===== EXAMPLE: number-ticker-decimal-demo ===== Title: Number Ticker Decimal Demo
--- file: example/number-ticker-decimal-demo.tsx --- import { NumberTicker } from "@/registry/magicui/number-ticker"
export default function NumberTickerDemo() { return ( ) }
===== COMPONENT: orbiting-circles ===== Title: Orbiting Circles Description: A collection of circles which move in orbit along a circular path
--- file: magicui/orbiting-circles.tsx --- import React from "react"
import { cn } from "@/lib/utils"
export interface OrbitingCirclesProps extends React.HTMLAttributes { className?: string children?: React.ReactNode reverse?: boolean duration?: number delay?: number radius?: number path?: boolean iconSize?: number speed?: number }
export function OrbitingCircles({
className,
children,
reverse,
duration = 20,
radius = 160,
path = true,
iconSize = 30,
speed = 1,
...props
}: OrbitingCirclesProps) {
const calculatedDuration = duration / speed
return (
<>
{path && (
)}
{React.Children.map(children, (child, index) => {
const angle = (360 / React.Children.count(children)) * index
return (
<div
style={
{
"--duration": calculatedDuration,
"--radius": radius,
"--angle": angle,
"--icon-size": ${iconSize}px,
} as React.CSSProperties
}
className={cn(
animate-orbit absolute flex size-[var(--icon-size)] transform-gpu items-center justify-center rounded-full,
{ "[animation-direction:reverse]": reverse },
className
)}
{...props}
>
{child}
)
})}
</>
)
}
===== EXAMPLE: orbiting-circles-demo ===== Title: Orbiting Circles Demo
--- file: example/orbiting-circles-demo.tsx --- import { OrbitingCircles } from "@/registry/magicui/orbiting-circles"
export default function OrbitingCirclesDemo() { return ( <Icons.whatsapp /> <Icons.notion /> <Icons.openai /> <Icons.googleDrive /> <Icons.whatsapp /> <Icons.whatsapp /> <Icons.notion /> <Icons.openai /> <Icons.googleDrive /> ) }
const Icons = { gitHub: () => ( ), notion: () => ( ), openai: () => ( ), googleDrive: () => ( ), whatsapp: () => ( ), }
===== COMPONENT: particles ===== Title: Particles Description: Particles are a fun way to add some visual flair to your website. They can be used to create a sense of depth, movement, and interactivity.
--- file: magicui/particles.tsx --- "use client"
import React, { ComponentPropsWithoutRef, useEffect, useRef, useState, } from "react"
import { cn } from "@/lib/utils"
interface MousePosition { x: number y: number }
function MousePosition(): MousePosition { const [mousePosition, setMousePosition] = useState({ x: 0, y: 0, })
useEffect(() => { const handleMouseMove = (event: MouseEvent) => { setMousePosition({ x: event.clientX, y: event.clientY }) }
window.addEventListener("mousemove", handleMouseMove)
return () => {
window.removeEventListener("mousemove", handleMouseMove)
}
}, [])
return mousePosition }
interface ParticlesProps extends ComponentPropsWithoutRef<"div"> { className?: string quantity?: number staticity?: number ease?: number size?: number refresh?: boolean color?: string vx?: number vy?: number }
function hexToRgb(hex: string): number[] { hex = hex.replace("#", "")
if (hex.length === 3) { hex = hex .split("") .map((char) => char + char) .join("") }
const hexInt = parseInt(hex, 16) const red = (hexInt >> 16) & 255 const green = (hexInt >> 8) & 255 const blue = hexInt & 255 return [red, green, blue] }
type Circle = { x: number y: number translateX: number translateY: number size: number alpha: number targetAlpha: number dx: number dy: number magnetism: number }
export const Particles: React.FC = ({ className = "", quantity = 100, staticity = 50, ease = 50, size = 0.4, refresh = false, color = "#ffffff", vx = 0, vy = 0, ...props }) => { const canvasRef = useRef(null) const canvasContainerRef = useRef(null) const context = useRef<CanvasRenderingContext2D | null>(null) const circles = useRef<Circle[]>([]) const mousePosition = MousePosition() const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) const canvasSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 }) const dpr = typeof window !== "undefined" ? window.devicePixelRatio : 1 const rafID = useRef<number | null>(null) const resizeTimeout = useRef<NodeJS.Timeout | null>(null)
useEffect(() => { if (canvasRef.current) { context.current = canvasRef.current.getContext("2d") } initCanvas() animate()
const handleResize = () => {
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current)
}
resizeTimeout.current = setTimeout(() => {
initCanvas()
}, 200)
}
window.addEventListener("resize", handleResize)
return () => {
if (rafID.current != null) {
window.cancelAnimationFrame(rafID.current)
}
if (resizeTimeout.current) {
clearTimeout(resizeTimeout.current)
}
window.removeEventListener("resize", handleResize)
}
}, [color])
useEffect(() => { onMouseMove() }, [mousePosition.x, mousePosition.y])
useEffect(() => { initCanvas() }, [refresh])
const initCanvas = () => { resizeCanvas() drawParticles() }
const onMouseMove = () => { if (canvasRef.current) { const rect = canvasRef.current.getBoundingClientRect() const { w, h } = canvasSize.current const x = mousePosition.x - rect.left - w / 2 const y = mousePosition.y - rect.top - h / 2 const inside = x < w / 2 && x > -w / 2 && y < h / 2 && y > -h / 2 if (inside) { mouse.current.x = x mouse.current.y = y } } }
const resizeCanvas = () => { if (canvasContainerRef.current && canvasRef.current && context.current) { canvasSize.current.w = canvasContainerRef.current.offsetWidth canvasSize.current.h = canvasContainerRef.current.offsetHeight
canvasRef.current.width = canvasSize.current.w * dpr
canvasRef.current.height = canvasSize.current.h * dpr
canvasRef.current.style.width = `${canvasSize.current.w}px`
canvasRef.current.style.height = `${canvasSize.current.h}px`
context.current.scale(dpr, dpr)
// Clear existing particles and create new ones with exact quantity
circles.current = []
for (let i = 0; i < quantity; i++) {
const circle = circleParams()
drawCircle(circle)
}
}
}
const circleParams = (): Circle => { const x = Math.floor(Math.random() * canvasSize.current.w) const y = Math.floor(Math.random() * canvasSize.current.h) const translateX = 0 const translateY = 0 const pSize = Math.floor(Math.random() * 2) + size const alpha = 0 const targetAlpha = parseFloat((Math.random() * 0.6 + 0.1).toFixed(1)) const dx = (Math.random() - 0.5) * 0.1 const dy = (Math.random() - 0.5) * 0.1 const magnetism = 0.1 + Math.random() * 4 return { x, y, translateX, translateY, size: pSize, alpha, targetAlpha, dx, dy, magnetism, } }
const rgb = hexToRgb(color)
const drawCircle = (circle: Circle, update = false) => {
if (context.current) {
const { x, y, translateX, translateY, size, alpha } = circle
context.current.translate(translateX, translateY)
context.current.beginPath()
context.current.arc(x, y, size, 0, 2 * Math.PI)
context.current.fillStyle = rgba(${rgb.join(", ")}, ${alpha})
context.current.fill()
context.current.setTransform(dpr, 0, 0, dpr, 0, 0)
if (!update) {
circles.current.push(circle)
}
}
}
const clearContext = () => { if (context.current) { context.current.clearRect( 0, 0, canvasSize.current.w, canvasSize.current.h ) } }
const drawParticles = () => { clearContext() const particleCount = quantity for (let i = 0; i < particleCount; i++) { const circle = circleParams() drawCircle(circle) } }
const remapValue = ( value: number, start1: number, end1: number, start2: number, end2: number ): number => { const remapped = ((value - start1) * (end2 - start2)) / (end1 - start1) + start2 return remapped > 0 ? remapped : 0 }
const animate = () => { clearContext() circles.current.forEach((circle: Circle, i: number) => { // Handle the alpha value const edge = [ circle.x + circle.translateX - circle.size, // distance from left edge canvasSize.current.w - circle.x - circle.translateX - circle.size, // distance from right edge circle.y + circle.translateY - circle.size, // distance from top edge canvasSize.current.h - circle.y - circle.translateY - circle.size, // distance from bottom edge ] const closestEdge = edge.reduce((a, b) => Math.min(a, b)) const remapClosestEdge = parseFloat( remapValue(closestEdge, 0, 20, 0, 1).toFixed(2) ) if (remapClosestEdge > 1) { circle.alpha += 0.02 if (circle.alpha > circle.targetAlpha) { circle.alpha = circle.targetAlpha } } else { circle.alpha = circle.targetAlpha * remapClosestEdge } circle.x += circle.dx + vx circle.y += circle.dy + vy circle.translateX += (mouse.current.x / (staticity / circle.magnetism) - circle.translateX) / ease circle.translateY += (mouse.current.y / (staticity / circle.magnetism) - circle.translateY) / ease
drawCircle(circle, true)
// circle gets out of the canvas
if (
circle.x < -circle.size ||
circle.x > canvasSize.current.w + circle.size ||
circle.y < -circle.size ||
circle.y > canvasSize.current.h + circle.size
) {
// remove the circle from the array
circles.current.splice(i, 1)
// create a new circle
const newCircle = circleParams()
drawCircle(newCircle)
}
})
rafID.current = window.requestAnimationFrame(animate)
}
return ( <div className={cn("pointer-events-none", className)} ref={canvasContainerRef} aria-hidden="true" {...props} > ) }
===== EXAMPLE: particles-demo ===== Title: Particles Demo
--- file: example/particles-demo.tsx --- "use client"
import { useEffect, useState } from "react" import { useTheme } from "next-themes"
import { Particles } from "@/registry/magicui/particles"
export default function ParticlesDemo() { const { resolvedTheme } = useTheme() const [color, setColor] = useState("#ffffff")
useEffect(() => { setColor(resolvedTheme === "dark" ? "#ffffff" : "#000000") }, [resolvedTheme])
return ( Particles ) }
===== COMPONENT: pixel-image ===== Title: Pixel Image Description: A component that displays an image with a pixelated effect, creating a retro aesthetic.
--- file: magicui/pixel-image.tsx --- "use client"
import { useEffect, useMemo, useState } from "react"
import { cn } from "@/lib/utils"
type Grid = { rows: number cols: number }
const DEFAULT_GRIDS: Record<string, Grid> = { "6x4": { rows: 4, cols: 6 }, "8x8": { rows: 8, cols: 8 }, "8x3": { rows: 3, cols: 8 }, "4x6": { rows: 6, cols: 4 }, "3x8": { rows: 8, cols: 3 }, }
type PredefinedGridKey = keyof typeof DEFAULT_GRIDS
interface PixelImageProps { src: string grid?: PredefinedGridKey customGrid?: Grid grayscaleAnimation?: boolean pixelFadeInDuration?: number // in ms maxAnimationDelay?: number // in ms colorRevealDelay?: number // in ms }
export const PixelImage = ({ src, grid = "6x4", grayscaleAnimation = true, pixelFadeInDuration = 1000, maxAnimationDelay = 1200, colorRevealDelay = 1300, customGrid, }: PixelImageProps) => { const [isVisible, setIsVisible] = useState(false) const [showColor, setShowColor] = useState(false)
const MIN_GRID = 1 const MAX_GRID = 16
const { rows, cols } = useMemo(() => { const isValidGrid = (grid?: Grid) => { if (!grid) return false const { rows, cols } = grid return ( Number.isInteger(rows) && Number.isInteger(cols) && rows >= MIN_GRID && cols >= MIN_GRID && rows <= MAX_GRID && cols <= MAX_GRID ) }
return isValidGrid(customGrid) ? customGrid! : DEFAULT_GRIDS[grid]
}, [customGrid, grid])
useEffect(() => { setIsVisible(true) const colorTimeout = setTimeout(() => { setShowColor(true) }, colorRevealDelay) return () => clearTimeout(colorTimeout) }, [colorRevealDelay])
const pieces = useMemo(() => { const total = rows * cols return Array.from({ length: total }, (_, index) => { const row = Math.floor(index / cols) const col = index % cols
const clipPath = `polygon(
${col * (100 / cols)}% ${row * (100 / rows)}%,
${(col + 1) * (100 / cols)}% ${row * (100 / rows)}%,
${(col + 1) * (100 / cols)}% ${(row + 1) * (100 / rows)}%,
${col * (100 / cols)}% ${(row + 1) * (100 / rows)}%
)`
const delay = Math.random() * maxAnimationDelay
return {
clipPath,
delay,
}
})
}, [rows, cols, maxAnimationDelay])
return (
{pieces.map((piece, index) => (
<div
key={index}
className={cn(
"absolute inset-0 transition-all ease-out",
isVisible ? "opacity-100" : "opacity-0"
)}
style={{
clipPath: piece.clipPath,
transitionDelay: ${piece.delay}ms,
transitionDuration: ${pixelFadeInDuration}ms,
}}
>
<img
src={src}
alt={Pixel image piece ${index + 1}}
className={cn(
"z-1 rounded-[2.5rem] object-cover",
grayscaleAnimation && (showColor ? "grayscale-0" : "grayscale")
)}
style={{
transition: grayscaleAnimation
? filter ${pixelFadeInDuration}ms cubic-bezier(0.4, 0, 0.2, 1)
: "none",
}}
draggable={false}
/>
))}
)
}
===== EXAMPLE: pixel-image-demo ===== Title: Pixel Image Demo
--- file: example/pixel-image-demo.tsx --- import { PixelImage } from "@/registry/magicui/pixel-image"
export default function Home() { return ( <PixelImage src="/pixel-image-demo.jpg" customGrid={{ rows: 4, cols: 6 }} grayscaleAnimation /> ) }
===== COMPONENT: pointer ===== Title: Pointer Description: A component that displays a pointer when hovering over an element
--- file: magicui/pointer.tsx --- "use client"
import { useEffect, useRef, useState } from "react" import { AnimatePresence, HTMLMotionProps, motion, useMotionValue, } from "motion/react"
import { cn } from "@/lib/utils"
/**
- A custom pointer component that displays an animated cursor.
- Add this as a child to any component to enable a custom pointer when hovering.
- You can pass custom children to render as the pointer.
- @component
- @param {HTMLMotionProps<"div">} props - The component props */ export function Pointer({ className, style, children, ...props }: HTMLMotionProps<"div">): React.ReactNode { const x = useMotionValue(0) const y = useMotionValue(0) const [isActive, setIsActive] = useState(false) const containerRef = useRef(null)
useEffect(() => { if (typeof window !== "undefined" && containerRef.current) { // Get the parent element directly from the ref const parentElement = containerRef.current.parentElement
if (parentElement) {
// Add cursor-none to parent
parentElement.style.cursor = "none"
// Add event listeners to parent
const handleMouseMove = (e: MouseEvent) => {
x.set(e.clientX)
y.set(e.clientY)
setIsActive(true)
}
const handleMouseEnter = (e: MouseEvent) => {
x.set(e.clientX)
y.set(e.clientY)
setIsActive(true)
}
const handleMouseLeave = () => {
setIsActive(false)
}
parentElement.addEventListener("mousemove", handleMouseMove)
parentElement.addEventListener("mouseenter", handleMouseEnter)
parentElement.addEventListener("mouseleave", handleMouseLeave)
return () => {
parentElement.style.cursor = ""
parentElement.removeEventListener("mousemove", handleMouseMove)
parentElement.removeEventListener("mouseenter", handleMouseEnter)
parentElement.removeEventListener("mouseleave", handleMouseLeave)
}
}
}
}, [x, y])
return ( <> {isActive && ( <motion.div className="pointer-events-none fixed z-50 transform-[translate(-50%,-50%)]" style={{ top: y, left: x, ...style, }} initial={{ scale: 0, opacity: 0, }} animate={{ scale: 1, opacity: 1, }} exit={{ scale: 0, opacity: 0, }} {...props} > {children || ( <svg stroke="currentColor" fill="currentColor" strokeWidth="1" viewBox="0 0 16 16" height="24" width="24" xmlns="http://www.w3.org/2000/svg" className={cn( "rotate-[-70deg] stroke-white text-black", className )} > )} </motion.div> )} </> ) }
===== EXAMPLE: pointer-demo-1 ===== Title: Pointer Demo 1
--- file: example/pointer-demo-1.tsx --- "use client"
import { motion } from "motion/react"
import { Pointer } from "@/registry/magicui/pointer"
export default function PointerDemo1() { return ( Animated Pointer Animated pointer <motion.div animate={{ scale: [0.8, 1, 0.8], rotate: [0, 5, -5, 0], }} transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut", }} > <motion.path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" fill="currentColor" animate={{ scale: [1, 1.2, 1] }} transition={{ duration: 0.8, repeat: Infinity, ease: "easeInOut", }} /> </motion.div>
<div className="border-border rounded-lg border p-4">
<div className="relative flex h-40 flex-col items-center justify-center">
<h3 className="text-xl font-semibold">Colored Pointer</h3>
<p className="text-muted-foreground text-sm">
A custom pointer with different color
</p>
</div>
<Pointer className="fill-blue-500" />
</div>
<div className="border-border rounded-lg border p-4">
<div className="relative flex h-40 flex-col items-center justify-center">
<h3 className="text-xl font-semibold">Custom Shape</h3>
<p className="text-muted-foreground text-sm">
A pointer with a custom SVG shape
</p>
</div>
<Pointer>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="12" cy="12" r="10" className="fill-purple-500" />
<circle cx="12" cy="12" r="5" className="fill-white" />
</svg>
</Pointer>
</div>
<div className="border-border rounded-lg border p-4">
<div className="relative flex h-40 flex-col items-center justify-center">
<h3 className="text-xl font-semibold">Emoji Pointer</h3>
<p className="text-muted-foreground text-sm">
Using an emoji as a custom pointer
</p>
</div>
<Pointer>
<div className="text-2xl">👆</div>
</Pointer>
</div>
</div>
) }
===== COMPONENT: progressive-blur ===== Title: Progressive Blur Description: The Progressive Blur component adds a smooth blur gradient effect to scrollable content, indicating more content below or above.
--- file: magicui/progressive-blur.tsx --- "use client"
import React from "react"
import { cn } from "@/lib/utils"
export interface ProgressiveBlurProps { className?: string height?: string position?: "top" | "bottom" | "both" blurLevels?: number[] children?: React.ReactNode }
export function ProgressiveBlur({ className, height = "30%", position = "bottom", blurLevels = [0.5, 1, 2, 4, 8, 16, 32, 64], }: ProgressiveBlurProps) { // Create array with length equal to blurLevels.length - 2 (for before/after pseudo elements) const divElements = Array(blurLevels.length - 2).fill(null)
return (
<div
className={cn(
"gradient-blur pointer-events-none absolute inset-x-0 z-10",
className,
position === "top"
? "top-0"
: position === "bottom"
? "bottom-0"
: "inset-y-0"
)}
style={{
height: position === "both" ? "100%" : height,
}}
>
{/* First blur layer (pseudo element) */}
<div
className="absolute inset-0"
style={{
zIndex: 1,
backdropFilter: blur(${blurLevels[0]}px),
WebkitBackdropFilter: blur(${blurLevels[0]}px),
maskImage:
position === "bottom"
? linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)
: position === "top"
? linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)
: linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%),
WebkitMaskImage:
position === "bottom"
? linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)
: position === "top"
? linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25%, rgba(0,0,0,0) 37.5%)
: linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%),
}}
/>
{/* Middle blur layers */}
{divElements.map((_, index) => {
const blurIndex = index + 1
const startPercent = blurIndex * 12.5
const midPercent = (blurIndex + 1) * 12.5
const endPercent = (blurIndex + 2) * 12.5
const maskGradient =
position === "bottom"
? `linear-gradient(to bottom, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
: position === "top"
? `linear-gradient(to top, rgba(0,0,0,0) ${startPercent}%, rgba(0,0,0,1) ${midPercent}%, rgba(0,0,0,1) ${endPercent}%, rgba(0,0,0,0) ${endPercent + 12.5}%)`
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`
return (
<div
key={`blur-${index}`}
className="absolute inset-0"
style={{
zIndex: index + 2,
backdropFilter: `blur(${blurLevels[blurIndex]}px)`,
WebkitBackdropFilter: `blur(${blurLevels[blurIndex]}px)`,
maskImage: maskGradient,
WebkitMaskImage: maskGradient,
}}
/>
)
})}
{/* Last blur layer (pseudo element) */}
<div
className="absolute inset-0"
style={{
zIndex: blurLevels.length,
backdropFilter: `blur(${blurLevels[blurLevels.length - 1]}px)`,
WebkitBackdropFilter: `blur(${blurLevels[blurLevels.length - 1]}px)`,
maskImage:
position === "bottom"
? `linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
: position === "top"
? `linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
WebkitMaskImage:
position === "bottom"
? `linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
: position === "top"
? `linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%)`
: `linear-gradient(rgba(0,0,0,0) 0%, rgba(0,0,0,1) 5%, rgba(0,0,0,1) 95%, rgba(0,0,0,0) 100%)`,
}}
/>
</div>
) }
===== EXAMPLE: progressive-blur-demo ===== Title: Progressive Blur Demo
--- file: example/progressive-blur-demo.tsx --- "use client"
import { ScrollArea } from "@/components/ui/scroll-area" import { ProgressiveBlur } from "@/registry/magicui/progressive-blur"
export default function ProgressiveBlurDemo() { return ( {Array.from({ length: 20 }).map((_, index) => ( {index} ))} ) }
===== COMPONENT: pulsating-button ===== Title: Pulsating Button Description: An animated pulsating button useful for capturing attention of users.
--- file: magicui/pulsating-button.tsx --- import React from "react"
import { cn } from "@/lib/utils"
interface PulsatingButtonProps extends React.ButtonHTMLAttributes { pulseColor?: string duration?: string }
export const PulsatingButton = React.forwardRef< HTMLButtonElement, PulsatingButtonProps
( ( { className, children, pulseColor = "#808080", duration = "1.5s", ...props }, ref ) => { return ( <button ref={ref} className={cn( "bg-primary text-primary-foreground relative flex cursor-pointer items-center justify-center rounded-lg px-4 py-2 text-center", className )} style={ { "--pulse-color": pulseColor, "--duration": duration, } as React.CSSProperties } {...props} > {children} ) } )
PulsatingButton.displayName = "PulsatingButton"
===== EXAMPLE: pulsating-button-demo ===== Title: Pulsating Button Demo
--- file: example/pulsating-button-demo.tsx --- import { PulsatingButton } from "@/registry/magicui/pulsating-button"
export default function PulsatingButtonDemo() { return Join Affiliate Program }
===== COMPONENT: rainbow-button ===== Title: Rainbow Button Description: An animated button with a rainbow effect.
--- file: magicui/rainbow-button.tsx --- import React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const rainbowButtonVariants = cva( cn( "relative cursor-pointer group transition-all animate-rainbow", "inline-flex items-center justify-center gap-2 shrink-0", "rounded-sm outline-none focus-visible:ring-[3px] aria-invalid:border-destructive", "text-sm font-medium whitespace-nowrap", "disabled:pointer-events-none disabled:opacity-50", "[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0" ), { variants: { variant: { default: "border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.125rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]", outline: "border border-input border-b-transparent bg-[linear-gradient(#ffffff,#ffffff),linear-gradient(#ffffff_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-accent-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#0a0a0a,#0a0a0a),linear-gradient(#0a0a0a_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]", }, size: { default: "h-9 px-4 py-2", sm: "h-8 rounded-xl px-3 text-xs", lg: "h-11 rounded-xl px-8", icon: "size-9", }, }, defaultVariants: { variant: "default", size: "default", }, } )
interface RainbowButtonProps extends React.ButtonHTMLAttributes, VariantProps { asChild?: boolean }
const RainbowButton = React.forwardRef<HTMLButtonElement, RainbowButtonProps>( ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : "button" return ( <Comp data-slot="button" className={cn(rainbowButtonVariants({ variant, size, className }))} ref={ref} {...props} /> ) } )
RainbowButton.displayName = "RainbowButton"
export { RainbowButton, rainbowButtonVariants, type RainbowButtonProps }
===== EXAMPLE: rainbow-button-demo ===== Title: Rainbow Button Demo
--- file: example/rainbow-button-demo.tsx --- import { RainbowButton } from "@/registry/magicui/rainbow-button"
export default function RainbowButtonDemo() { return Get Unlimited Access }
===== EXAMPLE: rainbow-button-demo-2 ===== Title: Rainbow Button Demo 2
--- file: example/rainbow-button-demo-2.tsx --- import { RainbowButton } from "@/registry/magicui/rainbow-button"
export default function RainbowButtonDemo() { return Get Unlimited Access }
===== COMPONENT: retro-grid ===== Title: Retro Grid Description: An animated scrolling retro grid effect
--- file: magicui/retro-grid.tsx --- import { cn } from "@/lib/utils"
interface RetroGridProps extends React.HTMLAttributes { /**
- Additional CSS classes to apply to the grid container / className?: string /*
- Rotation angle of the grid in degrees
- @default 65 / angle?: number /*
- Grid cell size in pixels
- @default 60 / cellSize?: number /*
- Grid opacity value between 0 and 1
- @default 0.5 / opacity?: number /*
- Grid line color in light mode
- @default "gray" / lightLineColor?: string /*
- Grid line color in dark mode
- @default "gray" */ darkLineColor?: string }
export function RetroGrid({
className,
angle = 65,
cellSize = 60,
opacity = 0.5,
lightLineColor = "gray",
darkLineColor = "gray",
...props
}: RetroGridProps) {
const gridStyles = {
"--grid-angle": ${angle}deg,
"--cell-size": ${cellSize}px,
"--opacity": opacity,
"--light-line": lightLineColor,
"--dark-line": darkLineColor,
} as React.CSSProperties
return (
<div
className={cn(
"pointer-events-none absolute size-full overflow-hidden [perspective:200px]",
opacity-[var(--opacity)],
className
)}
style={gridStyles}
{...props}
>
<div className="absolute inset-0 bg-gradient-to-t from-white to-transparent to-90% dark:from-black" />
</div>
) }
===== EXAMPLE: retro-grid-demo ===== Title: Retro Grid Demo
--- file: example/retro-grid-demo.tsx --- "use client"
import { RetroGrid } from "@/registry/magicui/retro-grid"
export default function RetroGridDemo() { return ( Retro Grid
<RetroGrid />
</div>
) }
===== COMPONENT: ripple ===== Title: Ripple Description: An animated ripple effect typically used behind elements to emphasize them.
--- file: magicui/ripple.tsx --- import React, { ComponentPropsWithoutRef, CSSProperties } from "react"
import { cn } from "@/lib/utils"
interface RippleProps extends ComponentPropsWithoutRef<"div"> { mainCircleSize?: number mainCircleOpacity?: number numCircles?: number }
export const Ripple = React.memo(function Ripple({
mainCircleSize = 210,
mainCircleOpacity = 0.24,
numCircles = 8,
className,
...props
}: RippleProps) {
return (
<div
className={cn(
"pointer-events-none absolute inset-0 [mask-image:linear-gradient(to_bottom,white,transparent)] select-none",
className
)}
{...props}
>
{Array.from({ length: numCircles }, (_, i) => {
const size = mainCircleSize + i * 70
const opacity = mainCircleOpacity - i * 0.03
const animationDelay = ${i * 0.06}s
const borderStyle = "solid"
return (
<div
key={i}
className={`animate-ripple bg-foreground/25 absolute rounded-full border shadow-xl`}
style={
{
"--i": i,
width: `${size}px`,
height: `${size}px`,
opacity,
animationDelay,
borderStyle,
borderWidth: "1px",
borderColor: `var(--foreground)`,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%) scale(1)",
} as CSSProperties
}
/>
)
})}
</div>
) })
Ripple.displayName = "Ripple"
===== EXAMPLE: ripple-demo ===== Title: Ripple Demo
--- file: example/ripple-demo.tsx --- import { Ripple } from "@/registry/magicui/ripple"
export default function RippleDemo() { return ( Ripple ) }
===== COMPONENT: ripple-button ===== Title: Ripple Button Description: An animated button with ripple useful for user engagement.
--- file: magicui/ripple-button.tsx --- "use client"
import React, { MouseEvent, useEffect, useState } from "react"
import { cn } from "@/lib/utils"
interface RippleButtonProps extends React.ButtonHTMLAttributes { rippleColor?: string duration?: string }
export const RippleButton = React.forwardRef< HTMLButtonElement, RippleButtonProps
( ( { className, children, rippleColor = "#ffffff", duration = "600ms", onClick, ...props }, ref ) => { const [buttonRipples, setButtonRipples] = useState< Array<{ x: number; y: number; size: number; key: number }> >([])
const handleClick = (event: MouseEvent<HTMLButtonElement>) => {
createRipple(event)
onClick?.(event)
}
const createRipple = (event: MouseEvent<HTMLButtonElement>) => {
const button = event.currentTarget
const rect = button.getBoundingClientRect()
const size = Math.max(rect.width, rect.height)
const x = event.clientX - rect.left - size / 2
const y = event.clientY - rect.top - size / 2
const newRipple = { x, y, size, key: Date.now() }
setButtonRipples((prevRipples) => [...prevRipples, newRipple])
}
useEffect(() => {
if (buttonRipples.length > 0) {
const lastRipple = buttonRipples[buttonRipples.length - 1]
const timeout = setTimeout(() => {
setButtonRipples((prevRipples) =>
prevRipples.filter((ripple) => ripple.key !== lastRipple.key)
)
}, parseInt(duration))
return () => clearTimeout(timeout)
}
}, [buttonRipples, duration])
return (
<button
className={cn(
"bg-background text-primary relative flex cursor-pointer items-center justify-center overflow-hidden rounded-lg border-2 px-4 py-2 text-center",
className
)}
onClick={handleClick}
ref={ref}
{...props}
>
<div className="relative z-10">{children}</div>
<span className="pointer-events-none absolute inset-0">
{buttonRipples.map((ripple) => (
<span
className="animate-rippling bg-background absolute rounded-full opacity-30"
key={ripple.key}
style={{
width: `${ripple.size}px`,
height: `${ripple.size}px`,
top: `${ripple.y}px`,
left: `${ripple.x}px`,
backgroundColor: rippleColor,
transform: `scale(0)`,
}}
/>
))}
</span>
</button>
)
} )
RippleButton.displayName = "RippleButton"
===== EXAMPLE: ripple-button-demo ===== Title: Ripple Button Demo
--- file: example/ripple-button-demo.tsx --- import { RippleButton } from "@/registry/magicui/ripple-button"
export default function RippleButtonDemo() { return Click me }
===== COMPONENT: safari ===== Title: Safari Description: A safari browser mockup to showcase your website.
--- file: magicui/safari.tsx --- import type { HTMLAttributes } from "react"
const SAFARI_WIDTH = 1203 const SAFARI_HEIGHT = 753 const SCREEN_X = 1 const SCREEN_Y = 52 const SCREEN_WIDTH = 1200 const SCREEN_HEIGHT = 700
// Calculated percentages const LEFT_PCT = (SCREEN_X / SAFARI_WIDTH) * 100 const TOP_PCT = (SCREEN_Y / SAFARI_HEIGHT) * 100 const WIDTH_PCT = (SCREEN_WIDTH / SAFARI_WIDTH) * 100 const HEIGHT_PCT = (SCREEN_HEIGHT / SAFARI_HEIGHT) * 100
type SafariMode = "default" | "simple"
export interface SafariProps extends HTMLAttributes { url?: string imageSrc?: string videoSrc?: string mode?: SafariMode }
export function Safari({ imageSrc, videoSrc, url, mode = "default", className, style, ...props }: SafariProps) { const hasVideo = !!videoSrc const hasMedia = hasVideo || !!imageSrc
return (
<div
className={relative inline-block w-full align-middle leading-none ${className ?? ""}}
style={{
aspectRatio: ${SAFARI_WIDTH}/${SAFARI_HEIGHT},
...style,
}}
{...props}
>
{hasVideo && (
<div
className="pointer-events-none absolute z-0 overflow-hidden"
style={{
left: ${LEFT_PCT}%,
top: ${TOP_PCT}%,
width: ${WIDTH_PCT}%,
height: ${HEIGHT_PCT}%,
}}
>
)}
{!hasVideo && imageSrc && (
<div
className="pointer-events-none absolute z-0 overflow-hidden"
style={{
left: `${LEFT_PCT}%`,
top: `${TOP_PCT}%`,
width: `${WIDTH_PCT}%`,
height: `${HEIGHT_PCT}%`,
borderRadius: "0 0 11px 11px",
}}
>
<img
src={imageSrc}
alt=""
className="block size-full object-cover object-top"
/>
</div>
)}
<svg
viewBox={`0 0 ${SAFARI_WIDTH} ${SAFARI_HEIGHT}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="absolute inset-0 z-10 size-full"
style={{ transform: "translateZ(0)" }}
>
<defs>
<mask id="safariPunch" maskUnits="userSpaceOnUse">
<rect
x="0"
y="0"
width={SAFARI_WIDTH}
height={SAFARI_HEIGHT}
fill="white"
/>
<path
d="M1 52H1201V741C1201 747.075 1196.08 752 1190 752H12C5.92486 752 1 747.075 1 741V52Z"
fill="black"
/>
</mask>
<clipPath id="path0">
<rect width={SAFARI_WIDTH} height={SAFARI_HEIGHT} fill="white" />
</clipPath>
<clipPath id="roundedBottom">
<path
d="M1 52H1201V741C1201 747.075 1196.08 752 1190 752H12C5.92486 752 1 747.075 1 741V52Z"
fill="white"
/>
</clipPath>
</defs>
<g
clipPath="url(#path0)"
mask={hasMedia ? "url(#safariPunch)" : undefined}
>
<path
d="M0 52H1202V741C1202 747.627 1196.63 753 1190 753H12C5.37258 753 0 747.627 0 741V52Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 12C0 5.37258 5.37258 0 12 0H1190C1196.63 0 1202 5.37258 1202 12V52H0L0 12Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M1.06738 12C1.06738 5.92487 5.99225 1 12.0674 1H1189.93C1196.01 1 1200.93 5.92487 1200.93 12V51H1.06738V12Z"
className="fill-white dark:fill-[#262626]"
/>
<circle
cx="27"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<circle
cx="47"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<circle
cx="67"
cy="25"
r="6"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<path
d="M286 17C286 13.6863 288.686 11 292 11H946C949.314 11 952 13.6863 952 17V35C952 38.3137 949.314 41 946 41H292C288.686 41 286 38.3137 286 35V17Z"
className="fill-[#E5E5E5] dark:fill-[#404040]"
/>
<g className="mix-blend-luminosity">
<path
d="M566.269 32.0852H572.426C573.277 32.0852 573.696 31.6663 573.696 30.7395V25.9851C573.696 25.1472 573.353 24.7219 572.642 24.6521V23.0842C572.642 20.6721 571.036 19.5105 569.348 19.5105C567.659 19.5105 566.053 20.6721 566.053 23.0842V24.6711C565.393 24.7727 565 25.1917 565 25.9851V30.7395C565 31.6663 565.418 32.0852 566.269 32.0852ZM567.272 22.97C567.272 21.491 568.211 20.6785 569.348 20.6785C570.478 20.6785 571.423 21.491 571.423 22.97V24.6394L567.272 24.6458V22.97Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<text
x="580"
y="30"
fill="#A3A3A3"
fontSize="12"
fontFamily="Arial, sans-serif"
>
{url}
</text>
</g>
{mode === "default" ? (
<>
<g className="mix-blend-luminosity">
<path
d="M265.5 33.8984C265.641 33.8984 265.852 33.8516 266.047 33.7422C270.547 31.2969 272.109 30.1641 272.109 27.3203V21.4219C272.109 20.4844 271.742 20.1484 270.961 19.8125C270.094 19.4453 267.18 18.4297 266.328 18.1406C266.07 18.0547 265.766 18 265.5 18C265.234 18 264.93 18.0703 264.672 18.1406C263.82 18.3828 260.906 19.4531 260.039 19.8125C259.258 20.1406 258.891 20.4844 258.891 21.4219V27.3203C258.891 30.1641 260.461 31.2812 264.945 33.7422C265.148 33.8516 265.359 33.8984 265.5 33.8984ZM265.922 19.5781C266.945 19.9766 269.172 20.7656 270.344 21.1875C270.562 21.2656 270.617 21.3828 270.617 21.6641V27.0234C270.617 29.3125 269.469 29.9375 265.945 32.0625C265.727 32.1875 265.617 32.2344 265.508 32.2344V19.4844C265.617 19.4844 265.734 19.5156 265.922 19.5781Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M936.273 24.9766C936.5 24.9766 936.68 24.9062 936.82 24.7578L940.023 21.5312C940.195 21.3594 940.273 21.1719 940.273 20.9531C940.273 20.7422 940.188 20.5391 940.023 20.3828L936.82 17.125C936.68 16.9688 936.5 16.8906 936.273 16.8906C935.852 16.8906 935.516 17.2422 935.516 17.6719C935.516 17.8828 935.594 18.0547 935.727 18.2031L937.594 20.0312C937.227 19.9766 936.852 19.9453 936.477 19.9453C932.609 19.9453 929.516 23.0391 929.516 26.9141C929.516 30.7891 932.633 33.9062 936.5 33.9062C940.375 33.9062 943.484 30.7891 943.484 26.9141C943.484 26.4453 943.156 26.1094 942.688 26.1094C942.234 26.1094 941.93 26.4453 941.93 26.9141C941.93 29.9297 939.516 32.3516 936.5 32.3516C933.492 32.3516 931.07 29.9297 931.07 26.9141C931.07 23.875 933.469 21.4688 936.477 21.4688C936.984 21.4688 937.453 21.5078 937.867 21.5781L935.734 23.6875C935.594 23.8281 935.516 24 935.516 24.2109C935.516 24.6406 935.852 24.9766 936.273 24.9766Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1134 33.0156C1134.49 33.0156 1134.89 32.6094 1134.89 32.1484V27.2578H1139.66C1140.13 27.2578 1140.54 26.8594 1140.54 26.3672C1140.54 25.8828 1140.13 25.4766 1139.66 25.4766H1134.89V20.5859C1134.89 20.1172 1134.49 19.7188 1134 19.7188C1133.52 19.7188 1133.11 20.1172 1133.11 20.5859V25.4766H1128.34C1127.88 25.4766 1127.46 25.8828 1127.46 26.3672C1127.46 26.8594 1127.88 27.2578 1128.34 27.2578H1133.11V32.1484C1133.11 32.6094 1133.52 33.0156 1134 33.0156Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1161.8 31.0703H1163.23V32.375C1163.23 34.0547 1164.12 34.9219 1165.81 34.9219H1174.2C1175.89 34.9219 1176.77 34.0547 1176.77 32.3828V24.0469C1176.77 22.375 1175.89 21.5 1174.2 21.5H1172.77V20.2578C1172.77 18.5859 1171.88 17.7109 1170.19 17.7109H1161.8C1160.1 17.7109 1159.23 18.5781 1159.23 20.2578V28.5234C1159.23 30.1953 1160.1 31.0703 1161.8 31.0703ZM1161.9 29.5078C1161.18 29.5078 1160.78 29.1328 1160.78 28.3828V20.3984C1160.78 19.6406 1161.18 19.2656 1161.9 19.2656H1170.09C1170.8 19.2656 1171.2 19.6406 1171.2 20.3984V21.5H1165.81C1164.12 21.5 1163.23 22.375 1163.23 24.0469V29.5078H1161.9ZM1165.91 33.3672C1165.19 33.3672 1164.8 32.9922 1164.8 32.2422V24.1875C1164.8 23.4297 1165.19 23.0625 1165.91 23.0625H1174.1C1174.81 23.0625 1175.21 23.4297 1175.21 24.1875V32.2422C1175.21 32.9922 1174.81 33.3672 1174.1 33.3672H1165.91Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M1099.51 28.4141C1099.91 28.4141 1100.24 28.0859 1100.24 27.6953V19.8359L1100.18 18.6797L1100.66 19.25L1101.75 20.4141C1101.88 20.5547 1102.06 20.625 1102.24 20.625C1102.6 20.625 1102.9 20.3672 1102.9 20C1102.9 19.8047 1102.82 19.6641 1102.69 19.5312L1100.06 17.0078C1099.88 16.8203 1099.7 16.7578 1099.51 16.7578C1099.32 16.7578 1099.14 16.8203 1098.95 17.0078L1096.33 19.5312C1096.2 19.6641 1096.12 19.8047 1096.12 20C1096.12 20.3672 1096.41 20.625 1096.77 20.625C1096.95 20.625 1097.14 20.5547 1097.27 20.4141L1098.35 19.25L1098.84 18.6719L1098.78 19.8359V27.6953C1098.78 28.0859 1099.11 28.4141 1099.51 28.4141ZM1095 34.6562H1104C1105.7 34.6562 1106.57 33.7812 1106.57 32.1094V24.4297C1106.57 22.7578 1105.7 21.8828 1104 21.8828H1101.89V23.4375H1103.9C1104.61 23.4375 1105.02 23.8125 1105.02 24.5625V31.9688C1105.02 32.7188 1104.61 33.0938 1103.9 33.0938H1095.1C1094.38 33.0938 1093.98 32.7188 1093.98 31.9688V24.5625C1093.98 23.8125 1094.38 23.4375 1095.1 23.4375H1097.13V21.8828H1095C1093.31 21.8828 1092.43 22.75 1092.43 24.4297V32.1094C1092.43 33.7812 1093.31 34.6562 1095 34.6562Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M99.5703 33.6016H112.938C114.633 33.6016 115.516 32.7266 115.516 31.0547V21.5469C115.516 19.875 114.633 19 112.938 19H99.5703C97.8828 19 97 19.8672 97 21.5469V31.0547C97 32.7266 97.8828 33.6016 99.5703 33.6016ZM99.6719 32.0469C98.9531 32.0469 98.5547 31.6719 98.5547 30.9141V21.6875C98.5547 20.9297 98.9531 20.5547 99.6719 20.5547H103.234V32.0469H99.6719ZM112.836 20.5547C113.555 20.5547 113.953 20.9297 113.953 21.6875V30.9141C113.953 31.6719 113.555 32.0469 112.836 32.0469H104.711V20.5547H112.836ZM101.703 23.4141C101.984 23.4141 102.219 23.1719 102.219 22.9062C102.219 22.6406 101.984 22.4062 101.703 22.4062H100.102C99.8203 22.4062 99.5859 22.6406 99.5859 22.9062C99.5859 23.1719 99.8203 23.4141 100.102 23.4141H101.703ZM101.703 25.5156C101.984 25.5156 102.219 25.2812 102.219 25.0078C102.219 24.7422 101.984 24.5078 101.703 24.5078H100.102C99.8203 24.5078 99.5859 24.7422 99.5859 25.0078C99.5859 25.2812 99.8203 25.5156 100.102 25.5156H101.703ZM101.703 27.6094C101.984 27.6094 102.219 27.3828 102.219 27.1094C102.219 26.8438 101.984 26.6172 101.703 26.6172H100.102C99.8203 26.6172 99.5859 26.8438 99.5859 27.1094C99.5859 27.3828 99.8203 27.6094 100.102 27.6094H101.703Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M143.914 32.5938C144.094 32.7656 144.312 32.8594 144.562 32.8594C145.086 32.8594 145.492 32.4531 145.492 31.9375C145.492 31.6797 145.391 31.4453 145.211 31.2656L139.742 25.9219L145.211 20.5938C145.391 20.4141 145.492 20.1719 145.492 19.9219C145.492 19.4062 145.086 19 144.562 19C144.312 19 144.094 19.0938 143.922 19.2656L137.844 25.2031C137.625 25.4062 137.516 25.6562 137.516 25.9297C137.516 26.2031 137.625 26.4375 137.836 26.6484L143.914 32.5938Z"
fill="#A3A3A3"
/>
</g>
<g className="mix-blend-luminosity">
<path
d="M168.422 32.8594C168.68 32.8594 168.891 32.7656 169.07 32.5938L175.148 26.6562C175.359 26.4375 175.469 26.2109 175.469 25.9297C175.469 25.6562 175.367 25.4141 175.148 25.2109L169.07 19.2656C168.891 19.0938 168.68 19 168.422 19C167.898 19 167.492 19.4062 167.492 19.9219C167.492 20.1719 167.602 20.4141 167.773 20.5938L173.25 25.9375L167.773 31.2656C167.594 31.4531 167.492 31.6797 167.492 31.9375C167.492 32.4531 167.898 32.8594 168.422 32.8594Z"
fill="#A3A3A3"
/>
</g>
</>
) : null}
</g>
</svg>
</div>
) }
===== EXAMPLE: safari-demo ===== Title: Safari Demo
--- file: example/safari-demo.tsx --- import { Safari } from "@/registry/magicui/safari"
export default function SafariDemo() { return ( ) }
===== EXAMPLE: safari-demo-2 ===== Title: Safari Demo 2
--- file: example/safari-demo-2.tsx --- import { Safari } from "@/registry/magicui/safari"
export default function SafariDemo() { return ( ) }
===== EXAMPLE: safari-demo-3 ===== Title: Safari Demo 3
--- file: example/safari-demo-3.tsx --- import { Safari } from "@/registry/magicui/safari"
export default function SafariDemo() { return ( ) }
===== EXAMPLE: safari-demo-4 ===== Title: Safari Demo 4
--- file: example/safari-demo-4.tsx --- import { Safari } from "@/registry/magicui/safari"
export default function SafariDemo() { return ( ) }
===== COMPONENT: scroll-based-velocity ===== Title: Scroll Based Velocity Description: Scrolling text whose speed changes based on scroll speed
--- file: magicui/scroll-based-velocity.tsx --- "use client"
import React, { useContext, useEffect, useRef, useState } from "react" import { motion, useAnimationFrame, useMotionValue, useScroll, useSpring, useTransform, useVelocity, } from "motion/react" import type { MotionValue } from "motion/react"
import { cn } from "@/lib/utils"
interface ScrollVelocityRowProps extends React.HTMLAttributes { children: React.ReactNode baseVelocity?: number direction?: 1 | -1 }
export const wrap = (min: number, max: number, v: number) => { const rangeSize = max - min return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min }
const ScrollVelocityContext = React.createContext<MotionValue | null>( null )
export function ScrollVelocityContainer({ children, className, ...props }: React.HTMLAttributes) { const { scrollY } = useScroll() const scrollVelocity = useVelocity(scrollY) const smoothVelocity = useSpring(scrollVelocity, { damping: 50, stiffness: 400, }) const velocityFactor = useTransform(smoothVelocity, (v) => { const sign = v < 0 ? -1 : 1 const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5) return sign * magnitude })
return ( <ScrollVelocityContext.Provider value={velocityFactor}> <div className={cn("relative w-full", className)} {...props}> {children} </ScrollVelocityContext.Provider> ) }
export function ScrollVelocityRow(props: ScrollVelocityRowProps) { const sharedVelocityFactor = useContext(ScrollVelocityContext) if (sharedVelocityFactor) { return ( <ScrollVelocityRowImpl {...props} velocityFactor={sharedVelocityFactor} /> ) } return <ScrollVelocityRowLocal {...props} /> }
interface ScrollVelocityRowImplProps extends ScrollVelocityRowProps { velocityFactor: MotionValue }
function ScrollVelocityRowImpl({ children, baseVelocity = 5, direction = 1, className, velocityFactor, ...props }: ScrollVelocityRowImplProps) { const containerRef = useRef(null) const blockRef = useRef(null) const [numCopies, setNumCopies] = useState(1)
const baseX = useMotionValue(0) const baseDirectionRef = useRef(direction >= 0 ? 1 : -1) const currentDirectionRef = useRef(direction >= 0 ? 1 : -1) const unitWidth = useMotionValue(0)
const isInViewRef = useRef(true) const isPageVisibleRef = useRef(true) const prefersReducedMotionRef = useRef(false)
useEffect(() => { const container = containerRef.current const block = blockRef.current if (!container || !block) return
const updateSizes = () => {
const cw = container.offsetWidth || 0
const bw = block.scrollWidth || 0
unitWidth.set(bw)
const nextCopies = bw > 0 ? Math.max(3, Math.ceil(cw / bw) + 2) : 1
setNumCopies((prev) => (prev === nextCopies ? prev : nextCopies))
}
updateSizes()
const ro = new ResizeObserver(updateSizes)
ro.observe(container)
ro.observe(block)
const io = new IntersectionObserver(([entry]) => {
isInViewRef.current = entry.isIntersecting
})
io.observe(container)
const handleVisibility = () => {
isPageVisibleRef.current = document.visibilityState === "visible"
}
document.addEventListener("visibilitychange", handleVisibility, {
passive: true,
})
handleVisibility()
const mq = window.matchMedia("(prefers-reduced-motion: reduce)")
const handlePRM = () => {
prefersReducedMotionRef.current = mq.matches
}
mq.addEventListener("change", handlePRM)
handlePRM()
return () => {
ro.disconnect()
io.disconnect()
document.removeEventListener("visibilitychange", handleVisibility)
mq.removeEventListener("change", handlePRM)
}
}, [children, unitWidth])
const x = useTransform([baseX, unitWidth], ([v, bw]) => {
const width = Number(bw) || 1
const offset = Number(v) || 0
return ${-wrap(0, width, offset)}px
})
useAnimationFrame((_, delta) => { if (!isInViewRef.current || !isPageVisibleRef.current) return const dt = delta / 1000 const vf = velocityFactor.get() const absVf = Math.min(5, Math.abs(vf)) const speedMultiplier = prefersReducedMotionRef.current ? 1 : 1 + absVf
if (absVf > 0.1) {
const scrollDirection = vf >= 0 ? 1 : -1
currentDirectionRef.current = baseDirectionRef.current * scrollDirection
}
const bw = unitWidth.get() || 0
if (bw <= 0) return
const pixelsPerSecond = (bw * baseVelocity) / 100
const moveBy =
currentDirectionRef.current * pixelsPerSecond * speedMultiplier * dt
baseX.set(baseX.get() + moveBy)
})
return ( <div ref={containerRef} className={cn("w-full overflow-hidden whitespace-nowrap", className)} {...props} > <motion.div className="inline-flex transform-gpu items-center will-change-transform select-none" style={{ x }} > {Array.from({ length: numCopies }).map((_, i) => ( <div key={i} ref={i === 0 ? blockRef : null} aria-hidden={i !== 0} className="inline-flex shrink-0 items-center" > {children} ))} </motion.div> ) }
function ScrollVelocityRowLocal(props: ScrollVelocityRowProps) { const { scrollY } = useScroll() const localVelocity = useVelocity(scrollY) const localSmoothVelocity = useSpring(localVelocity, { damping: 50, stiffness: 400, }) const localVelocityFactor = useTransform(localSmoothVelocity, (v) => { const sign = v < 0 ? -1 : 1 const magnitude = Math.min(5, (Math.abs(v) / 1000) * 5) return sign * magnitude }) return ( <ScrollVelocityRowImpl {...props} velocityFactor={localVelocityFactor} /> ) }
===== EXAMPLE: scroll-based-velocity-demo ===== Title: Scroll Based Velocity Demo
--- file: example/scroll-based-velocity-demo.tsx --- import { ScrollVelocityContainer, ScrollVelocityRow, } from "@/registry/magicui/scroll-based-velocity"
export default function ScrollBasedVelocityDemo() { return ( Velocity Scroll Velocity Scroll ) }
===== EXAMPLE: scroll-based-velocity-images-demo ===== Title: Scroll Based Velocity Images
--- file: example/scroll-based-velocity-images-demo.tsx --- import { ScrollVelocityContainer, ScrollVelocityRow, } from "@/registry/magicui/scroll-based-velocity"
const IMAGES_ROW_A = [ "https://images.unsplash.com/photo-1749738456487-2af715ab65ea?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1720139288219-e20aa9c8895b?q=80&w=1810&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", ]
const IMAGES_ROW_B = [ "https://images.unsplash.com/photo-1749738456487-2af715ab65ea?q=80&w=2340&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", "https://plus.unsplash.com/premium_photo-1720139288219-e20aa9c8895b?q=80&w=1810&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D", ]
export default function ScrollBasedVelocityImagesDemo() {
return (
{IMAGES_ROW_A.map((src, idx) => (
<img
key={idx}
src={${src}&ixlib=rb-4.0.3}
alt="Unsplash sample"
width={240}
height={160}
loading="lazy"
decoding="async"
className="mx-4 inline-block h-40 w-60 rounded-lg object-cover shadow-sm"
/>
))}
{IMAGES_ROW_B.map((src, idx) => (
<img
key={idx}
src={${src}&ixlib=rb-4.0.3}
alt="Unsplash sample"
width={240}
height={160}
loading="lazy"
decoding="async"
className="mx-4 inline-block h-40 w-60 rounded-lg object-cover shadow-sm"
/>
))}
<div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div>
<div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div>
</div>
) }
===== COMPONENT: scroll-progress ===== Title: Scroll Progress Description: Animated Scroll Progress for your pages
--- file: magicui/scroll-progress.tsx --- "use client"
import { motion, MotionProps, useScroll } from "motion/react"
import { cn } from "@/lib/utils"
interface ScrollProgressProps extends Omit< React.HTMLAttributes, keyof MotionProps
{ ref?: React.Ref }
export function ScrollProgress({ className, ref, ...props }: ScrollProgressProps) { const { scrollYProgress } = useScroll()
return ( <motion.div ref={ref} className={cn( "fixed inset-x-0 top-0 z-50 h-px origin-left bg-gradient-to-r from-[#A97CF8] via-[#F38CB8] to-[#FDCC92]", className )} style={{ scaleX: scrollYProgress, }} {...props} /> ) }
===== EXAMPLE: scroll-progress-demo ===== Title: Scroll Progress Demo
--- file: example/scroll-progress-demo.tsx --- import { ScrollProgress } from "@/registry/magicui/scroll-progress"
export default function ScrollProgressDemo() { return ( Note: The scroll progress is shown below the navbar of the page. ) }
===== COMPONENT: shimmer-button ===== Title: Shimmer Button Description: A button with a shimmering light which travels around the perimeter.
--- file: magicui/shimmer-button.tsx --- import React, { ComponentPropsWithoutRef, CSSProperties } from "react"
import { cn } from "@/lib/utils"
export interface ShimmerButtonProps extends ComponentPropsWithoutRef<"button"> { shimmerColor?: string shimmerSize?: string borderRadius?: string shimmerDuration?: string background?: string className?: string children?: React.ReactNode }
export const ShimmerButton = React.forwardRef< HTMLButtonElement, ShimmerButtonProps
( ( { shimmerColor = "#ffffff", shimmerSize = "0.05em", shimmerDuration = "3s", borderRadius = "100px", background = "rgba(0, 0, 0, 1)", className, children, ...props }, ref ) => { return ( <button style={ { "--spread": "90deg", "--shimmer-color": shimmerColor, "--radius": borderRadius, "--speed": shimmerDuration, "--cut": shimmerSize, "--bg": background, } as CSSProperties } className={cn( "group relative z-0 flex cursor-pointer items-center justify-center overflow-hidden [border-radius:var(--radius)] border border-white/10 px-6 py-3 whitespace-nowrap text-white [background:var(--bg)]", "transform-gpu transition-transform duration-300 ease-in-out active:translate-y-px", className )} ref={ref} {...props} > {/* spark container /} <div className={cn( "-z-30 blur-[2px]", "[container-type:size] absolute inset-0 overflow-visible" )} > {/ spark /} {/ spark before */} {children}
{/* Highlight */}
<div
className={cn(
"absolute inset-0 size-full",
"rounded-2xl px-4 py-1.5 text-sm font-medium shadow-[inset_0_-8px_10px_#ffffff1f]",
// transition
"transform-gpu transition-all duration-300 ease-in-out",
// on hover
"group-hover:shadow-[inset_0_-6px_10px_#ffffff3f]",
// on click
"group-active:shadow-[inset_0_-10px_10px_#ffffff3f]"
)}
/>
{/* backdrop */}
<div
className={cn(
"absolute [inset:var(--cut)] -z-20 [border-radius:var(--radius)] [background:var(--bg)]"
)}
/>
</button>
)
} )
ShimmerButton.displayName = "ShimmerButton"
===== EXAMPLE: shimmer-button-demo ===== Title: Shimmer Button Demo
--- file: example/shimmer-button-demo.tsx --- import { ShimmerButton } from "@/registry/magicui/shimmer-button"
export default function ShimmerButtonDemo() { return ( Shimmer Button ) }
===== COMPONENT: shine-border ===== Title: Shine Border Description: Shine border is an animated background border effect.
--- file: magicui/shine-border.tsx --- "use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface ShineBorderProps extends React.HTMLAttributes { /**
- Width of the border in pixels
- @default 1 / borderWidth?: number /*
- Duration of the animation in seconds
- @default 14 / duration?: number /*
- Color of the border, can be a single color or an array of colors
- @default "#000000" */ shineColor?: string | string[] }
/**
- Shine Border
- An animated background border effect component with configurable properties. */ export function ShineBorder({ borderWidth = 1, duration = 14, shineColor = "#000000", className, style, ...props }: ShineBorderProps) { return (
) }
===== EXAMPLE: shine-border-demo ===== Title: Shine Border Demo
--- file: example/shine-border-demo.tsx --- import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ShineBorder } from "@/registry/magicui/shine-border"
export default function ShineBorderDemo() { return ( <ShineBorder shineColor={["#A07CFE", "#FE8FB5", "#FFBE7B"]} /> Login Enter your credentials to access your account Email Password Sign In ) }
===== EXAMPLE: shine-border-demo-2 ===== Title: Shine Border Demo 2
--- file: example/shine-border-demo-2.tsx --- "use client"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from "@/components/ui/card" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { ShineBorder } from "@/registry/magicui/shine-border"
export default function ShineBorderDemo2() { const theme = useTheme() return ( <ShineBorder shineColor={theme.theme === "dark" ? "white" : "black"} /> Login Enter your credentials to access your account Email Password Sign In ) }
===== COMPONENT: shiny-button ===== Title: Shiny Button Description: A shiny button component with dynamic styles in the dark mode or light mode.
--- file: magicui/shiny-button.tsx --- "use client"
import React from "react" import { motion, type MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
const animationProps: MotionProps = { initial: { "--x": "100%", scale: 0.8 }, animate: { "--x": "-100%", scale: 1 }, whileTap: { scale: 0.95 }, transition: { repeat: Infinity, repeatType: "loop", repeatDelay: 1, type: "spring", stiffness: 20, damping: 15, mass: 2, scale: { type: "spring", stiffness: 200, damping: 5, mass: 0.5, }, }, }
interface ShinyButtonProps extends Omit<React.HTMLAttributes, keyof MotionProps>, MotionProps { children: React.ReactNode className?: string }
export const ShinyButton = React.forwardRef< HTMLButtonElement, ShinyButtonProps
(({ children, className, ...props }, ref) => { return ( <motion.button ref={ref} className={cn( "relative cursor-pointer rounded-lg border px-6 py-2 font-medium backdrop-blur-xl transition-shadow duration-300 ease-in-out hover:shadow dark:bg-[radial-gradient(circle_at_50%_0%,var(--primary)/10%_0%,transparent_60%)] dark:hover:shadow-[0_0_20px_var(--primary)/10%]", className )} {...animationProps} {...props} > <span className="relative block size-full text-sm tracking-wide text-[rgb(0,0,0,65%)] uppercase dark:font-light dark:text-[rgb(255,255,255,90%)]" style={{ maskImage: "linear-gradient(-75deg,var(--primary) calc(var(--x) + 20%),transparent calc(var(--x) + 30%),var(--primary) calc(var(--x) + 100%))", }} > {children} <span style={{ mask: "linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box exclude,linear-gradient(rgb(0,0,0), rgb(0,0,0))", WebkitMask: "linear-gradient(rgb(0,0,0), rgb(0,0,0)) content-box exclude,linear-gradient(rgb(0,0,0), rgb(0,0,0))", backgroundImage: "linear-gradient(-75deg,var(--primary)/10% calc(var(--x)+20%),var(--primary)/50% calc(var(--x)+25%),var(--primary)/10% calc(var(--x)+100%))", }} className="absolute inset-0 z-10 block rounded-[inherit] p-px" /> </motion.button> ) })
ShinyButton.displayName = "ShinyButton"
===== EXAMPLE: shiny-button-demo ===== Title: Shiny Button Demo
--- file: example/shiny-button-demo.tsx --- import { ShinyButton } from "@/registry/magicui/shiny-button"
export default function ShinyButtonDemo() { return Shiny Button }
===== COMPONENT: smooth-cursor ===== Title: smooth-cursor Description: A customizable, physics-based smooth cursor animation component with spring animations and rotation effects
--- file: magicui/smooth-cursor.tsx --- "use client"
import { FC, useEffect, useRef, useState } from "react" import { motion, useSpring } from "motion/react"
interface Position { x: number y: number }
export interface SmoothCursorProps { cursor?: React.ReactNode springConfig?: { damping: number stiffness: number mass: number restDelta: number } }
const DefaultCursorSVG: FC = () => { return ( <svg xmlns="http://www.w3.org/2000/svg" width={50} height={54} viewBox="0 0 50 54" fill="none" style={{ scale: 0.5 }} > ) }
export function SmoothCursor({ cursor = , springConfig = { damping: 45, stiffness: 400, mass: 1, restDelta: 0.001, }, }: SmoothCursorProps) { const [isMoving, setIsMoving] = useState(false) const lastMousePos = useRef({ x: 0, y: 0 }) const velocity = useRef({ x: 0, y: 0 }) const lastUpdateTime = useRef(Date.now()) const previousAngle = useRef(0) const accumulatedRotation = useRef(0)
const cursorX = useSpring(0, springConfig) const cursorY = useSpring(0, springConfig) const rotation = useSpring(0, { ...springConfig, damping: 60, stiffness: 300, }) const scale = useSpring(1, { ...springConfig, stiffness: 500, damping: 35, })
useEffect(() => { const updateVelocity = (currentPos: Position) => { const currentTime = Date.now() const deltaTime = currentTime - lastUpdateTime.current
if (deltaTime > 0) {
velocity.current = {
x: (currentPos.x - lastMousePos.current.x) / deltaTime,
y: (currentPos.y - lastMousePos.current.y) / deltaTime,
}
}
lastUpdateTime.current = currentTime
lastMousePos.current = currentPos
}
const smoothMouseMove = (e: MouseEvent) => {
const currentPos = { x: e.clientX, y: e.clientY }
updateVelocity(currentPos)
const speed = Math.sqrt(
Math.pow(velocity.current.x, 2) + Math.pow(velocity.current.y, 2)
)
cursorX.set(currentPos.x)
cursorY.set(currentPos.y)
if (speed > 0.1) {
const currentAngle =
Math.atan2(velocity.current.y, velocity.current.x) * (180 / Math.PI) +
90
let angleDiff = currentAngle - previousAngle.current
if (angleDiff > 180) angleDiff -= 360
if (angleDiff < -180) angleDiff += 360
accumulatedRotation.current += angleDiff
rotation.set(accumulatedRotation.current)
previousAngle.current = currentAngle
scale.set(0.95)
setIsMoving(true)
const timeout = setTimeout(() => {
scale.set(1)
setIsMoving(false)
}, 150)
return () => clearTimeout(timeout)
}
}
let rafId: number
const throttledMouseMove = (e: MouseEvent) => {
if (rafId) return
rafId = requestAnimationFrame(() => {
smoothMouseMove(e)
rafId = 0
})
}
document.body.style.cursor = "none"
window.addEventListener("mousemove", throttledMouseMove)
return () => {
window.removeEventListener("mousemove", throttledMouseMove)
document.body.style.cursor = "auto"
if (rafId) cancelAnimationFrame(rafId)
}
}, [cursorX, cursorY, rotation, scale])
return ( <motion.div style={{ position: "fixed", left: cursorX, top: cursorY, translateX: "-50%", translateY: "-50%", rotate: rotation, scale: scale, zIndex: 100, pointerEvents: "none", willChange: "transform", }} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 400, damping: 30, }} > {cursor} </motion.div> ) }
===== EXAMPLE: smooth-cursor-demo ===== Title: smooth-cursor-demo
--- file: example/smooth-cursor-demo.tsx --- import { SmoothCursor } from "@/registry/magicui/smooth-cursor"
export default function SmoothCursorDemo() { return ( <> Move your mouse around Tap anywhere to see the cursor </> ) }
===== COMPONENT: sparkles-text ===== Title: Sparkles Text Description: A dynamic text that generates continuous sparkles with smooth transitions, perfect for highlighting text with animated stars.
--- file: magicui/sparkles-text.tsx --- "use client"
import { CSSProperties, ReactElement, useEffect, useState } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
interface Sparkle { id: string x: string y: string color: string delay: number scale: number lifespan: number }
const Sparkle: React.FC = ({ id, x, y, color, delay, scale }) => { return ( <motion.svg key={id} className="pointer-events-none absolute z-20" initial={{ opacity: 0, left: x, top: y }} animate={{ opacity: [0, 1, 0], scale: [0, scale, 0], rotate: [75, 120, 150], }} transition={{ duration: 0.8, repeat: Infinity, delay }} width="21" height="21" viewBox="0 0 21 21" > </motion.svg> ) }
interface SparklesTextProps { /**
- @default
- @type ReactElement
- @description
- The component to be rendered as the text
- */ as?: ReactElement
/**
- @default ""
- @type string
- @description
- The className of the text */ className?: string
/**
- @required
- @type ReactNode
- @description
- The content to be displayed
- */ children: React.ReactNode
/**
- @default 10
- @type number
- @description
- The count of sparkles
- */ sparklesCount?: number
/**
- @default "{first: '#9E7AFF', second: '#FE8BBB'}"
- @type string
- @description
- The colors of the sparkles
- */ colors?: { first: string second: string } }
export const SparklesText: React.FC = ({ children, colors = { first: "#9E7AFF", second: "#FE8BBB" }, className, sparklesCount = 10, ...props }) => { const [sparkles, setSparkles] = useState<Sparkle[]>([])
useEffect(() => {
const generateStar = (): Sparkle => {
const starX = ${Math.random() * 100}%
const starY = ${Math.random() * 100}%
const color = Math.random() > 0.5 ? colors.first : colors.second
const delay = Math.random() * 2
const scale = Math.random() * 1 + 0.3
const lifespan = Math.random() * 10 + 5
const id = ${starX}-${starY}-${Date.now()}
return { id, x: starX, y: starY, color, delay, scale, lifespan }
}
const initializeStars = () => {
const newSparkles = Array.from({ length: sparklesCount }, generateStar)
setSparkles(newSparkles)
}
const updateStars = () => {
setSparkles((currentSparkles) =>
currentSparkles.map((star) => {
if (star.lifespan <= 0) {
return generateStar()
} else {
return { ...star, lifespan: star.lifespan - 0.1 }
}
})
)
}
initializeStars()
const interval = setInterval(updateStars, 100)
return () => clearInterval(interval)
}, [colors.first, colors.second, sparklesCount])
return (
<div
className={cn("text-6xl font-bold", className)}
{...props}
style={
{
"--sparkles-first-color": ${colors.first},
"--sparkles-second-color": ${colors.second},
} as CSSProperties
}
>
{sparkles.map((sparkle) => (
<Sparkle key={sparkle.id} {...sparkle} />
))}
{children}
)
}
===== EXAMPLE: sparkles-text-demo ===== Title: Sparkles Text Demo
--- file: example/sparkles-text-demo.tsx --- import { SparklesText } from "@/registry/magicui/sparkles-text"
export default function SparklesTextDemo() { return Magic UI }
===== COMPONENT: spinning-text ===== Title: Spinning Text Description: The Spinning Text component animates text in a circular motion with customizable speed, direction, color, and transitions for dynamic and engaging effects.
--- file: magicui/spinning-text.tsx --- "use client"
import React, { ComponentPropsWithoutRef } from "react" import { motion, Transition, Variants } from "motion/react"
import { cn } from "@/lib/utils"
interface SpinningTextProps extends ComponentPropsWithoutRef<"div"> { children: string | string[] duration?: number reverse?: boolean radius?: number transition?: Transition variants?: { container?: Variants item?: Variants } }
const BASE_TRANSITION: Transition = { repeat: Infinity, ease: "linear", }
const BASE_ITEM_VARIANTS: Variants = { hidden: { opacity: 1, }, visible: { opacity: 1, }, }
export function SpinningText({ children, duration = 10, reverse = false, radius = 5, transition, variants, className, style, }: SpinningTextProps) { if (typeof children !== "string" && !Array.isArray(children)) { throw new Error("children must be a string or an array of strings") }
if (Array.isArray(children)) { // Validate all elements are strings if (!children.every((child) => typeof child === "string")) { throw new Error("all elements in children array must be strings") } children = children.join("") }
const letters = children.split("") letters.push(" ")
const finalTransition: Transition = { ...BASE_TRANSITION, ...transition, duration: (transition as { duration?: number })?.duration ?? duration, }
const containerVariants: Variants = { visible: { rotate: reverse ? -360 : 360 }, ...variants?.container, }
const itemVariants: Variants = { ...BASE_ITEM_VARIANTS, ...variants?.item, }
return (
<motion.div
className={cn("relative", className)}
style={{
...style,
}}
initial="hidden"
animate="visible"
variants={containerVariants}
transition={finalTransition}
>
{letters.map((letter, index) => (
<motion.span
aria-hidden="true"
key={${index}-${letter}}
variants={itemVariants}
className="absolute top-1/2 left-1/2 inline-block"
style={
{
"--index": index,
"--total": letters.length,
"--radius": radius,
transform: translate(-50%, -50%) rotate(calc(360deg / var(--total) * var(--index))) translateY(calc(var(--radius, 5) * -1ch)) ,
transformOrigin: "center",
} as React.CSSProperties
}
>
{letter}
</motion.span>
))}
{children}
</motion.div>
)
}
===== EXAMPLE: spinning-text-demo ===== Title: Spinning Text Demo
--- file: example/spinning-text-demo.tsx --- import { SpinningText } from "@/registry/magicui/spinning-text"
export default function SpinningTextBasic() { return learn more • earn more • grow more • }
===== EXAMPLE: spinning-text-demo-2 ===== Title: Spinning Text Demo 2
--- file: example/spinning-text-demo-2.tsx --- import { SpinningText } from "@/registry/magicui/spinning-text"
export default function SpinningTextBasic() { return ( learn more • earn more • grow more • ) }
===== COMPONENT: striped-pattern ===== Title: Striped Pattern Description: A background striped pattern made with SVGs, fully customizable using Tailwind CSS.
--- file: magicui/striped-pattern.tsx --- import React, { useId } from "react"
import { cn } from "@/lib/utils"
interface StripedPatternProps extends React.SVGProps { direction?: "left" | "right" }
export function StripedPattern({ direction = "left", className, width = 10, height = 10, ...props }: StripedPatternProps) { const id = useId() const w = Number(width) const h = Number(height)
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 z-10 h-full w-full stroke-[0.5]",
className
)}
xmlns="http://www.w3.org/2000/svg"
{...props}
>
{direction === "left" ? (
<>
<line x1={w} y1={h} x2={w * 2} y2="0" stroke="currentColor" />
</>
) : (
<>
<line x1={w} y1="0" x2={w * 2} y2={h} stroke="currentColor" />
</>
)}
<rect width="100%" height="100%" fill={url(#${id})} />
)
}
===== EXAMPLE: striped-pattern-demo ===== Title: Striped Pattern Demo
--- file: example/striped-pattern-demo.tsx --- import { StripedPattern } from "@/registry/magicui/striped-pattern"
export default function StripedPatternDemo() { return ( ) }
===== EXAMPLE: striped-pattern-dashed ===== Title: Striped Pattern (Dashed)
--- file: example/striped-pattern-dashed.tsx --- import { StripedPattern } from "@/registry/magicui/striped-pattern"
export default function Component() { return ( ) }
===== EXAMPLE: striped-pattern-right ===== Title: Striped Pattern (Right)
--- file: example/striped-pattern-right.tsx --- import { StripedPattern } from "@/registry/magicui/striped-pattern"
export default function StripedPatternRight() { return ( ) }
===== COMPONENT: terminal ===== Title: Terminal Description: A terminal component
--- file: magicui/terminal.tsx --- "use client"
import { Children, createContext, useContext, useEffect, useMemo, useRef, useState, } from "react" import { motion, MotionProps, useInView } from "motion/react"
import { cn } from "@/lib/utils"
interface SequenceContextValue { completeItem: (index: number) => void activeIndex: number sequenceStarted: boolean }
const SequenceContext = createContext<SequenceContextValue | null>(null)
const useSequence = () => useContext(SequenceContext)
const ItemIndexContext = createContext<number | null>(null) const useItemIndex = () => useContext(ItemIndexContext)
interface AnimatedSpanProps extends MotionProps { children: React.ReactNode delay?: number className?: string startOnView?: boolean }
export const AnimatedSpan = ({ children, delay = 0, className, startOnView = false, ...props }: AnimatedSpanProps) => { const elementRef = useRef<HTMLDivElement | null>(null) const isInView = useInView(elementRef as React.RefObject, { amount: 0.3, once: true, })
const sequence = useSequence() const itemIndex = useItemIndex() const [hasStarted, setHasStarted] = useState(false) useEffect(() => { if (!sequence || itemIndex === null) return if (!sequence.sequenceStarted) return if (hasStarted) return if (sequence.activeIndex === itemIndex) { setHasStarted(true) } }, [sequence?.activeIndex, sequence?.sequenceStarted, hasStarted, itemIndex])
const shouldAnimate = sequence ? hasStarted : startOnView ? isInView : true
return ( <motion.div ref={elementRef} initial={{ opacity: 0, y: -5 }} animate={shouldAnimate ? { opacity: 1, y: 0 } : { opacity: 0, y: -5 }} transition={{ duration: 0.3, delay: sequence ? 0 : delay / 1000 }} className={cn("grid text-sm font-normal tracking-tight", className)} onAnimationComplete={() => { if (!sequence) return if (itemIndex === null) return sequence.completeItem(itemIndex) }} {...props} > {children} </motion.div> ) }
interface TypingAnimationProps extends MotionProps { children: string className?: string duration?: number delay?: number as?: React.ElementType startOnView?: boolean }
export const TypingAnimation = ({ children, className, duration = 60, delay = 0, as: Component = "span", startOnView = true, ...props }: TypingAnimationProps) => { if (typeof children !== "string") { throw new Error("TypingAnimation: children must be a string. Received:") }
const MotionComponent = useMemo( () => motion.create(Component, { forwardMotionProps: true, }), [Component] )
const [displayedText, setDisplayedText] = useState("") const [started, setStarted] = useState(false) const elementRef = useRef<HTMLElement | null>(null) const isInView = useInView(elementRef as React.RefObject, { amount: 0.3, once: true, })
const sequence = useSequence() const itemIndex = useItemIndex()
useEffect(() => { if (sequence && itemIndex !== null) { if (!sequence.sequenceStarted) return if (started) return if (sequence.activeIndex === itemIndex) { setStarted(true) } return }
if (!startOnView) {
const startTimeout = setTimeout(() => setStarted(true), delay)
return () => clearTimeout(startTimeout)
}
if (!isInView) return
const startTimeout = setTimeout(() => setStarted(true), delay)
return () => clearTimeout(startTimeout)
}, [ delay, startOnView, isInView, started, sequence?.activeIndex, sequence?.sequenceStarted, itemIndex, ])
useEffect(() => { if (!started) return
let i = 0
const typingEffect = setInterval(() => {
if (i < children.length) {
setDisplayedText(children.substring(0, i + 1))
i++
} else {
clearInterval(typingEffect)
if (sequence && itemIndex !== null) {
sequence.completeItem(itemIndex)
}
}
}, duration)
return () => {
clearInterval(typingEffect)
}
}, [children, duration, started])
return ( <MotionComponent ref={elementRef} className={cn("text-sm font-normal tracking-tight", className)} {...props} > {displayedText} ) }
interface TerminalProps { children: React.ReactNode className?: string sequence?: boolean startOnView?: boolean }
export const Terminal = ({ children, className, sequence = true, startOnView = true, }: TerminalProps) => { const containerRef = useRef<HTMLDivElement | null>(null) const isInView = useInView(containerRef as React.RefObject, { amount: 0.3, once: true, })
const [activeIndex, setActiveIndex] = useState(0) const sequenceHasStarted = sequence ? !startOnView || isInView : false
const contextValue = useMemo<SequenceContextValue | null>(() => { if (!sequence) return null return { completeItem: (index: number) => { setActiveIndex((current) => (index === current ? current + 1 : current)) }, activeIndex, sequenceStarted: sequenceHasStarted, } }, [sequence, activeIndex, sequenceHasStarted])
const wrappedChildren = useMemo(() => { if (!sequence) return children const array = Children.toArray(children) return array.map((child, index) => ( <ItemIndexContext.Provider key={index} value={index}> {child as React.ReactNode} </ItemIndexContext.Provider> )) }, [children, sequence])
const content = ( <div ref={containerRef} className={cn( "border-border bg-background z-0 h-full max-h-[400px] w-full max-w-lg rounded-xl border", className )} > {wrappedChildren} )
if (!sequence) return content
return ( <SequenceContext.Provider value={contextValue}> {content} </SequenceContext.Provider> ) }
===== EXAMPLE: terminal-demo ===== Title: Terminal Demo
--- file: example/terminal-demo.tsx --- import { AnimatedSpan, Terminal, TypingAnimation, } from "@/registry/magicui/terminal"
export default function TerminalDemo() { return ( > pnpm dlx shadcn@latest init
<AnimatedSpan className="text-green-500">
✔ Preflight checks.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Verifying framework. Found Next.js.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Validating Tailwind CSS.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Validating import alias.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Writing components.json.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Checking registry.
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Updating tailwind.config.ts
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Updating app/globals.css
</AnimatedSpan>
<AnimatedSpan className="text-green-500">
✔ Installing dependencies.
</AnimatedSpan>
<AnimatedSpan className="text-blue-500">
<span>ℹ Updated 1 file:</span>
<span className="pl-2">- lib/utils.ts</span>
</AnimatedSpan>
<TypingAnimation className="text-muted-foreground">
Success! Project initialization completed.
</TypingAnimation>
<TypingAnimation className="text-muted-foreground">
You may now add components.
</TypingAnimation>
</Terminal>
) }
===== EXAMPLE: terminal-demo-2 ===== Title: Terminal Demo
--- file: example/terminal-demo-2.tsx --- import { AnimatedSpan, Terminal, TypingAnimation, } from "@/registry/magicui/terminal"
export default function TerminalDemo2() { return ( $ ls
<AnimatedSpan delay={800} className="text-blue-500">
Documents Downloads Pictures
</AnimatedSpan>
<TypingAnimation delay={1600}>$ cd Documents</TypingAnimation>
<TypingAnimation delay={2400}>$ pwd</TypingAnimation>
<AnimatedSpan delay={3200} className="text-green-500">
/home/user/Documents
</AnimatedSpan>
</Terminal>
) }
===== COMPONENT: text-animate ===== Title: Text Animate Description: A text animation component that animates text using a variety of different animations.
--- file: magicui/text-animate.tsx --- "use client"
import { ElementType, memo } from "react" import { AnimatePresence, motion, MotionProps, Variants } from "motion/react"
import { cn } from "@/lib/utils"
type AnimationType = "text" | "word" | "character" | "line" type AnimationVariant = | "fadeIn" | "blurIn" | "blurInUp" | "blurInDown" | "slideUp" | "slideDown" | "slideLeft" | "slideRight" | "scaleUp" | "scaleDown"
interface TextAnimateProps extends MotionProps { /**
- The text content to animate / children: string /*
- The class name to be applied to the component / className?: string /*
- The class name to be applied to each segment / segmentClassName?: string /*
- The delay before the animation starts / delay?: number /*
- The duration of the animation / duration?: number /*
- Custom motion variants for the animation / variants?: Variants /*
- The element type to render / as?: ElementType /*
- How to split the text ("text", "word", "character") / by?: AnimationType /*
- Whether to start animation when component enters viewport / startOnView?: boolean /*
- Whether to animate only once / once?: boolean /*
- The animation preset to use / animation?: AnimationVariant /*
- Whether to enable accessibility features (default: true) */ accessible?: boolean }
const staggerTimings: Record<AnimationType, number> = { text: 0.06, word: 0.05, character: 0.03, line: 0.06, }
const defaultContainerVariants = { hidden: { opacity: 1 }, show: { opacity: 1, transition: { delayChildren: 0, staggerChildren: 0.05, }, }, exit: { opacity: 0, transition: { staggerChildren: 0.05, staggerDirection: -1, }, }, }
const defaultItemVariants: Variants = { hidden: { opacity: 0 }, show: { opacity: 1, }, exit: { opacity: 0, }, }
const defaultItemAnimationVariants: Record< AnimationVariant, { container: Variants; item: Variants }
= { fadeIn: { container: defaultContainerVariants, item: { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0, transition: { duration: 0.3, }, }, exit: { opacity: 0, y: 20, transition: { duration: 0.3 }, }, }, }, blurIn: { container: defaultContainerVariants, item: { hidden: { opacity: 0, filter: "blur(10px)" }, show: { opacity: 1, filter: "blur(0px)", transition: { duration: 0.3, }, }, exit: { opacity: 0, filter: "blur(10px)", transition: { duration: 0.3 }, }, }, }, blurInUp: { container: defaultContainerVariants, item: { hidden: { opacity: 0, filter: "blur(10px)", y: 20 }, show: { opacity: 1, filter: "blur(0px)", y: 0, transition: { y: { duration: 0.3 }, opacity: { duration: 0.4 }, filter: { duration: 0.3 }, }, }, exit: { opacity: 0, filter: "blur(10px)", y: 20, transition: { y: { duration: 0.3 }, opacity: { duration: 0.4 }, filter: { duration: 0.3 }, }, }, }, }, blurInDown: { container: defaultContainerVariants, item: { hidden: { opacity: 0, filter: "blur(10px)", y: -20 }, show: { opacity: 1, filter: "blur(0px)", y: 0, transition: { y: { duration: 0.3 }, opacity: { duration: 0.4 }, filter: { duration: 0.3 }, }, }, }, }, slideUp: { container: defaultContainerVariants, item: { hidden: { y: 20, opacity: 0 }, show: { y: 0, opacity: 1, transition: { duration: 0.3, }, }, exit: { y: -20, opacity: 0, transition: { duration: 0.3, }, }, }, }, slideDown: { container: defaultContainerVariants, item: { hidden: { y: -20, opacity: 0 }, show: { y: 0, opacity: 1, transition: { duration: 0.3 }, }, exit: { y: 20, opacity: 0, transition: { duration: 0.3 }, }, }, }, slideLeft: { container: defaultContainerVariants, item: { hidden: { x: 20, opacity: 0 }, show: { x: 0, opacity: 1, transition: { duration: 0.3 }, }, exit: { x: -20, opacity: 0, transition: { duration: 0.3 }, }, }, }, slideRight: { container: defaultContainerVariants, item: { hidden: { x: -20, opacity: 0 }, show: { x: 0, opacity: 1, transition: { duration: 0.3 }, }, exit: { x: 20, opacity: 0, transition: { duration: 0.3 }, }, }, }, scaleUp: { container: defaultContainerVariants, item: { hidden: { scale: 0.5, opacity: 0 }, show: { scale: 1, opacity: 1, transition: { duration: 0.3, scale: { type: "spring", damping: 15, stiffness: 300, }, }, }, exit: { scale: 0.5, opacity: 0, transition: { duration: 0.3 }, }, }, }, scaleDown: { container: defaultContainerVariants, item: { hidden: { scale: 1.5, opacity: 0 }, show: { scale: 1, opacity: 1, transition: { duration: 0.3, scale: { type: "spring", damping: 15, stiffness: 300, }, }, }, exit: { scale: 1.5, opacity: 0, transition: { duration: 0.3 }, }, }, }, }
const TextAnimateBase = ({ children, delay = 0, duration = 0.3, variants, className, segmentClassName, as: Component = "p", startOnView = true, once = false, by = "word", animation = "fadeIn", accessible = true, ...props }: TextAnimateProps) => { const MotionComponent = motion.create(Component)
let segments: string[] = [] switch (by) { case "word": segments = children.split(/(\s+)/) break case "character": segments = children.split("") break case "line": segments = children.split("\n") break case "text": default: segments = [children] break }
const finalVariants = variants ? { container: { hidden: { opacity: 0 }, show: { opacity: 1, transition: { opacity: { duration: 0.01, delay }, delayChildren: delay, staggerChildren: duration / segments.length, }, }, exit: { opacity: 0, transition: { staggerChildren: duration / segments.length, staggerDirection: -1, }, }, }, item: variants, } : animation ? { container: { ...defaultItemAnimationVariants[animation].container, show: { ...defaultItemAnimationVariants[animation].container.show, transition: { delayChildren: delay, staggerChildren: duration / segments.length, }, }, exit: { ...defaultItemAnimationVariants[animation].container.exit, transition: { staggerChildren: duration / segments.length, staggerDirection: -1, }, }, }, item: defaultItemAnimationVariants[animation].item, } : { container: defaultContainerVariants, item: defaultItemVariants }
return (
<MotionComponent
variants={finalVariants.container as Variants}
initial="hidden"
whileInView={startOnView ? "show" : undefined}
animate={startOnView ? undefined : "show"}
exit="exit"
className={cn("whitespace-pre-wrap", className)}
viewport={{ once }}
aria-label={accessible ? children : undefined}
{...props}
>
{accessible && {children}}
{segments.map((segment, i) => (
<motion.span
key={${by}-${segment}-${i}}
variants={finalVariants.item}
custom={i * staggerTimings[by]}
className={cn(
by === "line" ? "block" : "inline-block whitespace-pre",
by === "character" && "",
segmentClassName
)}
aria-hidden={accessible ? true : undefined}
>
{segment}
</motion.span>
))}
)
}
// Export the memoized version export const TextAnimate = memo(TextAnimateBase)
===== EXAMPLE: text-animate-demo ===== Title: Text Animate Demo
--- file: example/text-animate-demo.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo() { return ( Blur in by character ) }
===== EXAMPLE: text-animate-demo-2 ===== Title: Text Animate Demo 2
--- file: example/text-animate-demo-2.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo2() { return ( Blur in text ) }
===== EXAMPLE: text-animate-demo-3 ===== Title: Text Animate Demo 3
--- file: example/text-animate-demo-3.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo3() { return ( Slide up by word ) }
===== EXAMPLE: text-animate-demo-4 ===== Title: Text Animate Demo 4
--- file: example/text-animate-demo-4.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo4() { return ( Scale up by text ) }
===== EXAMPLE: text-animate-demo-5 ===== Title: Text Animate Demo 5
--- file: example/text-animate-demo-5.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo5() {
return (
{Fade in by line as paragraph\n\nFade in by line as paragraph\n\nFade in by line as paragraph}
)
}
===== EXAMPLE: text-animate-demo-6 ===== Title: Text Animate Demo 6
--- file: example/text-animate-demo-6.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo6() { return ( Slide left by character ) }
===== EXAMPLE: text-animate-demo-7 ===== Title: Text Animate Demo 7
--- file: example/text-animate-demo-7.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo7() { return ( Blur in by character ) }
===== EXAMPLE: text-animate-demo-8 ===== Title: Text Animate Demo 8
--- file: example/text-animate-demo-8.tsx --- import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo8() { return ( Blur in by character ) }
===== EXAMPLE: text-animate-demo-9 ===== Title: Text Animate Demo 9
--- file: example/text-animate-demo-9.tsx --- "use client"
import { TextAnimate } from "@/registry/magicui/text-animate"
export default function TextAnimateDemo9() { return ( <TextAnimate variants={{ hidden: { opacity: 0, y: 30, rotate: 45, scale: 0.5, }, show: (i) => ({ opacity: 1, y: 0, rotate: 0, scale: 1, transition: { delay: i * 0.1, duration: 0.4, y: { type: "spring", damping: 12, stiffness: 200, mass: 0.8, }, rotate: { type: "spring", damping: 8, stiffness: 150, }, scale: { type: "spring", damping: 10, stiffness: 300, }, }, }), exit: (i) => ({ opacity: 0, y: 30, rotate: 45, scale: 0.5, transition: { delay: i * 0.1, duration: 0.4, }, }), }} by="character" > Wavy Motion! ) }
===== COMPONENT: text-reveal ===== Title: Text Reveal Description: Fade in text as you scroll down the page.
--- file: magicui/text-reveal.tsx --- "use client"
import { ComponentPropsWithoutRef, FC, ReactNode, useRef } from "react" import { motion, MotionValue, useScroll, useTransform } from "motion/react"
import { cn } from "@/lib/utils"
export interface TextRevealProps extends ComponentPropsWithoutRef<"div"> { children: string }
export const TextReveal: FC = ({ children, className }) => { const targetRef = useRef<HTMLDivElement | null>(null) const { scrollYProgress } = useScroll({ target: targetRef, })
if (typeof children !== "string") { throw new Error("TextReveal: children must be a string") }
const words = children.split(" ")
return ( <div ref={targetRef} className={cn("relative z-0 h-[200vh]", className)}> <div className={ "sticky top-0 mx-auto flex h-[50%] max-w-4xl items-center bg-transparent px-[1rem] py-[5rem]" } > <span ref={targetRef} className={ "flex flex-wrap p-5 text-2xl font-bold text-black/20 md:p-8 md:text-3xl lg:p-10 lg:text-4xl xl:text-5xl dark:text-white/20" } > {words.map((word, i) => { const start = i / words.length const end = start + 1 / words.length return ( <Word key={i} progress={scrollYProgress} range={[start, end]}> {word} ) })} ) }
interface WordProps { children: ReactNode progress: MotionValue range: [number, number] }
const Word: FC = ({ children, progress, range }) => { const opacity = useTransform(progress, range, [0, 1]) return ( {children} <motion.span style={{ opacity: opacity }} className={"text-black dark:text-white"} > {children} </motion.span> ) }
===== EXAMPLE: text-reveal-demo ===== Title: Text Reveal Demo
--- file: example/text-reveal-demo.tsx --- import { TextReveal } from "@/registry/magicui/text-reveal"
export default function TextRevealDemo() { return Magic UI will change the way you design. }
===== COMPONENT: tweet-card ===== Title: Tweet Card Description: A card that displays a tweet with the author's name, handle, and profile picture.
--- file: magicui/tweet-card.tsx --- /* eslint-disable @next/next/no-img-element */ import { Suspense } from "react" import { enrichTweet, type EnrichedTweet, type TweetProps } from "react-tweet" import { getTweet, type Tweet } from "react-tweet/api"
import { cn } from "@/lib/utils"
interface TwitterIconProps { className?: string [key: string]: unknown } const Twitter = ({ className, ...props }: TwitterIconProps) => ( <svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg" className={className} {...props}
<g>
<path fill="none" d="M0 0h24v24H0z"></path>
<path d="M22.162 5.656a8.384 8.384 0 0 1-2.402.658A4.196 4.196 0 0 0 21.6 4c-.82.488-1.719.83-2.656 1.015a4.182 4.182 0 0 0-7.126 3.814 11.874 11.874 0 0 1-8.62-4.37 4.168 4.168 0 0 0-.566 2.103c0 1.45.738 2.731 1.86 3.481a4.168 4.168 0 0 1-1.894-.523v.052a4.185 4.185 0 0 0 3.355 4.101 4.21 4.21 0 0 1-1.89.072A4.185 4.185 0 0 0 7.97 16.65a8.394 8.394 0 0 1-6.191 1.732 11.83 11.83 0 0 0 6.41 1.88c7.693 0 11.9-6.373 11.9-11.9 0-.18-.005-.362-.013-.54a8.496 8.496 0 0 0 2.087-2.165z"></path>
</g>
const Verified = ({ className, ...props }: TwitterIconProps) => ( <svg aria-label="Verified Account" viewBox="0 0 24 24" className={className} {...props}
<g fill="currentColor">
<path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z" />
</g>
export const truncate = (str: string | null, length: number) => {
if (!str || str.length <= length) return str
return ${str.slice(0, length - 3)}...
}
const Skeleton = ({ className, ...props }: React.HTMLAttributes) => { return ( <div className={cn("bg-primary/10 rounded-md", className)} {...props} /> ) }
export const TweetSkeleton = ({ className, ...props }: { className?: string [key: string]: unknown }) => (
export const TweetNotFound = ({ className, ...props }: { className?: string [key: string]: unknown }) => (
export const TweetHeader = ({ tweet }: { tweet: EnrichedTweet }) => (
export const TweetBody = ({ tweet }: { tweet: EnrichedTweet }) => (
export const TweetMedia = ({ tweet }: { tweet: EnrichedTweet }) => { if (!tweet.video && !tweet.photos) return null return ( {tweet.video && ( Your browser does not support the video tag. )} {tweet.photos && ( {tweet.photos.map((photo) => ( <img key={photo.url} src={photo.url} width={photo.width} height={photo.height} title={"Photo by " + tweet.user.name} alt={tweet.text} className="h-64 w-5/6 shrink-0 snap-center snap-always rounded-xl border object-cover shadow-sm" /> ))} )} {!tweet.video && !tweet.photos && // @ts-expect-error package doesn't have type definitions tweet?.card?.binding_values?.thumbnail_image_large?.image_value.url && ( <img src={ // @ts-expect-error package doesn't have type definitions tweet.card.binding_values.thumbnail_image_large.image_value.url } className="h-64 rounded-xl border object-cover shadow-sm" alt={tweet.text} /> )} ) }
export const MagicTweet = ({ tweet, className, ...props }: { tweet: Tweet className?: string }) => { const enrichedTweet = enrichTweet(tweet) return ( <div className={cn( "relative flex h-fit w-full max-w-lg flex-col gap-4 overflow-hidden rounded-xl border p-5", className )} {...props} > ) }
/**
- TweetCard (Server Side Only) */ export const TweetCard = async ({ id, components, fallback = , onError, ...props }: TweetProps & { className?: string }) => { const tweet = id ? await getTweet(id).catch((err) => { if (onError) { onError(err) } else { console.error(err) } }) : undefined
if (!tweet) { const NotFound = components?.TweetNotFound || TweetNotFound return <NotFound {...props} /> }
return ( <MagicTweet tweet={tweet} {...props} /> ) }
===== EXAMPLE: tweet-card-demo ===== Title: Tweet Card Demo
--- file: example/tweet-card-demo.tsx --- import { ClientTweetCard } from "@/registry/magicui/client-tweet-card"
/**
- (Server Side Only)
- (Client Side Only) */ export default function TweetDemo() { return }
===== EXAMPLE: tweet-card-images ===== Title: Tweet Card Images
--- file: example/tweet-card-images.tsx --- import { ClientTweetCard } from "@/registry/magicui/client-tweet-card"
export default function TweetImages() { return }
===== EXAMPLE: tweet-card-meta-preview ===== Title: Tweet Card Meta Preview
--- file: example/tweet-card-meta-preview.tsx --- import { ClientTweetCard } from "@/registry/magicui/client-tweet-card"
export default function TweetMetaPreview() { return }
===== COMPONENT: typing-animation ===== Title: Typing Animation Description: Characters appearing in typed animation
--- file: magicui/typing-animation.tsx --- "use client"
import { useEffect, useMemo, useRef, useState } from "react" import { motion, MotionProps, useInView } from "motion/react"
import { cn } from "@/lib/utils"
interface TypingAnimationProps extends MotionProps { children?: string words?: string[] className?: string duration?: number typeSpeed?: number deleteSpeed?: number delay?: number pauseDelay?: number loop?: boolean as?: React.ElementType startOnView?: boolean showCursor?: boolean blinkCursor?: boolean cursorStyle?: "line" | "block" | "underscore" }
export function TypingAnimation({ children, words, className, duration = 100, typeSpeed, deleteSpeed, delay = 0, pauseDelay = 1000, loop = false, as: Component = "span", startOnView = true, showCursor = true, blinkCursor = true, cursorStyle = "line", ...props }: TypingAnimationProps) { const MotionComponent = motion.create(Component, { forwardMotionProps: true, })
const [displayedText, setDisplayedText] = useState("") const [currentWordIndex, setCurrentWordIndex] = useState(0) const [currentCharIndex, setCurrentCharIndex] = useState(0) const [phase, setPhase] = useState<"typing" | "pause" | "deleting">("typing") const elementRef = useRef<HTMLElement | null>(null) const isInView = useInView(elementRef as React.RefObject, { amount: 0.3, once: true, })
const wordsToAnimate = useMemo( () => words || (children ? [children] : []), [words, children] ) const hasMultipleWords = wordsToAnimate.length > 1
const typingSpeed = typeSpeed || duration const deletingSpeed = deleteSpeed || typingSpeed / 2
const shouldStart = startOnView ? isInView : true
useEffect(() => { if (!shouldStart || wordsToAnimate.length === 0) return
const timeoutDelay =
delay > 0 && displayedText === ""
? delay
: phase === "typing"
? typingSpeed
: phase === "deleting"
? deletingSpeed
: pauseDelay
const timeout = setTimeout(() => {
const currentWord = wordsToAnimate[currentWordIndex] || ""
const graphemes = Array.from(currentWord)
switch (phase) {
case "typing":
if (currentCharIndex < graphemes.length) {
setDisplayedText(graphemes.slice(0, currentCharIndex + 1).join(""))
setCurrentCharIndex(currentCharIndex + 1)
} else {
if (hasMultipleWords || loop) {
const isLastWord = currentWordIndex === wordsToAnimate.length - 1
if (!isLastWord || loop) {
setPhase("pause")
}
}
}
break
case "pause":
setPhase("deleting")
break
case "deleting":
if (currentCharIndex > 0) {
setDisplayedText(graphemes.slice(0, currentCharIndex - 1).join(""))
setCurrentCharIndex(currentCharIndex - 1)
} else {
const nextIndex = (currentWordIndex + 1) % wordsToAnimate.length
setCurrentWordIndex(nextIndex)
setPhase("typing")
}
break
}
}, timeoutDelay)
return () => clearTimeout(timeout)
}, [ shouldStart, phase, currentCharIndex, currentWordIndex, displayedText, wordsToAnimate, hasMultipleWords, loop, typingSpeed, deletingSpeed, pauseDelay, delay, ])
const currentWordGraphemes = Array.from( wordsToAnimate[currentWordIndex] || "" ) const isComplete = !loop && currentWordIndex === wordsToAnimate.length - 1 && currentCharIndex >= currentWordGraphemes.length && phase !== "deleting"
const shouldShowCursor = showCursor && !isComplete && (hasMultipleWords || loop || currentCharIndex < currentWordGraphemes.length)
const getCursorChar = () => { switch (cursorStyle) { case "block": return "▌" case "underscore": return "_" case "line": default: return "|" } }
return ( <MotionComponent ref={elementRef} className={cn("leading-[5rem] tracking-[-0.02em]", className)} {...props} > {displayedText} {shouldShowCursor && ( <span className={cn("inline-block", blinkCursor && "animate-blink-cursor")} > {getCursorChar()} )} ) }
===== EXAMPLE: typing-animation-demo ===== Title: Typing Animation Demo
--- file: example/typing-animation-demo.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return Hello World! 👋 }
===== EXAMPLE: typing-animation-demo-2 ===== Title: Typing Animation Multiple Words
--- file: example/typing-animation-demo-2.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return <TypingAnimation words={["Design 🎨", "Build 🔨", "Ship 🚀"]} loop /> }
===== EXAMPLE: typing-animation-demo-3 ===== Title: Typing Animation Custom Speed
--- file: example/typing-animation-demo-3.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return ( <TypingAnimation words={["Fast typing", "Slow delete"]} typeSpeed={50} deleteSpeed={150} pauseDelay={2000} loop /> ) }
===== EXAMPLE: typing-animation-demo-4 ===== Title: Typing Animation Start on View
--- file: example/typing-animation-demo-4.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return ( Starts typing when in view ) }
===== EXAMPLE: typing-animation-demo-5 ===== Title: Typing Animation Without Cursor
--- file: example/typing-animation-demo-5.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return No cursor shown }
===== EXAMPLE: typing-animation-demo-6 ===== Title: Typing Animation Single Play
--- file: example/typing-animation-demo-6.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return <TypingAnimation words={["First", "Second", "Final"]} loop={false} /> }
===== EXAMPLE: typing-animation-demo-7 ===== Title: Typing Animation Cursor Blinking
--- file: example/typing-animation-demo-7.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return ( With blinking cursor (default) - watch during pause <TypingAnimation words={["Type", "Pause", "Delete"]} blinkCursor={true} pauseDelay={2000} loop className="text-4xl font-bold" > Blinking cursor Without blinking cursor - static during pause <TypingAnimation words={["Type", "Pause", "Delete"]} blinkCursor={false} pauseDelay={2000} loop className="text-4xl font-bold" > Static cursor ) }
===== EXAMPLE: typing-animation-demo-8 ===== Title: Typing Animation Cursor Styles
--- file: example/typing-animation-demo-8.tsx --- import { TypingAnimation } from "@/registry/magicui/typing-animation"
export default function Component() { return ( Line cursor (default) <TypingAnimation words={["Line cursor"]} cursorStyle="line" loop className="text-4xl font-bold" /> Block cursor (VSCode style) <TypingAnimation words={["Block cursor"]} cursorStyle="block" loop className="text-4xl font-bold" /> Underscore cursor <TypingAnimation words={["Underscore cursor"]} cursorStyle="underscore" loop className="text-4xl font-bold" /> ) }
===== COMPONENT: video-text ===== Title: Video Text Description: A component that displays text with a video playing in the background.
--- file: magicui/video-text.tsx --- "use client"
import React, { ElementType, ReactNode, useEffect, useState } from "react"
import { cn } from "@/lib/utils"
export interface VideoTextProps { /**
- The video source URL / src: string /*
- Additional className for the container / className?: string /*
- Whether to autoplay the video / autoPlay?: boolean /*
- Whether to mute the video / muted?: boolean /*
- Whether to loop the video / loop?: boolean /*
- Whether to preload the video / preload?: "auto" | "metadata" | "none" /*
- The content to display (will have the video "inside" it) / children: ReactNode /*
- Font size for the text mask (in viewport width units)
- @default 10 / fontSize?: string | number /*
- Font weight for the text mask
- @default "bold" / fontWeight?: string | number /*
- Text anchor for the text mask
- @default "middle" / textAnchor?: string /*
- Dominant baseline for the text mask
- @default "middle" / dominantBaseline?: string /*
- Font family for the text mask
- @default "sans-serif" / fontFamily?: string /*
- The element type to render for the text
- @default "div" */ as?: ElementType }
export function VideoText({ src, children, className = "", autoPlay = true, muted = true, loop = true, preload = "auto", fontSize = 20, fontWeight = "bold", textAnchor = "middle", dominantBaseline = "middle", fontFamily = "sans-serif", as: Component = "div", }: VideoTextProps) { const [svgMask, setSvgMask] = useState("") const content = React.Children.toArray(children).join("")
useEffect(() => {
const updateSvgMask = () => {
const responsiveFontSize =
typeof fontSize === "number" ? ${fontSize}vw : fontSize
const newSvgMask = <svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'><text x='50%' y='50%' font-size='${responsiveFontSize}' font-weight='${fontWeight}' text-anchor='${textAnchor}' dominant-baseline='${dominantBaseline}' font-family='${fontFamily}'>${content}</text></svg>
setSvgMask(newSvgMask)
}
updateSvgMask()
window.addEventListener("resize", updateSvgMask)
return () => window.removeEventListener("resize", updateSvgMask)
}, [content, fontSize, fontWeight, textAnchor, dominantBaseline, fontFamily])
const dataUrlMask = url("data:image/svg+xml,${encodeURIComponent(svgMask)}")
return (
<Component className={cn(relative size-full, className)}>
{/* Create a container that masks the video to only show within text */}
<div
className="absolute inset-0 flex items-center justify-center"
style={{
maskImage: dataUrlMask,
WebkitMaskImage: dataUrlMask,
maskSize: "contain",
WebkitMaskSize: "contain",
maskRepeat: "no-repeat",
WebkitMaskRepeat: "no-repeat",
maskPosition: "center",
WebkitMaskPosition: "center",
}}
>
Your browser does not support the video tag.
{/* Add a backup text element for SEO/accessibility */}
<span className="sr-only">{content}</span>
</Component>
) }
===== EXAMPLE: video-text-demo ===== Title: Video Text Demo
--- file: example/video-text-demo.tsx --- import { VideoText } from "@/registry/magicui/video-text"
export default function VideoTextDemo() { return ( OCEAN ) }
===== COMPONENT: warp-background ===== Title: Warp Background Description: A card with a time warping background effect.
--- file: magicui/warp-background.tsx --- "use client"
import React, { HTMLAttributes, useCallback, useMemo } from "react" import { motion } from "motion/react"
import { cn } from "@/lib/utils"
interface WarpBackgroundProps extends HTMLAttributes { children: React.ReactNode perspective?: number beamsPerSide?: number beamSize?: number beamDelayMax?: number beamDelayMin?: number beamDuration?: number gridColor?: string }
const Beam = ({ width, x, delay, duration, }: { width: string | number x: string | number delay: number duration: number }) => { const hue = Math.floor(Math.random() * 360) const ar = Math.floor(Math.random() * 10) + 1
return (
<motion.div
style={
{
"--x": ${x},
"--width": ${width},
"--aspect-ratio": ${ar},
"--background": linear-gradient(hsl(${hue} 80% 60%), transparent),
} as React.CSSProperties
}
className={absolute top-0 left-[var(--x)] [aspect-ratio:1/var(--aspect-ratio)] [width:var(--width)] [background:var(--background)]}
initial={{ y: "100cqmax", x: "-50%" }}
animate={{ y: "-100%", x: "-50%" }}
transition={{
duration,
delay,
repeat: Infinity,
ease: "linear",
}}
/>
)
}
export const WarpBackground: React.FC = ({ children, perspective = 100, className, beamsPerSide = 3, beamSize = 5, beamDelayMax = 3, beamDelayMin = 0, beamDuration = 3, gridColor = "var(--border)", ...props }) => { const generateBeams = useCallback(() => { const beams = [] const cellsPerSide = Math.floor(100 / beamSize) const step = cellsPerSide / beamsPerSide
for (let i = 0; i < beamsPerSide; i++) {
const x = Math.floor(i * step)
const delay = Math.random() * (beamDelayMax - beamDelayMin) + beamDelayMin
beams.push({ x, delay })
}
return beams
}, [beamsPerSide, beamSize, beamDelayMax, beamDelayMin])
const topBeams = useMemo(() => generateBeams(), [generateBeams]) const rightBeams = useMemo(() => generateBeams(), [generateBeams]) const bottomBeams = useMemo(() => generateBeams(), [generateBeams]) const leftBeams = useMemo(() => generateBeams(), [generateBeams])
return (
<div className={cn("relative rounded border p-20", className)} {...props}>
<div
style={
{
"--perspective": ${perspective}px,
"--grid-color": gridColor,
"--beam-size": ${beamSize}%,
} as React.CSSProperties
}
className={
"[container-type:size] pointer-events-none absolute top-0 left-0 size-full overflow-hidden [clipPath:inset(0)] [perspective:var(--perspective)] [transform-style:preserve-3d]"
}
>
{/* top side /}
{topBeams.map((beam, index) => (
<Beam
key={top-${index}}
width={${beamSize}%}
x={${beam.x * beamSize}%}
delay={beam.delay}
duration={beamDuration}
/>
))}
{/ bottom side /}
{bottomBeams.map((beam, index) => (
<Beam
key={bottom-${index}}
width={${beamSize}%}
x={${beam.x * beamSize}%}
delay={beam.delay}
duration={beamDuration}
/>
))}
{/ left side /}
{leftBeams.map((beam, index) => (
<Beam
key={left-${index}}
width={${beamSize}%}
x={${beam.x * beamSize}%}
delay={beam.delay}
duration={beamDuration}
/>
))}
{/ right side */}
{rightBeams.map((beam, index) => (
<Beam
key={right-${index}}
width={${beamSize}%}
x={${beam.x * beamSize}%}
delay={beam.delay}
duration={beamDuration}
/>
))}
{children}
)
}
===== EXAMPLE: warp-background-demo ===== Title: Warp Background Demo
--- file: example/warp-background-demo.tsx --- import { Card, CardContent, CardDescription, CardTitle, } from "@/components/ui/card" import { WarpBackground } from "@/registry/magicui/warp-background"
export default function ExampleComponentDemo() { return ( Congratulations on Your Promotion! Your hard work and dedication have paid off. We're thrilled to see you take this next step in your career. Keep up the fantastic work! ) }
===== COMPONENT: word-rotate ===== Title: Word Rotate Description: A vertical rotation of words
--- file: magicui/word-rotate.tsx --- "use client"
import { useEffect, useState } from "react" import { AnimatePresence, motion, MotionProps } from "motion/react"
import { cn } from "@/lib/utils"
interface WordRotateProps { words: string[] duration?: number motionProps?: MotionProps className?: string }
export function WordRotate({ words, duration = 2500, motionProps = { initial: { opacity: 0, y: -50 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: 50 }, transition: { duration: 0.25, ease: "easeOut" }, }, className, }: WordRotateProps) { const [index, setIndex] = useState(0)
useEffect(() => { const interval = setInterval(() => { setIndex((prevIndex) => (prevIndex + 1) % words.length) }, duration)
// Clean up interval on unmount
return () => clearInterval(interval)
}, [words, duration])
return ( <motion.h1 key={words[index]} className={cn(className)} {...motionProps} > {words[index]} </motion.h1> ) }
===== EXAMPLE: word-rotate-demo ===== Title: Word Rotate Demo
--- file: example/word-rotate-demo.tsx --- import { WordRotate } from "@/registry/magicui/word-rotate"
export default function WordRotateDemo() { return ( <WordRotate className="text-4xl font-bold text-black dark:text-white" words={["Word", "Rotate"]} /> ) }
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon