diff --git a/components.json b/components.json
new file mode 100644
index 0000000..fec6d0e
--- /dev/null
+++ b/components.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://ui.shadcn.com/schema.json",
+ "style": "new-york",
+ "rsc": false,
+ "tsx": true,
+ "tailwind": {
+ "config": "",
+ "css": "src/index.css",
+ "baseColor": "neutral",
+ "cssVariables": true,
+ "prefix": ""
+ },
+ "iconLibrary": "lucide",
+ "aliases": {
+ "components": "@/components",
+ "utils": "@/lib/utils",
+ "ui": "@/components/ui",
+ "lib": "@/lib",
+ "hooks": "@/hooks"
+ },
+ "registries": {
+ "@magicui": "https://magicui.design/r/{name}.json"
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index b4085e6..a0b9c28 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,9 +8,15 @@
"name": "bcdigital-challenge",
"version": "0.0.0",
"dependencies": {
+ "@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.16",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.552.0",
+ "motion": "^12.23.24",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
@@ -27,6 +33,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.6",
"prettier": "3.6.2",
+ "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
@@ -1022,6 +1029,39 @@
"node": ">= 8"
}
},
+ "node_modules/@radix-ui/react-compose-refs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
+ "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.43",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.43.tgz",
@@ -1644,7 +1684,7 @@
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.0.2"
@@ -2167,6 +2207,18 @@
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
+ "node_modules/class-variance-authority": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/cli-cursor": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@@ -2200,6 +2252,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -2270,7 +2331,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
- "dev": true,
+ "devOptional": true,
"license": "MIT"
},
"node_modules/debug": {
@@ -2736,6 +2797,33 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/framer-motion": {
+ "version": "12.23.24",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz",
+ "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^12.23.23",
+ "motion-utils": "^12.23.6",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3376,6 +3464,15 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/lucide-react": {
+ "version": "0.552.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.552.0.tgz",
+ "integrity": "sha512-g9WCjmfwqbexSnZE+2cl21PCfXOcqnGeWeMTNAOGEfpPbm/ZF4YIq77Z8qWrxbu660EKuLB4nSLggoKnCb+isw==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/magic-string": {
"version": "0.30.21",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
@@ -3435,6 +3532,47 @@
"node": "*"
}
},
+ "node_modules/motion": {
+ "version": "12.23.24",
+ "resolved": "https://registry.npmjs.org/motion/-/motion-12.23.24.tgz",
+ "integrity": "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw==",
+ "license": "MIT",
+ "dependencies": {
+ "framer-motion": "^12.23.24",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "12.23.23",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz",
+ "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^12.23.6"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "12.23.6",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
+ "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -4004,6 +4142,16 @@
"node": ">=8"
}
},
+ "node_modules/tailwind-merge": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
+ "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/dcastil"
+ }
+ },
"node_modules/tailwindcss": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
@@ -4094,6 +4242,22 @@
"typescript": ">=4.8.4"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/tw-animate-css": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
+ "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Wombosvideo"
+ }
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index 3e42f11..09c430c 100644
--- a/package.json
+++ b/package.json
@@ -16,9 +16,15 @@
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
+ "@radix-ui/react-slot": "^1.2.3",
"@tailwindcss/vite": "^4.1.16",
+ "class-variance-authority": "^0.7.1",
+ "clsx": "^2.1.1",
+ "lucide-react": "^0.552.0",
+ "motion": "^12.23.24",
"react": "^19.1.1",
"react-dom": "^19.1.1",
+ "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.16"
},
"devDependencies": {
@@ -35,6 +41,7 @@
"husky": "^9.1.7",
"lint-staged": "^16.2.6",
"prettier": "3.6.2",
+ "tw-animate-css": "^1.4.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.45.0",
"vite": "^7.1.7"
diff --git a/src/app.tsx b/src/app.tsx
index 406748e..7fd6599 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,3 +1,8 @@
+import { AuroraText } from "./components/ui/aurora-text";
+import { DotPattern } from "./components/ui/dot-pattern";
+import { RainbowButton } from "./components/ui/rainbow-button";
+import { TextAnimate } from "./components/ui/text-animate";
+
export function App() {
return (
@@ -19,25 +24,28 @@ export function App() {
"
>
-
-
+
+
- Orchestral Event Management
+ Orchestral Event Management
-
without the hassle
-
-
-
-
+
+ Without The Hassle
+
+
Learn More
-
Section 2 coming soon...
+
+
+ Section 2 coming soon...
+
);
diff --git a/src/components/ui/aurora-text.tsx b/src/components/ui/aurora-text.tsx
new file mode 100644
index 0000000..30f0e6a
--- /dev/null
+++ b/src/components/ui/aurora-text.tsx
@@ -0,0 +1,41 @@
+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 (
+
+ {children}
+
+ {children}
+
+
+ );
+ },
+);
+
+AuroraText.displayName = "AuroraText";
diff --git a/src/components/ui/dot-pattern.tsx b/src/components/ui/dot-pattern.tsx
new file mode 100644
index 0000000..d904872
--- /dev/null
+++ b/src/components/ui/dot-pattern.tsx
@@ -0,0 +1,156 @@
+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
+ *
+ *
+ * @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 (
+
+ );
+}
diff --git a/src/components/ui/rainbow-button.tsx b/src/components/ui/rainbow-button.tsx
new file mode 100644
index 0000000..ea827e1
--- /dev/null
+++ b/src/components/ui/rainbow-button.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type 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(
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
+ const Comp = asChild ? Slot : "button";
+ return (
+
+ );
+ },
+);
+
+RainbowButton.displayName = "RainbowButton";
+
+export { RainbowButton, rainbowButtonVariants, type RainbowButtonProps };
diff --git a/src/components/ui/text-animate.tsx b/src/components/ui/text-animate.tsx
new file mode 100644
index 0000000..a6f6cdd
--- /dev/null
+++ b/src/components/ui/text-animate.tsx
@@ -0,0 +1,422 @@
+import { type ElementType, memo } from "react";
+import {
+ AnimatePresence,
+ motion,
+ type MotionProps,
+ type 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 = {
+ 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 (
+
+
+ {accessible && {children}}
+ {segments.map((segment, i) => (
+
+ {segment}
+
+ ))}
+
+
+ );
+};
+
+// Export the memoized version
+export const TextAnimate = memo(TextAnimateBase);
diff --git a/src/index.css b/src/index.css
index f1d8c73..58878da 100644
--- a/src/index.css
+++ b/src/index.css
@@ -1 +1,167 @@
@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+ --radius-sm: calc(var(--radius) - 4px);
+ --radius-md: calc(var(--radius) - 2px);
+ --radius-lg: var(--radius);
+ --radius-xl: calc(var(--radius) + 4px);
+ --color-background: var(--background);
+ --color-foreground: var(--foreground);
+ --color-card: var(--card);
+ --color-card-foreground: var(--card-foreground);
+ --color-popover: var(--popover);
+ --color-popover-foreground: var(--popover-foreground);
+ --color-primary: var(--primary);
+ --color-primary-foreground: var(--primary-foreground);
+ --color-secondary: var(--secondary);
+ --color-secondary-foreground: var(--secondary-foreground);
+ --color-muted: var(--muted);
+ --color-muted-foreground: var(--muted-foreground);
+ --color-accent: var(--accent);
+ --color-accent-foreground: var(--accent-foreground);
+ --color-destructive: var(--destructive);
+ --color-border: var(--border);
+ --color-input: var(--input);
+ --color-ring: var(--ring);
+ --color-chart-1: var(--chart-1);
+ --color-chart-2: var(--chart-2);
+ --color-chart-3: var(--chart-3);
+ --color-chart-4: var(--chart-4);
+ --color-chart-5: var(--chart-5);
+ --color-sidebar: var(--sidebar);
+ --color-sidebar-foreground: var(--sidebar-foreground);
+ --color-sidebar-primary: var(--sidebar-primary);
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+ --color-sidebar-accent: var(--sidebar-accent);
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+ --color-sidebar-border: var(--sidebar-border);
+ --color-sidebar-ring: var(--sidebar-ring);
+ --animate-aurora: aurora 8s ease-in-out infinite alternate;
+ @keyframes aurora {
+ 0% {
+ background-position: 0% 50%;
+ transform: rotate(-5deg) scale(0.9);
+ }
+ 25% {
+ background-position: 50% 100%;
+ transform: rotate(5deg) scale(1.1);
+ }
+ 50% {
+ background-position: 100% 50%;
+ transform: rotate(-3deg) scale(0.95);
+ }
+ 75% {
+ background-position: 50% 0%;
+ transform: rotate(3deg) scale(1.05);
+ }
+ 100% {
+ background-position: 0% 50%;
+ transform: rotate(-5deg) scale(0.9);
+ }
+ }
+ --animate-rainbow: rainbow var(--speed, 2s) infinite linear;
+ --color-color-5: var(--color-5);
+ --color-color-4: var(--color-4);
+ --color-color-3: var(--color-3);
+ --color-color-2: var(--color-2);
+ --color-color-1: var(--color-1);
+ @keyframes rainbow {
+ 0% {
+ background-position: 0%;
+ }
+ 100% {
+ background-position: 200%;
+ }
+ }
+}
+
+:root {
+ --radius: 0.625rem;
+ --background: oklch(1 0 0);
+ --foreground: oklch(0.145 0 0);
+ --card: oklch(1 0 0);
+ --card-foreground: oklch(0.145 0 0);
+ --popover: oklch(1 0 0);
+ --popover-foreground: oklch(0.145 0 0);
+ --primary: oklch(0.205 0 0);
+ --primary-foreground: oklch(0.985 0 0);
+ --secondary: oklch(0.97 0 0);
+ --secondary-foreground: oklch(0.205 0 0);
+ --muted: oklch(0.97 0 0);
+ --muted-foreground: oklch(0.556 0 0);
+ --accent: oklch(0.97 0 0);
+ --accent-foreground: oklch(0.205 0 0);
+ --destructive: oklch(0.577 0.245 27.325);
+ --border: oklch(0.922 0 0);
+ --input: oklch(0.922 0 0);
+ --ring: oklch(0.708 0 0);
+ --chart-1: oklch(0.646 0.222 41.116);
+ --chart-2: oklch(0.6 0.118 184.704);
+ --chart-3: oklch(0.398 0.07 227.392);
+ --chart-4: oklch(0.828 0.189 84.429);
+ --chart-5: oklch(0.769 0.188 70.08);
+ --sidebar: oklch(0.985 0 0);
+ --sidebar-foreground: oklch(0.145 0 0);
+ --sidebar-primary: oklch(0.205 0 0);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.97 0 0);
+ --sidebar-accent-foreground: oklch(0.205 0 0);
+ --sidebar-border: oklch(0.922 0 0);
+ --sidebar-ring: oklch(0.708 0 0);
+ --color-1: oklch(66.2% 0.225 25.9);
+ --color-2: oklch(60.4% 0.26 302);
+ --color-3: oklch(69.6% 0.165 251);
+ --color-4: oklch(80.2% 0.134 225);
+ --color-5: oklch(90.7% 0.231 133);
+}
+
+.dark {
+ --background: oklch(0.145 0 0);
+ --foreground: oklch(0.985 0 0);
+ --card: oklch(0.205 0 0);
+ --card-foreground: oklch(0.985 0 0);
+ --popover: oklch(0.205 0 0);
+ --popover-foreground: oklch(0.985 0 0);
+ --primary: oklch(0.922 0 0);
+ --primary-foreground: oklch(0.205 0 0);
+ --secondary: oklch(0.269 0 0);
+ --secondary-foreground: oklch(0.985 0 0);
+ --muted: oklch(0.269 0 0);
+ --muted-foreground: oklch(0.708 0 0);
+ --accent: oklch(0.269 0 0);
+ --accent-foreground: oklch(0.985 0 0);
+ --destructive: oklch(0.704 0.191 22.216);
+ --border: oklch(1 0 0 / 10%);
+ --input: oklch(1 0 0 / 15%);
+ --ring: oklch(0.556 0 0);
+ --chart-1: oklch(0.488 0.243 264.376);
+ --chart-2: oklch(0.696 0.17 162.48);
+ --chart-3: oklch(0.769 0.188 70.08);
+ --chart-4: oklch(0.627 0.265 303.9);
+ --chart-5: oklch(0.645 0.246 16.439);
+ --sidebar: oklch(0.205 0 0);
+ --sidebar-foreground: oklch(0.985 0 0);
+ --sidebar-primary: oklch(0.488 0.243 264.376);
+ --sidebar-primary-foreground: oklch(0.985 0 0);
+ --sidebar-accent: oklch(0.269 0 0);
+ --sidebar-accent-foreground: oklch(0.985 0 0);
+ --sidebar-border: oklch(1 0 0 / 10%);
+ --sidebar-ring: oklch(0.556 0 0);
+ --color-1: oklch(66.2% 0.225 25.9);
+ --color-2: oklch(60.4% 0.26 302);
+ --color-3: oklch(69.6% 0.165 251);
+ --color-4: oklch(80.2% 0.134 225);
+ --color-5: oklch(90.7% 0.231 133);
+}
+
+@layer base {
+ * {
+ @apply border-border outline-ring/50;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
new file mode 100644
index 0000000..a5ef193
--- /dev/null
+++ b/src/lib/utils.ts
@@ -0,0 +1,6 @@
+import { clsx, type ClassValue } from "clsx";
+import { twMerge } from "tailwind-merge";
+
+export function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
diff --git a/tsconfig.app.json b/tsconfig.app.json
index a9b5a59..fe12059 100644
--- a/tsconfig.app.json
+++ b/tsconfig.app.json
@@ -7,7 +7,6 @@
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
-
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
@@ -15,14 +14,17 @@
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
-
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
+ "noUncheckedSideEffectImports": true,
+ /* Path aliases */
+ "paths": {
+ "@/*": ["./src/*"]
+ }
},
"include": ["src"]
}
diff --git a/tsconfig.json b/tsconfig.json
index 1ffef60..1e17393 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,17 @@
{
"files": [],
"references": [
- { "path": "./tsconfig.app.json" },
- { "path": "./tsconfig.node.json" }
- ]
+ {
+ "path": "./tsconfig.app.json"
+ },
+ {
+ "path": "./tsconfig.node.json"
+ }
+ ],
+ "compilerOptions": {
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ }
}
diff --git a/vite.config.ts b/vite.config.ts
index 4ff4f8f..506e619 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,8 +1,14 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
+import path from "path";
// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "./src"),
+ },
+ },
});