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} + + + ); + }, +); + +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"), + }, + }, });