diff --git a/apps/www/content/docs/components/magic-card.mdx b/apps/www/content/docs/components/magic-card.mdx index 03e793151..f1098ddc9 100644 --- a/apps/www/content/docs/components/magic-card.mdx +++ b/apps/www/content/docs/components/magic-card.mdx @@ -2,7 +2,7 @@ title: Magic Card date: 2024-07-07 description: A spotlight effect that follows your mouse cursor and highlights borders on hover. -author: dillionverma +author: dillionverma, Yeom-JinHo published: true --- @@ -40,6 +40,12 @@ npx shadcn@latest add @magicui/magic-card +## Examples + +### Orb + + + ## Usage ```tsx showLineNumbers @@ -59,12 +65,19 @@ import { MagicCard } from "@/registry/magicui/magic-card" ### MagicCard -| Prop name | Type | Default | Description | -| ----------------- | ----------------- | --------- | ------------------------------------------- | -| `children` | `React.ReactNode` | `-` | The content to be rendered inside the card | -| `className` | `string` | `-` | Additional CSS classes to apply to the card | -| `gradientSize` | `number` | `200` | Size of the gradient effect | -| `gradientColor` | `string` | `#262626` | Color of the gradient effect | -| `gradientOpacity` | `number` | `0.8` | Opacity of the gradient effect | -| `gradientFrom` | `string` | `#9E7AFF` | Start color of the gradient border | -| `gradientTo` | `string` | `#FE8BBB` | End color of the gradient border | +| Prop name | Type | Default | Description | +| ----------------- | --------------------- | ------------ | -------------------------------------------------- | +| `children` | `React.ReactNode` | `-` | The content to be rendered inside the card | +| `className` | `string` | `-` | Additional CSS classes to apply to the card | +| `mode` | `"gradient" \| "orb"` | `"gradient"` | Display mode: gradient or orb effect | +| `gradientSize` | `number` | `200` | Size of the gradient effect | +| `gradientColor` | `string` | `#262626` | Color of the gradient effect | +| `gradientOpacity` | `number` | `0.8` | Opacity of the gradient effect | +| `gradientFrom` | `string` | `#9E7AFF` | Start color of the gradient border | +| `gradientTo` | `string` | `#FE8BBB` | End color of the gradient border | +| `glowFrom` | `string` | `#ee4f27` | Start color of the orb glow effect (orb mode only) | +| `glowTo` | `string` | `#6b21ef` | End color of the orb glow effect (orb mode only) | +| `glowAngle` | `number` | `90` | Angle of the orb glow gradient (orb mode only) | +| `glowSize` | `number` | `420` | Size of the orb glow effect (orb mode only) | +| `glowBlur` | `number` | `60` | Blur amount of the orb glow effect (orb mode only) | +| `glowOpacity` | `number` | `0.9` | Opacity of the orb glow effect (orb mode only) | diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index 15d8dabca..0e675ea46 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -9318,36 +9318,128 @@ Description: A spotlight effect that follows your mouse cursor and highlights bo --- file: magicui/magic-card.tsx --- "use client" -import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" +import { useTheme } from "next-themes" import { cn } from "@/lib/utils" -interface MagicCardProps { +interface MagicCardBaseProps { 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) { +interface MagicCardGradientProps extends MagicCardBaseProps { + mode?: "gradient" + + gradientColor?: string + gradientOpacity?: number + + glowFrom?: never + glowTo?: never + glowAngle?: never + glowSize?: never + glowBlur?: never + glowOpacity?: never +} + +interface MagicCardOrbProps extends MagicCardBaseProps { + mode: "orb" + + glowFrom?: string + glowTo?: string + glowAngle?: number + glowSize?: number + glowBlur?: number + glowOpacity?: number + + gradientColor?: never + gradientOpacity?: never +} + +type MagicCardProps = MagicCardGradientProps | MagicCardOrbProps +type ResetReason = "enter" | "leave" | "global" | "init" + +function isOrbMode(props: MagicCardProps): props is MagicCardOrbProps { + return props.mode === "orb" +} + +export function MagicCard(props: MagicCardProps) { + const { + children, + className, + gradientSize = 200, + gradientColor = "#262626", + gradientOpacity = 0.8, + gradientFrom = "#9E7AFF", + gradientTo = "#FE8BBB", + mode = "gradient", + } = props + + const glowFrom = isOrbMode(props) ? (props.glowFrom ?? "#ee4f27") : "#ee4f27" + const glowTo = isOrbMode(props) ? (props.glowTo ?? "#6b21ef") : "#6b21ef" + const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90 + const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420 + const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60 + const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9 + const { theme, systemTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => setMounted(true), []) + + const isDarkTheme = useMemo(() => { + if (!mounted) return true + const currentTheme = theme === "system" ? systemTheme : theme + return currentTheme === "dark" + }, [theme, systemTheme, mounted]) + const mouseX = useMotionValue(-gradientSize) const mouseY = useMotionValue(-gradientSize) - const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + + const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbVisible = useSpring(0, { stiffness: 300, damping: 35 }) + + const modeRef = useRef(mode) + const glowOpacityRef = useRef(glowOpacity) + const gradientSizeRef = useRef(gradientSize) + + useEffect(() => { + modeRef.current = mode + }, [mode]) + + useEffect(() => { + glowOpacityRef.current = glowOpacity + }, [glowOpacity]) + + useEffect(() => { + gradientSizeRef.current = gradientSize + }, [gradientSize]) + + const reset = useCallback( + (reason: ResetReason = "leave") => { + const currentMode = modeRef.current + + if (currentMode === "orb") { + if (reason === "enter") orbVisible.set(glowOpacityRef.current) + else orbVisible.set(0) + return + } + + const off = -gradientSizeRef.current + mouseX.set(off) + mouseY.set(off) + }, + [mouseX, mouseY, orbVisible] + ) const handlePointerMove = useCallback( (e: React.PointerEvent) => { @@ -9359,42 +9451,41 @@ export function MagicCard({ ) useEffect(() => { - reset() + reset("init") }, [reset]) useEffect(() => { const handleGlobalPointerOut = (e: PointerEvent) => { - if (!e.relatedTarget) { - reset() - } + if (!e.relatedTarget) reset("global") } - + const handleBlur = () => reset("global") const handleVisibility = () => { - if (document.visibilityState !== "visible") { - reset() - } + if (document.visibilityState !== "visible") reset("global") } window.addEventListener("pointerout", handleGlobalPointerOut) - window.addEventListener("blur", reset) + window.addEventListener("blur", handleBlur) document.addEventListener("visibilitychange", handleVisibility) return () => { window.removeEventListener("pointerout", handleGlobalPointerOut) - window.removeEventListener("blur", reset) + window.removeEventListener("blur", handleBlur) document.removeEventListener("visibilitychange", handleVisibility) } }, [reset]) return (
reset("leave")} + onPointerEnter={() => reset("enter")} > -
- -
{children}
+ +
+ + {mode === "gradient" && ( + + )} + + {mode === "orb" && ( +
) } @@ -9479,6 +9601,96 @@ export default function MagicCardDemo() { } +===== EXAMPLE: magic-card-demo-2 ===== +Title: Magic Card Demo 2 + +--- file: example/magic-card-demo2.tsx --- +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Icons } from "@/components/icons" +import { MagicCard } from "@/registry/magicui/magic-card" + +import { AvatarCircles } from "../magicui/avatar-circles" + +export default function MagicCardDemo() { + const { theme, systemTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + const isDark = mounted + ? (theme === "system" ? systemTheme : theme) === "dark" + : true + + return ( + + + +
+ +
+ Yeom JinHo + + Frontend Developer + +
+
+
+ +

+ Frontend Developer focused on Interactive UI & Performance +

+

+ I'm passionate about visual presentation and currently focusing + on interactive UI. +

+
+ + + +
+
+ ) +} + + ===== COMPONENT: marquee ===== Title: Marquee @@ -11401,6 +11613,7 @@ export function Pointer({ const handleMouseMove = (e: MouseEvent) => { x.set(e.clientX) y.set(e.clientY) + setIsActive(true) } const handleMouseEnter = (e: MouseEvent) => { diff --git a/apps/www/public/llms.txt b/apps/www/public/llms.txt index 94f1e90c9..7eba6fc95 100644 --- a/apps/www/public/llms.txt +++ b/apps/www/public/llms.txt @@ -79,6 +79,7 @@ This file provides LLM-friendly entry points to documentation and examples. ## Examples - [Magic Card Demo](https://github.com/magicuidesign/magicui/blob/main/example/magic-card-demo.tsx): Example usage +- [Magic Card Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/magic-card-demo2.tsx): Example usage - [Android Demo](https://github.com/magicuidesign/magicui/blob/main/example/android-demo.tsx): Example usage - [Android Demo 2](https://github.com/magicuidesign/magicui/blob/main/example/android-demo-2.tsx): Example usage - [Android Demo 3](https://github.com/magicuidesign/magicui/blob/main/example/android-demo-3.tsx): Example usage diff --git a/apps/www/public/r/magic-card-demo-2.json b/apps/www/public/r/magic-card-demo-2.json new file mode 100644 index 000000000..3a6f64a26 --- /dev/null +++ b/apps/www/public/r/magic-card-demo-2.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "magic-card-demo-2", + "type": "registry:example", + "title": "Magic Card Demo 2", + "description": "Example showing a magic card with an orb effect.", + "registryDependencies": [ + "@magicui/magic-card" + ], + "files": [ + { + "path": "registry/example/magic-card-demo2.tsx", + "content": "\"use client\"\n\nimport { useEffect, useState } from \"react\"\nimport Link from \"next/link\"\nimport { useTheme } from \"next-themes\"\n\nimport { Button } from \"@/components/ui/button\"\nimport {\n Card,\n CardContent,\n CardDescription,\n CardFooter,\n CardHeader,\n CardTitle,\n} from \"@/components/ui/card\"\nimport { Icons } from \"@/components/icons\"\nimport { MagicCard } from \"@/registry/magicui/magic-card\"\n\nimport { AvatarCircles } from \"../magicui/avatar-circles\"\n\nexport default function MagicCardDemo() {\n const { theme, systemTheme } = useTheme()\n const [mounted, setMounted] = useState(false)\n\n useEffect(() => {\n setMounted(true)\n }, [])\n\n const isDark = mounted\n ? (theme === \"system\" ? systemTheme : theme) === \"dark\"\n : true\n\n return (\n \n \n \n
\n \n
\n Yeom JinHo\n \n Frontend Developer\n \n
\n
\n
\n \n

\n Frontend Developer focused on Interactive UI & Performance\n

\n

\n I'm passionate about visual presentation and currently focusing\n on interactive UI.\n

\n
\n \n \n \n \n
\n )\n}\n", + "type": "registry:example" + } + ] +} \ No newline at end of file diff --git a/apps/www/public/r/magic-card.json b/apps/www/public/r/magic-card.json index 9b8ecd314..59f491923 100644 --- a/apps/www/public/r/magic-card.json +++ b/apps/www/public/r/magic-card.json @@ -10,7 +10,7 @@ "files": [ { "path": "registry/magicui/magic-card.tsx", - "content": "\"use client\"\n\nimport React, { useCallback, useEffect } from \"react\"\nimport { motion, useMotionTemplate, useMotionValue } from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface MagicCardProps {\n children?: React.ReactNode\n className?: string\n gradientSize?: number\n gradientColor?: string\n gradientOpacity?: number\n gradientFrom?: string\n gradientTo?: string\n}\n\nexport function MagicCard({\n children,\n className,\n gradientSize = 200,\n gradientColor = \"#262626\",\n gradientOpacity = 0.8,\n gradientFrom = \"#9E7AFF\",\n gradientTo = \"#FE8BBB\",\n}: MagicCardProps) {\n const mouseX = useMotionValue(-gradientSize)\n const mouseY = useMotionValue(-gradientSize)\n const reset = useCallback(() => {\n mouseX.set(-gradientSize)\n mouseY.set(-gradientSize)\n }, [gradientSize, mouseX, mouseY])\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const rect = e.currentTarget.getBoundingClientRect()\n mouseX.set(e.clientX - rect.left)\n mouseY.set(e.clientY - rect.top)\n },\n [mouseX, mouseY]\n )\n\n useEffect(() => {\n reset()\n }, [reset])\n\n useEffect(() => {\n const handleGlobalPointerOut = (e: PointerEvent) => {\n if (!e.relatedTarget) {\n reset()\n }\n }\n\n const handleVisibility = () => {\n if (document.visibilityState !== \"visible\") {\n reset()\n }\n }\n\n window.addEventListener(\"pointerout\", handleGlobalPointerOut)\n window.addEventListener(\"blur\", reset)\n document.addEventListener(\"visibilitychange\", handleVisibility)\n\n return () => {\n window.removeEventListener(\"pointerout\", handleGlobalPointerOut)\n window.removeEventListener(\"blur\", reset)\n document.removeEventListener(\"visibilitychange\", handleVisibility)\n }\n }, [reset])\n\n return (\n \n \n
\n \n
{children}
\n
\n )\n}\n", + "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n} from \"motion/react\"\nimport { useTheme } from \"next-themes\"\n\nimport { cn } from \"@/lib/utils\"\n\ninterface MagicCardBaseProps {\n children?: React.ReactNode\n className?: string\n gradientSize?: number\n gradientFrom?: string\n gradientTo?: string\n}\n\ninterface MagicCardGradientProps extends MagicCardBaseProps {\n mode?: \"gradient\"\n\n gradientColor?: string\n gradientOpacity?: number\n\n glowFrom?: never\n glowTo?: never\n glowAngle?: never\n glowSize?: never\n glowBlur?: never\n glowOpacity?: never\n}\n\ninterface MagicCardOrbProps extends MagicCardBaseProps {\n mode: \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\n\n gradientColor?: never\n gradientOpacity?: never\n}\n\ntype MagicCardProps = MagicCardGradientProps | MagicCardOrbProps\ntype ResetReason = \"enter\" | \"leave\" | \"global\" | \"init\"\n\nfunction isOrbMode(props: MagicCardProps): props is MagicCardOrbProps {\n return props.mode === \"orb\"\n}\n\nexport function MagicCard(props: MagicCardProps) {\n const {\n children,\n className,\n gradientSize = 200,\n gradientColor = \"#262626\",\n gradientOpacity = 0.8,\n gradientFrom = \"#9E7AFF\",\n gradientTo = \"#FE8BBB\",\n mode = \"gradient\",\n } = props\n\n const glowFrom = isOrbMode(props) ? (props.glowFrom ?? \"#ee4f27\") : \"#ee4f27\"\n const glowTo = isOrbMode(props) ? (props.glowTo ?? \"#6b21ef\") : \"#6b21ef\"\n const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90\n const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420\n const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60\n const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9\n const { theme, systemTheme } = useTheme()\n const [mounted, setMounted] = useState(false)\n\n useEffect(() => setMounted(true), [])\n\n const isDarkTheme = useMemo(() => {\n if (!mounted) return true\n const currentTheme = theme === \"system\" ? systemTheme : theme\n return currentTheme === \"dark\"\n }, [theme, systemTheme, mounted])\n\n const mouseX = useMotionValue(-gradientSize)\n const mouseY = useMotionValue(-gradientSize)\n\n const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 })\n const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 })\n const orbVisible = useSpring(0, { stiffness: 300, damping: 35 })\n\n const modeRef = useRef(mode)\n const glowOpacityRef = useRef(glowOpacity)\n const gradientSizeRef = useRef(gradientSize)\n\n useEffect(() => {\n modeRef.current = mode\n }, [mode])\n\n useEffect(() => {\n glowOpacityRef.current = glowOpacity\n }, [glowOpacity])\n\n useEffect(() => {\n gradientSizeRef.current = gradientSize\n }, [gradientSize])\n\n const reset = useCallback(\n (reason: ResetReason = \"leave\") => {\n const currentMode = modeRef.current\n\n if (currentMode === \"orb\") {\n if (reason === \"enter\") orbVisible.set(glowOpacityRef.current)\n else orbVisible.set(0)\n return\n }\n\n const off = -gradientSizeRef.current\n mouseX.set(off)\n mouseY.set(off)\n },\n [mouseX, mouseY, orbVisible]\n )\n\n const handlePointerMove = useCallback(\n (e: React.PointerEvent) => {\n const rect = e.currentTarget.getBoundingClientRect()\n mouseX.set(e.clientX - rect.left)\n mouseY.set(e.clientY - rect.top)\n },\n [mouseX, mouseY]\n )\n\n useEffect(() => {\n reset(\"init\")\n }, [reset])\n\n useEffect(() => {\n const handleGlobalPointerOut = (e: PointerEvent) => {\n if (!e.relatedTarget) reset(\"global\")\n }\n const handleBlur = () => reset(\"global\")\n const handleVisibility = () => {\n if (document.visibilityState !== \"visible\") reset(\"global\")\n }\n\n window.addEventListener(\"pointerout\", handleGlobalPointerOut)\n window.addEventListener(\"blur\", handleBlur)\n document.addEventListener(\"visibilitychange\", handleVisibility)\n\n return () => {\n window.removeEventListener(\"pointerout\", handleGlobalPointerOut)\n window.removeEventListener(\"blur\", handleBlur)\n document.removeEventListener(\"visibilitychange\", handleVisibility)\n }\n }, [reset])\n\n return (\n reset(\"leave\")}\n onPointerEnter={() => reset(\"enter\")}\n >\n \n\n
\n\n {mode === \"gradient\" && (\n \n )}\n\n {mode === \"orb\" && (\n \n )}\n
{children}
\n
\n )\n}\n", "type": "registry:ui" } ] diff --git a/apps/www/public/r/registry.json b/apps/www/public/r/registry.json index 9a498d52b..3ae0b0660 100644 --- a/apps/www/public/r/registry.json +++ b/apps/www/public/r/registry.json @@ -1275,6 +1275,21 @@ } ] }, + { + "name": "magic-card-demo-2", + "type": "registry:example", + "title": "Magic Card Demo 2", + "description": "Example showing a magic card with an orb effect.", + "registryDependencies": [ + "@magicui/magic-card" + ], + "files": [ + { + "path": "registry/example/magic-card-demo2.tsx", + "type": "registry:example" + } + ] + }, { "name": "android-demo", "type": "registry:example", diff --git a/apps/www/public/registry.json b/apps/www/public/registry.json index 9a498d52b..3ae0b0660 100644 --- a/apps/www/public/registry.json +++ b/apps/www/public/registry.json @@ -1275,6 +1275,21 @@ } ] }, + { + "name": "magic-card-demo-2", + "type": "registry:example", + "title": "Magic Card Demo 2", + "description": "Example showing a magic card with an orb effect.", + "registryDependencies": [ + "@magicui/magic-card" + ], + "files": [ + { + "path": "registry/example/magic-card-demo2.tsx", + "type": "registry:example" + } + ] + }, { "name": "android-demo", "type": "registry:example", diff --git a/apps/www/registry.json b/apps/www/registry.json index 9a498d52b..3ae0b0660 100644 --- a/apps/www/registry.json +++ b/apps/www/registry.json @@ -1275,6 +1275,21 @@ } ] }, + { + "name": "magic-card-demo-2", + "type": "registry:example", + "title": "Magic Card Demo 2", + "description": "Example showing a magic card with an orb effect.", + "registryDependencies": [ + "@magicui/magic-card" + ], + "files": [ + { + "path": "registry/example/magic-card-demo2.tsx", + "type": "registry:example" + } + ] + }, { "name": "android-demo", "type": "registry:example", diff --git a/apps/www/registry/__index__.tsx b/apps/www/registry/__index__.tsx index 28b83d4ca..4b592c1bb 100644 --- a/apps/www/registry/__index__.tsx +++ b/apps/www/registry/__index__.tsx @@ -1205,6 +1205,23 @@ export const Index: Record = { }), meta: undefined, }, + "magic-card-demo-2": { + name: "magic-card-demo-2", + description: "Example showing a magic card with an orb effect.", + type: "registry:example", + registryDependencies: ["@magicui/magic-card"], + files: [{ + path: "registry/example/magic-card-demo2.tsx", + type: "registry:example", + target: "" + }], + component: React.lazy(async () => { + const mod = await import("@/registry/example/magic-card-demo2.tsx") + const exportName = Object.keys(mod).find(key => typeof mod[key] === 'function' || typeof mod[key] === 'object') || item.name + return { default: mod.default || mod[exportName] } + }), + meta: undefined, + }, "android-demo": { name: "android-demo", description: "Example showing a mockup of an Android device.", diff --git a/apps/www/registry/example/magic-card-demo2.tsx b/apps/www/registry/example/magic-card-demo2.tsx new file mode 100644 index 000000000..86df8bcf6 --- /dev/null +++ b/apps/www/registry/example/magic-card-demo2.tsx @@ -0,0 +1,84 @@ +"use client" + +import { useEffect, useState } from "react" +import Link from "next/link" +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Icons } from "@/components/icons" +import { MagicCard } from "@/registry/magicui/magic-card" + +import { AvatarCircles } from "../magicui/avatar-circles" + +export default function MagicCardDemo() { + const { theme, systemTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => { + setMounted(true) + }, []) + + const isDark = mounted + ? (theme === "system" ? systemTheme : theme) === "dark" + : true + + return ( + + + +
+ +
+ Yeom JinHo + + Frontend Developer + +
+
+
+ +

