mosaicmap/app/animation_card.tsx
2025-07-17 22:57:07 +08:00

169 lines
5.3 KiB
TypeScript

"use client"
import { animate, motion } from "framer-motion"
import React, { useEffect } from "react"
import { cn } from "@/lib/utils"
export interface CardProps {
className?: string
children?: React.ReactNode
withSparkles?: boolean
withIcons?: Array<{
icon: React.ReactNode
size?: "sm" | "md" | "lg"
className?: string
}>
}
const sizeMap = {
sm: "h-8 w-8",
md: "h-12 w-12",
lg: "h-16 w-16",
}
export function Card({ className, children, withSparkles = false, withIcons = [] }: CardProps) {
return (
<div
className={cn(
"max-w-sm w-full mx-auto p-8 rounded-xl border border-white/20 backdrop-blur-md bg-white/20 dark:bg-black/20 shadow-lg group",
className
)}
>
{(withSparkles || withIcons.length > 0) && (
<div
className={cn(
"h-[15rem] md:h-[20rem] rounded-xl z-40 relative",
"bg-neutral-300 dark:bg-[rgba(40,40,40,0.70)] [mask-image:radial-gradient(50%_50%_at_50%_50%,white_0%,transparent_100%)]"
)}
>
{withIcons.length > 0 && <AnimatedIcons icons={withIcons} />}
{withSparkles && <AnimatedSparkles />}
</div>
)}
{children}
</div>
)
}
// 保持原有的 AnimatedCard 组件以向后兼容
export function AnimatedCard({ className, title, description, icons = [] }: {
className?: string
title?: React.ReactNode
description?: React.ReactNode
icons?: Array<{
icon: React.ReactNode
size?: "sm" | "md" | "lg"
className?: string
}>
}) {
return (
<Card className={className} withIcons={icons}>
{title && (
<h3 className="text-lg font-semibold text-gray-800 dark:text-white py-2">
{title}
</h3>
)}
{description && (
<p className="text-sm font-normal text-neutral-600 dark:text-neutral-400 max-w-sm">
{description}
</p>
)}
</Card>
)
}
const AnimatedIcons = ({ icons }: {
icons: Array<{
icon: React.ReactNode
size?: "sm" | "md" | "lg"
className?: string
}>
}) => {
return (
<div className="absolute inset-0 flex items-center justify-center">
{icons.map((iconData, index) => (
<motion.div
key={index}
className={cn(
"rounded-full flex items-center justify-center bg-[rgba(248,248,248,0.01)] shadow-[0px_0px_8px_0px_rgba(248,248,248,0.25)_inset,0px_32px_24px_-16px_rgba(0,0,0,0.40)]",
sizeMap[iconData.size || "md"],
iconData.className
)}
animate={{
scale: [1, 1.1, 1],
rotate: [0, 5, -5, 0],
}}
transition={{
duration: 3,
repeat: Infinity,
delay: index * 0.2,
}}
>
{iconData.icon}
</motion.div>
))}
</div>
)
}
const Container = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
`rounded-full flex items-center justify-center bg-[rgba(248,248,248,0.01)]
shadow-[0px_0px_8px_0px_rgba(248,248,248,0.25)_inset,0px_32px_24px_-16px_rgba(0,0,0,0.40)]`,
className
)}
{...props}
/>
))
Container.displayName = "Container"
const AnimatedSparkles = () => (
<div className="h-40 w-px absolute top-20 m-auto z-40 bg-gradient-to-b from-transparent via-cyan-500 to-transparent animate-move">
<div className="w-10 h-32 top-1/2 -translate-y-1/2 absolute -left-10">
<Sparkles />
</div>
</div>
)
const Sparkles = () => {
const randomMove = () => Math.random() * 2 - 1
const randomOpacity = () => Math.random()
const random = () => Math.random()
return (
<div className="absolute inset-0">
{[...Array(12)].map((_, i) => (
<motion.span
key={`star-${i}`}
animate={{
top: `calc(${random() * 100}% + ${randomMove()}px)`,
left: `calc(${random() * 100}% + ${randomMove()}px)`,
opacity: randomOpacity(),
scale: [1, 1.2, 0],
}}
transition={{
duration: random() * 2 + 4,
repeat: Infinity,
ease: "linear",
}}
style={{
position: "absolute",
top: `${random() * 100}%`,
left: `${random() * 100}%`,
width: `2px`,
height: `2px`,
borderRadius: "50%",
zIndex: 1,
}}
className="inline-block bg-black dark:bg-white"
/>
))}
</div>
)
}