feat: Add Magic UI components including DotPattern, RainbowButton, ShineBorder, WordRotate, and BlurIn, along with corresponding Tailwind configuration updates.

This commit is contained in:
2026-02-03 16:23:28 +00:00
parent 8d1923203d
commit 494acebeb3
14 changed files with 752 additions and 237 deletions

120
components/hero-shader.tsx Normal file
View File

@@ -0,0 +1,120 @@
"use client";
import { useEffect, useRef } from "react";
export function HeroShader() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
let t = 0;
let animationFrameId: number;
const resize = () => {
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
};
resize();
window.addEventListener("resize", resize);
const draw = () => {
// Clear canvas with some transparency for trails?
// Or just clear completely given the math looks like it redraws?
// Tweet says background(9), which is very dark.
// Tweetcarts usually redraw fully or rely on trails.
// Given complexity, let's clear fully first.
ctx.fillStyle = "#090909"; // background(9) approximation
// ctx.fillRect(0, 0, canvas.width, canvas.height);
// Actually, let's make it transparent so it blends with our site
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillStyle = "rgba(255, 255, 255, 0.5)"; // stroke(w, 116) approx white with alpha
// Center of drawing - offset to the right
const cx = canvas.width * 0.75;
const cy = canvas.height / 2;
// Loop for points
// for(t+=PI/90,i=1e4;i--;)a()
t += Math.PI / 90;
for (let i = 10000; i > 0; i--) {
// y = i / 790
let y = i / 790;
// k = (y<8 ? 9+sin(y^9)*6 : 4+cos(y)) * cos(i+t/4)
// Note: processing sin(y^9) is bitwise XOR in JS, but usually in math it's power?
// "y^9" in many tweetcarts (JS) is XOR if it's straight JS evaluation,
// but if it's GLSL it might mean pow.
// Given "tweetcart", it's likely JS/Processing, where ^ is XOR in standard JS but often used as Pow in math context?
// Processing language: ^ is bitwise XOR. pow(n, e) is power.
// Let's assume XOR as it's common in condensed code.
// Wait, standard JS `^` is XOR.
let k_term1 = (y < 8)
? (9 + Math.sin(y ** 9) * 6) // Trying Power first as it produces curves
: (4 + Math.cos(y));
// Wait, dweet/tweetcart usually use JS syntax.
// Let's try to replicate exact syntax logic.
// Logic: k = (condition) * cos(i + t/4)
// Re-evaluating y^9. If y is float, XOR converts to int.
// y = i / 790, which is float.
// Let's stick to Math.pow for smoother graphs if intended.
// But let's try standard JS behavior for ^ (XOR) might be intended for glitchy look?
// Let's try power first.
const k = ((y < 8) ? (9 + Math.sin(Math.pow(y, 9)) * 6) : (4 + Math.cos(y))) * Math.cos(i + t / 4);
// e = y/3 - 13 + cos(e ?? no, that was likely comma operator)
// Original: d=mag(k=(...), e=y/3-13) + cos(e+t*2+i%2*4)
// mag(a, b) = sqrt(a*a + b*b)
// So args to mag are k and e.
const e = y / 3 - 13;
const mag_ke = Math.sqrt(k * k + e * e);
// d = mag(...) + cos(e + t*2 + i%2*4)
const d = mag_ke + Math.cos(e + t * 2 + (i % 2) * 4);
// c = d/4 - t/2 + i%2*3
const c = d / 4 - t / 2 + (i % 2) * 3;
// q = y * k / 5 * (2 + sin(d*2 + y - t*4)) + 80
const q = y * k / 5 * (2 + Math.sin(d * 2 + y - t * 4)) + 80;
// x = q * cos(c) + 200
// y_out = q * sin(c) + d * 9 + 60
// 200 and 60 are likely offsets for 400x400 canvas.
// We should center it.
const x = (q * Math.cos(c)) + cx;
const y_out = (q * Math.sin(c) + d * 9) + cy;
ctx.fillRect(x, y_out, 1.5, 1.5);
}
animationFrameId = requestAnimationFrame(draw);
};
draw();
return () => {
window.removeEventListener("resize", resize);
cancelAnimationFrame(animationFrameId);
};
}, []);
return (
<canvas
ref={canvasRef}
className="absolute inset-0 h-full w-full opacity-60 mix-blend-screen pointer-events-none"
/>
);
}

View File

@@ -0,0 +1,43 @@
"use client";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface BlurInProps {
word: string;
className?: string;
variant?: {
hidden: { filter: string; opacity: number };
visible: { filter: string; opacity: number };
};
duration?: number;
}
export function BlurIn({
word,
className,
variant,
duration = 1,
}: BlurInProps) {
const defaultVariants = {
hidden: { filter: "blur(10px)", opacity: 0 },
visible: { filter: "blur(0px)", opacity: 1 },
};
const combinedVariants = variant || defaultVariants;
return (
<motion.h1
initial="hidden"
animate="visible"
transition={{ duration }}
variants={combinedVariants}
className={cn(
"font-display text-center text-4xl font-bold tracking-[-0.02em] drop-shadow-sm md:text-7xl md:leading-[5rem]",
className,
)}
>
{word}
</motion.h1>
);
}