+ Frontend Developer focused on Interactive UI & Performance +

+

+ I'm passionate about visual presentation and currently focusing + on interactive UI. +

+
+ + + +
+
+ ) +} diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index c78a5f87e..9a2613a6e 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -1,35 +1,127 @@ "use client" -import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" +import { useTheme } from "next-themes" import { cn } from "@/lib/utils" -interface MagicCardProps { +interface MagicCardBaseProps { 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) { +interface MagicCardGradientProps extends MagicCardBaseProps { + mode?: "gradient" + + gradientColor?: string + gradientOpacity?: number + + glowFrom?: never + glowTo?: never + glowAngle?: never + glowSize?: never + glowBlur?: never + glowOpacity?: never +} + +interface MagicCardOrbProps extends MagicCardBaseProps { + mode: "orb" + + glowFrom?: string + glowTo?: string + glowAngle?: number + glowSize?: number + glowBlur?: number + glowOpacity?: number + + gradientColor?: never + gradientOpacity?: never +} + +type MagicCardProps = MagicCardGradientProps | MagicCardOrbProps +type ResetReason = "enter" | "leave" | "global" | "init" + +function isOrbMode(props: MagicCardProps): props is MagicCardOrbProps { + return props.mode === "orb" +} + +export function MagicCard(props: MagicCardProps) { + const { + children, + className, + gradientSize = 200, + gradientColor = "#262626", + gradientOpacity = 0.8, + gradientFrom = "#9E7AFF", + gradientTo = "#FE8BBB", + mode = "gradient", + } = props + + const glowFrom = isOrbMode(props) ? (props.glowFrom ?? "#ee4f27") : "#ee4f27" + const glowTo = isOrbMode(props) ? (props.glowTo ?? "#6b21ef") : "#6b21ef" + const glowAngle = isOrbMode(props) ? (props.glowAngle ?? 90) : 90 + const glowSize = isOrbMode(props) ? (props.glowSize ?? 420) : 420 + const glowBlur = isOrbMode(props) ? (props.glowBlur ?? 60) : 60 + const glowOpacity = isOrbMode(props) ? (props.glowOpacity ?? 0.9) : 0.9 + const { theme, systemTheme } = useTheme() + const [mounted, setMounted] = useState(false) + + useEffect(() => setMounted(true), []) + + const isDarkTheme = useMemo(() => { + if (!mounted) return true + const currentTheme = theme === "system" ? systemTheme : theme + return currentTheme === "dark" + }, [theme, systemTheme, mounted]) + const mouseX = useMotionValue(-gradientSize) const mouseY = useMotionValue(-gradientSize) - const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + + const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbVisible = useSpring(0, { stiffness: 300, damping: 35 }) + + const modeRef = useRef(mode) + const glowOpacityRef = useRef(glowOpacity) + const gradientSizeRef = useRef(gradientSize) + + useEffect(() => { + modeRef.current = mode + }, [mode]) + + useEffect(() => { + glowOpacityRef.current = glowOpacity + }, [glowOpacity]) + + useEffect(() => { + gradientSizeRef.current = gradientSize + }, [gradientSize]) + + const reset = useCallback( + (reason: ResetReason = "leave") => { + const currentMode = modeRef.current + + if (currentMode === "orb") { + if (reason === "enter") orbVisible.set(glowOpacityRef.current) + else orbVisible.set(0) + return + } + + const off = -gradientSizeRef.current + mouseX.set(off) + mouseY.set(off) + }, + [mouseX, mouseY, orbVisible] + ) const handlePointerMove = useCallback( (e: React.PointerEvent) => { @@ -41,42 +133,41 @@ export function MagicCard({ ) useEffect(() => { - reset() + reset("init") }, [reset]) useEffect(() => { const handleGlobalPointerOut = (e: PointerEvent) => { - if (!e.relatedTarget) { - reset() - } + if (!e.relatedTarget) reset("global") } - + const handleBlur = () => reset("global") const handleVisibility = () => { - if (document.visibilityState !== "visible") { - reset() - } + if (document.visibilityState !== "visible") reset("global") } window.addEventListener("pointerout", handleGlobalPointerOut) - window.addEventListener("blur", reset) + window.addEventListener("blur", handleBlur) document.addEventListener("visibilitychange", handleVisibility) return () => { window.removeEventListener("pointerout", handleGlobalPointerOut) - window.removeEventListener("blur", reset) + window.removeEventListener("blur", handleBlur) document.removeEventListener("visibilitychange", handleVisibility) } }, [reset]) return (
reset("leave")} + onPointerEnter={() => reset("enter")} > -
- -
{children}
+ +
+ + {mode === "gradient" && ( + + )} + + {mode === "orb" && ( +
) } diff --git a/apps/www/registry/registry-examples.ts b/apps/www/registry/registry-examples.ts index e99cb2547..4900e9a67 100644 --- a/apps/www/registry/registry-examples.ts +++ b/apps/www/registry/registry-examples.ts @@ -15,6 +15,19 @@ export const examples: Registry["items"] = [ }, ], }, + { + name: "magic-card-demo-2", + type: "registry:example", + title: "Magic Card Demo 2", + description: "Example showing a magic card with an orb effect.", + registryDependencies: ["@magicui/magic-card"], + files: [ + { + path: "example/magic-card-demo2.tsx", + type: "registry:example", + }, + ], + }, { name: "android-demo", type: "registry:example",