From 6bc4de1cc2f813559cbdb3514b8890a6510c27c6 Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Tue, 20 Jan 2026 20:39:53 +0900 Subject: [PATCH 1/8] feat(magic-card): add orb mode with animated glow effect --- apps/www/public/llms-full.txt | 57 +++++++++++++++++++++- apps/www/public/r/magic-card.json | 2 +- apps/www/registry/magicui/magic-card.tsx | 60 ++++++++++++++++++++++-- 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index 15d8dabca..d5c479a30 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -9319,7 +9319,12 @@ Description: A spotlight effect that follows your mouse cursor and highlights bo "use client" import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" import { cn } from "@/lib/utils" @@ -9331,6 +9336,16 @@ interface MagicCardProps { gradientOpacity?: number gradientFrom?: string gradientTo?: string + + mode?: "gradient" | "orb" + + glowFrom?: string + glowTo?: string + glowAngle?: number + + glowSize?: number + glowBlur?: number + glowOpacity?: number } export function MagicCard({ @@ -9341,9 +9356,23 @@ export function MagicCard({ gradientOpacity = 0.8, gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", + + mode = "orb", + + glowFrom = "#ee4f27", + glowTo = "#6b21ef", + glowAngle = 90, + + glowSize = 420, + glowBlur = 60, + glowOpacity = 0.9, }: MagicCardProps) { const mouseX = useMotionValue(-gradientSize) const mouseY = useMotionValue(-gradientSize) + + const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) + const reset = useCallback(() => { mouseX.set(-gradientSize) mouseY.set(-gradientSize) @@ -9388,7 +9417,10 @@ export function MagicCard({ return (
+ + {mode === "orb" && ( +
) @@ -11401,6 +11453,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/r/magic-card.json b/apps/www/public/r/magic-card.json index 9b8ecd314..1ea923910 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 } from \"react\"\nimport {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n} 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 mode?: \"gradient\" | \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\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\n mode = \"orb\",\n\n glowFrom = \"#ee4f27\",\n glowTo = \"#6b21ef\",\n glowAngle = 90,\n\n glowSize = 420,\n glowBlur = 60,\n glowOpacity = 0.9,\n}: MagicCardProps) {\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\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\n {mode === \"orb\" && (\n \n )}\n
{children}
\n
\n )\n}\n", "type": "registry:ui" } ] diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index c78a5f87e..26b1db8dc 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -1,7 +1,12 @@ "use client" import React, { useCallback, useEffect } from "react" -import { motion, useMotionTemplate, useMotionValue } from "motion/react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" import { cn } from "@/lib/utils" @@ -13,6 +18,16 @@ interface MagicCardProps { gradientOpacity?: number gradientFrom?: string gradientTo?: string + + mode?: "gradient" | "orb" + + glowFrom?: string + glowTo?: string + glowAngle?: number + + glowSize?: number + glowBlur?: number + glowOpacity?: number } export function MagicCard({ @@ -23,9 +38,23 @@ export function MagicCard({ gradientOpacity = 0.8, gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", + + mode = "orb", + + glowFrom = "#ee4f27", + glowTo = "#6b21ef", + glowAngle = 90, + + glowSize = 420, + glowBlur = 60, + glowOpacity = 0.9, }: MagicCardProps) { const mouseX = useMotionValue(-gradientSize) const mouseY = useMotionValue(-gradientSize) + + const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) + const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) + const reset = useCallback(() => { mouseX.set(-gradientSize) mouseY.set(-gradientSize) @@ -70,7 +99,10 @@ export function MagicCard({ return (
- + />)} + + {mode === "orb" && ( +
) From 35c5cd0873b3a45cbb360d9c6d9375c7dcf2e8db Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Tue, 20 Jan 2026 20:41:33 +0900 Subject: [PATCH 2/8] refactor(magic-card): improve orb mode animation with spring-based opacity --- apps/www/registry/magicui/magic-card.tsx | 78 ++++++++++++++---------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index 26b1db8dc..1b26792a6 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -1,13 +1,7 @@ "use client" -import React, { useCallback, useEffect } from "react" -import { - motion, - useMotionTemplate, - useMotionValue, - useSpring, -} from "motion/react" - +import React, { useCallback, useEffect, useRef } from "react" +import { motion, useMotionTemplate, useMotionValue, useSpring } from "motion/react" import { cn } from "@/lib/utils" interface MagicCardProps { @@ -30,6 +24,8 @@ interface MagicCardProps { glowOpacity?: number } +type ResetReason = "enter" | "leave" | "global" | "init" + export function MagicCard({ children, className, @@ -55,10 +51,37 @@ export function MagicCard({ const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) - const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + 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) => { @@ -70,42 +93,35 @@ 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")} >
) -} +} \ No newline at end of file From 336c883b40e5dc61a5c6f710df847570a2d2e428 Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Tue, 20 Jan 2026 21:07:22 +0900 Subject: [PATCH 3/8] feat(magic-card): add z-index layering and screen blend mode for orb glow --- apps/www/public/llms-full.txt | 93 ++++++++++++++++-------- apps/www/public/r/magic-card.json | 2 +- apps/www/registry/magicui/magic-card.tsx | 67 ++++++++++------- 3 files changed, 104 insertions(+), 58 deletions(-) diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index d5c479a30..51c7ee944 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -9318,7 +9318,7 @@ 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 React, { useCallback, useEffect, useRef } from "react" import { motion, useMotionTemplate, @@ -9348,6 +9348,8 @@ interface MagicCardProps { glowOpacity?: number } +type ResetReason = "enter" | "leave" | "global" | "init" + export function MagicCard({ children, className, @@ -9357,7 +9359,7 @@ export function MagicCard({ gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", - mode = "orb", + mode = "gradient", glowFrom = "#ee4f27", glowTo = "#6b21ef", @@ -9373,10 +9375,40 @@ export function MagicCard({ const orbX = useSpring(mouseX, { stiffness: 250, damping: 30, mass: 0.6 }) const orbY = useSpring(mouseY, { stiffness: 250, damping: 30, mass: 0.6 }) - const reset = useCallback(() => { - mouseX.set(-gradientSize) - mouseY.set(-gradientSize) - }, [gradientSize, mouseX, mouseY]) + 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) => { @@ -9388,29 +9420,25 @@ 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]) @@ -9418,15 +9446,15 @@ export function MagicCard({ return (
reset("leave")} + onPointerEnter={() => reset("enter")} > -
- + {mode === "gradient" && ( + + opacity: gradientOpacity, + }} + /> + )} {mode === "orb" && (
) } diff --git a/apps/www/public/r/magic-card.json b/apps/www/public/r/magic-card.json index 1ea923910..4ef6f632c 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 {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n} 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 mode?: \"gradient\" | \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\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\n mode = \"orb\",\n\n glowFrom = \"#ee4f27\",\n glowTo = \"#6b21ef\",\n glowAngle = 90,\n\n glowSize = 420,\n glowBlur = 60,\n glowOpacity = 0.9,\n}: MagicCardProps) {\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\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\n {mode === \"orb\" && (\n \n )}\n
{children}
\n
\n )\n}\n", + "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useRef } from \"react\"\nimport {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n} 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 mode?: \"gradient\" | \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\n}\n\ntype ResetReason = \"enter\" | \"leave\" | \"global\" | \"init\"\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\n mode = \"gradient\",\n\n glowFrom = \"#ee4f27\",\n glowTo = \"#6b21ef\",\n glowAngle = 90,\n\n glowSize = 420,\n glowBlur = 60,\n glowOpacity = 0.9,\n}: MagicCardProps) {\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\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 {mode === \"gradient\" && (\n \n )}\n\n {mode === \"orb\" && (\n \n )}\n
{children}
\n
\n )\n}\n", "type": "registry:ui" } ] diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index 1b26792a6..1abc08194 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -1,7 +1,13 @@ "use client" import React, { useCallback, useEffect, useRef } from "react" -import { motion, useMotionTemplate, useMotionValue, useSpring } from "motion/react" +import { + motion, + useMotionTemplate, + useMotionValue, + useSpring, +} from "motion/react" + import { cn } from "@/lib/utils" interface MagicCardProps { @@ -35,7 +41,7 @@ export function MagicCard({ gradientFrom = "#9E7AFF", gradientTo = "#FE8BBB", - mode = "orb", + mode = "gradient", glowFrom = "#ee4f27", glowTo = "#6b21ef", @@ -69,19 +75,22 @@ export function MagicCard({ gradientSizeRef.current = gradientSize }, [gradientSize]) - const reset = useCallback((reason: ResetReason = "leave") => { - const currentMode = modeRef.current + 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 - } + 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 off = -gradientSizeRef.current + mouseX.set(off) + mouseY.set(off) + }, + [mouseX, mouseY, orbVisible] + ) const handlePointerMove = useCallback( (e: React.PointerEvent) => { @@ -118,13 +127,16 @@ export function MagicCard({ return (
reset("leave")} onPointerEnter={() => reset("enter")} > -
- {mode === "gradient" && ( + {mode === "gradient" && ( + )} + opacity: gradientOpacity, + }} + /> + )} {mode === "orb" && (
) -} \ No newline at end of file +} From 83491be900a279c0d036cc24c3b2f3aa7cd560ea Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Tue, 20 Jan 2026 21:55:27 +0900 Subject: [PATCH 4/8] feat(magic-card): add theme-aware blend mode with SSR-safe theme detection --- apps/www/public/llms-full.txt | 109 ++++++++++++++++++----- apps/www/public/r/magic-card.json | 2 +- apps/www/registry/magicui/magic-card.tsx | 34 +++++-- 3 files changed, 115 insertions(+), 30 deletions(-) diff --git a/apps/www/public/llms-full.txt b/apps/www/public/llms-full.txt index 51c7ee944..e8973276d 100644 --- a/apps/www/public/llms-full.txt +++ b/apps/www/public/llms-full.txt @@ -9318,14 +9318,9 @@ Description: A spotlight effect that follows your mouse cursor and highlights bo --- file: magicui/magic-card.tsx --- "use client" -import React, { useCallback, useEffect, useRef } from "react" -import { - motion, - useMotionTemplate, - useMotionValue, - useSpring, -} 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 { @@ -9342,7 +9337,6 @@ interface MagicCardProps { glowFrom?: string glowTo?: string glowAngle?: number - glowSize?: number glowBlur?: number glowOpacity?: number @@ -9369,12 +9363,22 @@ export function MagicCard({ glowBlur = 60, glowOpacity = 0.9, }: MagicCardProps) { + 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 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) @@ -9445,10 +9449,7 @@ export function MagicCard({ return (
reset("leave")} onPointerEnter={() => reset("enter")} @@ -9456,7 +9457,7 @@ export function MagicCard({ +
+ {mode === "gradient" && ( @@ -9492,8 +9498,9 @@ export function MagicCard({ borderRadius: 9999, filter: `blur(${glowBlur}px)`, opacity: orbVisible, - background: `linear-gradient(${glowAngle}deg, ${glowFrom}, ${glowTo})`, - mixBlendMode: "screen", + backgroundImage: `linear-gradient(${glowAngle}deg, ${glowFrom}, ${glowTo})`, + + mixBlendMode: isDarkTheme ? "screen" : "multiply", willChange: "transform, opacity", }} /> @@ -9530,6 +9537,7 @@ export default function MagicCardDemo() { return ( @@ -9562,6 +9570,67 @@ export default function MagicCardDemo() { } +===== EXAMPLE: magic-card-demo-2 ===== +Title: Magic Card Demo 2 + +--- file: example/magic-card-demo2.tsx --- +"use client" + +import { useTheme } from "next-themes" + +import { Button } from "@/components/ui/button" +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card" +import { Input } from "@/components/ui/input" +import { Label } from "@/components/ui/label" +import { MagicCard } from "@/registry/magicui/magic-card" + +export default function MagicCardDemo() { + const { theme } = useTheme() + return ( + + + + Login + + Enter your credentials to access your account + + + +
+
+
+ + +
+
+ + +
+
+
+
+ + + +
+
+ ) +} + + ===== COMPONENT: marquee ===== Title: Marquee diff --git a/apps/www/public/r/magic-card.json b/apps/www/public/r/magic-card.json index 4ef6f632c..316734155 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, useRef } from \"react\"\nimport {\n motion,\n useMotionTemplate,\n useMotionValue,\n useSpring,\n} 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 mode?: \"gradient\" | \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\n}\n\ntype ResetReason = \"enter\" | \"leave\" | \"global\" | \"init\"\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\n mode = \"gradient\",\n\n glowFrom = \"#ee4f27\",\n glowTo = \"#6b21ef\",\n glowAngle = 90,\n\n glowSize = 420,\n glowBlur = 60,\n glowOpacity = 0.9,\n}: MagicCardProps) {\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\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 {mode === \"gradient\" && (\n \n )}\n\n {mode === \"orb\" && (\n \n )}\n
{children}
\n
\n )\n}\n", + "content": "\"use client\"\n\nimport React, { useCallback, useEffect, useMemo, useRef, useState } from \"react\"\nimport { motion, useMotionTemplate, useMotionValue, useSpring } from \"motion/react\"\nimport { useTheme } from \"next-themes\"\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 mode?: \"gradient\" | \"orb\"\n\n glowFrom?: string\n glowTo?: string\n glowAngle?: number\n glowSize?: number\n glowBlur?: number\n glowOpacity?: number\n}\n\ntype ResetReason = \"enter\" | \"leave\" | \"global\" | \"init\"\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\n mode = \"gradient\",\n\n glowFrom = \"#ee4f27\",\n glowTo = \"#6b21ef\",\n glowAngle = 90,\n\n glowSize = 420,\n glowBlur = 60,\n glowOpacity = 0.9,\n}: MagicCardProps) {\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}", "type": "registry:ui" } ] diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index 1abc08194..f8f09320f 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -1,12 +1,13 @@ "use client" -import React, { useCallback, useEffect, useRef } from "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" @@ -24,7 +25,6 @@ interface MagicCardProps { glowFrom?: string glowTo?: string glowAngle?: number - glowSize?: number glowBlur?: number glowOpacity?: number @@ -51,12 +51,22 @@ export function MagicCard({ glowBlur = 60, glowOpacity = 0.9, }: MagicCardProps) { + 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 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) @@ -138,7 +148,7 @@ export function MagicCard({ +
+ {mode === "gradient" && ( @@ -174,8 +189,9 @@ export function MagicCard({ borderRadius: 9999, filter: `blur(${glowBlur}px)`, opacity: orbVisible, - background: `linear-gradient(${glowAngle}deg, ${glowFrom}, ${glowTo})`, - mixBlendMode: "screen", + backgroundImage: `linear-gradient(${glowAngle}deg, ${glowFrom}, ${glowTo})`, + + mixBlendMode: isDarkTheme ? "screen" : "multiply", willChange: "transform, opacity", }} /> From 2c5a697204c07419acf0f334f5d1f51f65113218 Mon Sep 17 00:00:00 2001 From: Yeom-JinHo Date: Tue, 20 Jan 2026 22:12:11 +0900 Subject: [PATCH 5/8] fix(magic-card): add suppressHydrationWarning and use background instead of backgroundImage --- apps/www/registry/magicui/magic-card.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/www/registry/magicui/magic-card.tsx b/apps/www/registry/magicui/magic-card.tsx index f8f09320f..638d24452 100644 --- a/apps/www/registry/magicui/magic-card.tsx +++ b/apps/www/registry/magicui/magic-card.tsx @@ -148,7 +148,7 @@ export function MagicCard({