View File

@@ -0,0 +1,55 @@
import { useId } from "react";
import { cn } from "@/lib/utils";
interface DotPatternProps {
width?: any;
height?: any;
x?: any;
y?: any;
cx?: any;
cy?: any;
cr?: any;
className?: string;
[key: string]: any;
}
export function DotPattern({
width = 16,
height = 16,
x = 0,
y = 0,
cx = 1,
cy = 1,
cr = 1,
className,
...props
}: DotPatternProps) {
const id = useId();
return (
<svg
aria-hidden="true"
className={cn(
"pointer-events-none absolute inset-0 h-full w-full fill-neutral-400/80",
className,
)}
{...props}
>
<defs>
<pattern
id={id}
width={width}
height={height}
patternUnits="userSpaceOnUse"
patternContentUnits="userSpaceOnUse"
x={x}
y={y}
>
<circle id="pattern-circle" cx={cx} cy={cy} r={cr} />
</pattern>
</defs>
<rect width="100%" height="100%" strokeWidth={0} fill={`url(#${id})`} />
</svg>
);
}

View File

@@ -0,0 +1,24 @@
import React from "react";
import { cn } from "@/lib/utils";
interface RainbowButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> { }
export function RainbowButton({
children,
className,
...props
}: RainbowButtonProps) {
return (
<button
className={cn(
"group relative inline-flex h-11 animate-rainbow cursor-pointer items-center justify-center rounded-xl border-0 bg-[length:200%] px-8 py-2 font-medium text-primary-foreground transition-colors [background-image:linear-gradient(90deg,hsl(var(--color-1)),hsl(var(--color-5)),hsl(var(--color-3)),hsl(var(--color-4)),hsl(var(--color-2)))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
>
{children}
</button>
);
}

View File

@@ -0,0 +1,62 @@
"use client";
import { cn } from "@/lib/utils";
type TColorProp = string | string[];
interface ShineBorderProps {
borderRadius?: number;
borderWidth?: number;
duration?: number;
color?: TColorProp;
className?: string;
children: React.ReactNode;
}
/**
* @name Shine Border
* @description It is an animated background border effect component with easy to use and configurable props.
* @param borderRadius defines the radius of the border.
* @param borderWidth defines the width of the border.
* @param duration defines the animation duration to be applied on the shining border
* @param color a string or string array to define border color.
* @param className defines the class name to be applied to the component
* @param children nodes to be rendered in the center of the component
*/
export function ShineBorder({
borderRadius = 8,
borderWidth = 1,
duration = 14,
color = "#000000",
className,
children,
}: ShineBorderProps) {
return (
<div
style={
{
"--border-radius": `${borderRadius}px`,
} as React.CSSProperties
}
className={cn(
"relative min-h-[60px] w-fit min-w-[300px] place-items-center rounded-[--border-radius] bg-white p-3 text-black dark:bg-black dark:text-white",
className,
)}
>
<div
style={
{
"--border-width": `${borderWidth}px`,
"--border-radius": `${borderRadius}px`,
"--duration": `${duration}s`,
"--mask-linear-gradient": `linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)`,
"--background-radial-gradient": `radial-gradient(transparent,transparent, ${Array.isArray(color) ? color.join(",") : color
},transparent,transparent)`,
} as React.CSSProperties
}
className={`before:bg-shine-size before:absolute before:inset-0 before:aspect-square before:size-full before:rounded-[--border-radius] before:p-[--border-width] before:will-change-[background-position] before:content-[""] before:![-webkit-mask-composite:xor] before:![mask-composite:exclude] before:[background-image:--background-radial-gradient] before:[background-size:300%_300%] before:[mask:--mask-linear-gradient] motion-safe:before:animate-shine`}
></div>
{children}
</div>
);
}

View File

@@ -0,0 +1,48 @@
"use client";
import { cn } from "@/lib/utils";
import { AnimatePresence, HTMLMotionProps, motion } from "framer-motion";
import { useEffect, useState } from "react";
interface WordRotateProps {
words: string[];
duration?: number;
framerProps?: HTMLMotionProps<"h1">;
className?: string;
}
export function WordRotate({
words,
duration = 2500,
framerProps = {
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);
return () => clearInterval(interval);
}, [words, duration]);
return (
<div className="overflow-hidden py-2">
<AnimatePresence mode="wait">
<motion.h1
key={words[index]}
className={cn(className)}
{...framerProps}
>
{words[index]}
</motion.h1>
</AnimatePresence>
</div>
);
}