diff --git a/package.json b/package.json index ea97086..5901c3a 100644 --- a/package.json +++ b/package.json @@ -13,13 +13,17 @@ "@radix-ui/react-collapsible": "^1.1.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.2", "@radix-ui/react-separator": "^1.1.2", "@radix-ui/react-slot": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@types/canvas-confetti": "^1.9.0", + "canvas-confetti": "^1.9.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.475.0", + "motion": "^12.4.3", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7843c26..f020348 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@radix-ui/react-dropdown-menu': specifier: ^2.1.6 version: 2.1.6(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-icons': + specifier: ^1.3.2 + version: 1.3.2(react@19.0.0) '@radix-ui/react-label': specifier: ^2.1.2 version: 2.1.2(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -32,6 +35,12 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.8 version: 1.1.8(@types/react-dom@19.0.3(@types/react@19.0.9))(@types/react@19.0.9)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@types/canvas-confetti': + specifier: ^1.9.0 + version: 1.9.0 + canvas-confetti: + specifier: ^1.9.3 + version: 1.9.3 class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -41,6 +50,9 @@ importers: lucide-react: specifier: ^0.475.0 version: 0.475.0(react@19.0.0) + motion: + specifier: ^12.4.3 + version: 12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next: specifier: 15.1.7 version: 15.1.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -437,6 +449,11 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-icons@1.3.2': + resolution: {integrity: sha512-fyQIhGDhzfc9pK2kH6Pl9c4BDJGfMkPqkyIgYDthyNYoNg3wVhoJMMh19WS4Up/1KMPFVpNsT2q3WmXn2N1m6g==} + peerDependencies: + react: ^16.x || ^17.x || ^18.x || ^19.0.0 || ^19.0.0-rc + '@radix-ui/react-id@1.1.0': resolution: {integrity: sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==} peerDependencies: @@ -648,6 +665,9 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@types/canvas-confetti@1.9.0': + resolution: {integrity: sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==} + '@types/node@20.17.19': resolution: {integrity: sha512-LEwC7o1ifqg/6r2gn9Dns0f1rhK+fPFDoMiceTJ6kWmVk6bgXBI/9IOWfVan4WiAavK9pIVWdX0/e3J+eEUh5A==} @@ -714,6 +734,9 @@ packages: caniuse-lite@1.0.30001699: resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} + canvas-confetti@1.9.3: + resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==} + chokidar@3.6.0: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} @@ -795,6 +818,20 @@ packages: resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} engines: {node: '>=14'} + framer-motion@12.4.3: + resolution: {integrity: sha512-rsMeO7w3dKyNG09o3cGwSH49iHU+VgDmfSSfsX+wfkO3zDA6WWkh4sUsMXd155YROjZP+7FTIhDrBYfgZeHjKQ==} + 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 + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -891,6 +928,26 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + motion-dom@12.0.0: + resolution: {integrity: sha512-CvYd15OeIR6kHgMdonCc1ihsaUG4MYh/wrkz8gZ3hBX/uamyZCXN9S9qJoYF03GqfTt7thTV/dxnHYX4+55vDg==} + + motion-utils@12.0.0: + resolution: {integrity: sha512-MNFiBKbbqnmvOjkPyOKgHUp3Q6oiokLkI1bEwm5QA28cxMZrv0CbbBGDNmhF6DIXsi1pCQBSs0dX8xjeER1tmA==} + + motion@12.4.3: + resolution: {integrity: sha512-KeoMpKFEVdofN0v/z1g3tm4cMtk1WAHQ5Pg7M1ElxeRLA8cBSrkmSCJ9q6hLo7spp/n906h2kmeDNvBkysaxcQ==} + 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 + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1523,6 +1580,10 @@ snapshots: '@types/react': 19.0.9 '@types/react-dom': 19.0.3(@types/react@19.0.9) + '@radix-ui/react-icons@1.3.2(react@19.0.0)': + dependencies: + react: 19.0.0 + '@radix-ui/react-id@1.1.0(@types/react@19.0.9)(react@19.0.0)': dependencies: '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@19.0.9)(react@19.0.0) @@ -1722,6 +1783,8 @@ snapshots: dependencies: tslib: 2.8.1 + '@types/canvas-confetti@1.9.0': {} + '@types/node@20.17.19': dependencies: undici-types: 6.19.8 @@ -1777,6 +1840,8 @@ snapshots: caniuse-lite@1.0.30001699: {} + canvas-confetti@1.9.3: {} + chokidar@3.6.0: dependencies: anymatch: 3.1.3 @@ -1863,6 +1928,15 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + framer-motion@12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + motion-dom: 12.0.0 + motion-utils: 12.0.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + fsevents@2.3.3: optional: true @@ -1945,6 +2019,20 @@ snapshots: minipass@7.1.2: {} + motion-dom@12.0.0: + dependencies: + motion-utils: 12.0.0 + + motion-utils@12.0.0: {} + + motion@12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + framer-motion: 12.4.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + tslib: 2.8.1 + optionalDependencies: + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + mz@2.7.0: dependencies: any-promise: 1.3.0 diff --git a/src/app/globals.css b/src/app/globals.css index d4528b5..a3e3dca 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -41,6 +41,11 @@ body { --sidebar-accent-foreground: 240 5.9% 10%; --sidebar-border: 220 13% 91%; --sidebar-ring: 217.2 91.2% 59.8%; + --color-1: 0 100% 63%; + --color-2: 270 100% 63%; + --color-3: 210 100% 63%; + --color-4: 195 100% 63%; + --color-5: 90 100% 63%; } .dark { --background: 240 10% 3.9%; @@ -75,6 +80,11 @@ body { --sidebar-accent-foreground: 240 4.8% 95.9%; --sidebar-border: 240 3.7% 15.9%; --sidebar-ring: 217.2 91.2% 59.8%; + --color-1: 0 100% 63%; + --color-2: 270 100% 63%; + --color-3: 210 100% 63%; + --color-4: 195 100% 63%; + --color-5: 90 100% 63%; } } diff --git a/src/components/magicui/animated-shiny-text.tsx b/src/components/magicui/animated-shiny-text.tsx new file mode 100644 index 0000000..7a8506b --- /dev/null +++ b/src/components/magicui/animated-shiny-text.tsx @@ -0,0 +1,39 @@ +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 ( + + {children} + + ); +}; diff --git a/src/components/magicui/bento-grid.tsx b/src/components/magicui/bento-grid.tsx new file mode 100644 index 0000000..cb1837b --- /dev/null +++ b/src/components/magicui/bento-grid.tsx @@ -0,0 +1,83 @@ +import { ArrowRightIcon } from "@radix-ui/react-icons"; +import { ComponentPropsWithoutRef, ReactNode } from "react"; + +import { Button } from "@/components/ui/button"; +import { cn } from "@/lib/utils"; + +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 ( +
+ {children} +
+ ); +}; + +const BentoCard = ({ + name, + className, + background, + Icon, + description, + href, + cta, + ...props +}: BentoCardProps) => ( +
+
{background}
+
+ +

+ {name} +

+

{description}

+
+ +
+ +
+
+
+); + +export { BentoCard, BentoGrid }; diff --git a/src/components/magicui/border-beam.tsx b/src/components/magicui/border-beam.tsx new file mode 100644 index 0000000..1ddcd5d --- /dev/null +++ b/src/components/magicui/border-beam.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { cn } from "@/lib/utils"; +import { motion, MotionStyle, Transition } from "motion/react"; + +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; +} + +export const BorderBeam = ({ + className, + size = 50, + delay = 0, + duration = 6, + colorFrom = "#ffaa40", + colorTo = "#9c40ff", + transition, + style, + reverse = false, + initialOffset = 0, +}: BorderBeamProps) => { + return ( +
+ +
+ ); +}; diff --git a/src/components/magicui/confetti.tsx b/src/components/magicui/confetti.tsx new file mode 100644 index 0000000..c6df6ce --- /dev/null +++ b/src/components/magicui/confetti.tsx @@ -0,0 +1,149 @@ +"use client"; + +import type { + GlobalOptions as ConfettiGlobalOptions, + CreateTypes as ConfettiInstance, + Options as ConfettiOptions, +} from "canvas-confetti"; +import confetti from "canvas-confetti"; +import type { ReactNode } from "react"; +import React, { + createContext, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, +} from "react"; + +import { Button, ButtonProps } 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((props, ref) => { + const { + options, + globalOptions = { resize: true, useWorker: true }, + manualstart = false, + children, + ...rest + } = props; + const instanceRef = useRef(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 ( + + + {children} + + ); +}); + +// Set display name immediately +ConfettiComponent.displayName = "Confetti"; + +// Export as Confetti +export const Confetti = ConfettiComponent; + +interface ConfettiButtonProps extends ButtonProps { + options?: ConfettiOptions & + ConfettiGlobalOptions & { canvas?: HTMLCanvasElement }; + children?: React.ReactNode; +} + +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 ( + + ); +}; + +ConfettiButtonComponent.displayName = "ConfettiButton"; + +export const ConfettiButton = ConfettiButtonComponent; diff --git a/src/components/magicui/grid-pattern.tsx b/src/components/magicui/grid-pattern.tsx new file mode 100644 index 0000000..6840f45 --- /dev/null +++ b/src/components/magicui/grid-pattern.tsx @@ -0,0 +1,70 @@ +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 ( + + ); +} diff --git a/src/components/magicui/hero-video-dialog.tsx b/src/components/magicui/hero-video-dialog.tsx new file mode 100644 index 0000000..066edf8 --- /dev/null +++ b/src/components/magicui/hero-video-dialog.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { useState } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import { Play, XIcon } from "lucide-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 default function HeroVideoDialog({ + animationStyle = "from-center", + videoSrc, + thumbnailSrc, + thumbnailAlt = "Video thumbnail", + className, +}: HeroVideoProps) { + const [isVideoOpen, setIsVideoOpen] = useState(false); + const selectedAnimation = animationVariants[animationStyle]; + + return ( +
+
setIsVideoOpen(true)} + > + {thumbnailAlt} +
+
+
+ +
+
+
+
+ + {isVideoOpen && ( + setIsVideoOpen(false)} + exit={{ opacity: 0 }} + className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-md" + > + + + + +
+ +
+
+
+ )} +
+
+ ); +} diff --git a/src/components/magicui/rainbow-button.tsx b/src/components/magicui/rainbow-button.tsx new file mode 100644 index 0000000..51926cb --- /dev/null +++ b/src/components/magicui/rainbow-button.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { cn } from "@/lib/utils"; + +interface RainbowButtonProps + extends React.ButtonHTMLAttributes {} + +export const RainbowButton = React.forwardRef< + HTMLButtonElement, + RainbowButtonProps +>(({ children, className, ...props }, ref) => { + return ( + + ); +}); + +RainbowButton.displayName = "RainbowButton"; diff --git a/tailwind.config.ts b/tailwind.config.ts index 6113ca2..b04cd16 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -59,12 +59,39 @@ export default { 'accent-foreground': 'hsl(var(--sidebar-accent-foreground))', border: 'hsl(var(--sidebar-border))', ring: 'hsl(var(--sidebar-ring))' - } + }, + 'color-1': 'hsl(var(--color-1))', + 'color-2': 'hsl(var(--color-2))', + 'color-3': 'hsl(var(--color-3))', + 'color-4': 'hsl(var(--color-4))', + 'color-5': 'hsl(var(--color-5))' }, borderRadius: { lg: 'var(--radius)', md: 'calc(var(--radius) - 2px)', sm: 'calc(var(--radius) - 4px)' + }, + animation: { + 'shiny-text': 'shiny-text 8s infinite', + rainbow: 'rainbow var(--speed, 2s) infinite linear' + }, + keyframes: { + 'shiny-text': { + '0%, 90%, 100%': { + 'background-position': 'calc(-100% - var(--shiny-width)) 0' + }, + '30%, 60%': { + 'background-position': 'calc(100% + var(--shiny-width)) 0' + } + }, + rainbow: { + '0%': { + 'background-position': '0%' + }, + '100%': { + 'background-position': '200%' + } + } } } },