@zmzai/theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +76 -0
  3. package/package.json +91 -0
  4. package/src/brand/index.ts +3 -0
  5. package/src/brand/logo/Logo.tsx +46 -0
  6. package/src/brand/logo/favicon.png +0 -0
  7. package/src/brand/logo/index.ts +2 -0
  8. package/src/brand/logo/zmzai-logo.png +0 -0
  9. package/src/brand/wordmark/Wordmark.tsx +48 -0
  10. package/src/brand/wordmark/index.ts +2 -0
  11. package/src/components/aceternity-sidebar/AceternitySidebar.tsx +133 -0
  12. package/src/components/aceternity-sidebar/index.ts +2 -0
  13. package/src/components/animated-tabs/AnimatedTabs.tsx +60 -0
  14. package/src/components/animated-tabs/index.ts +2 -0
  15. package/src/components/animated-tooltip/AnimatedTooltip.tsx +93 -0
  16. package/src/components/animated-tooltip/index.ts +2 -0
  17. package/src/components/avatar/Avatar.tsx +59 -0
  18. package/src/components/avatar/index.ts +2 -0
  19. package/src/components/badge/Badge.tsx +45 -0
  20. package/src/components/badge/index.ts +2 -0
  21. package/src/components/bento-grid/BentoGrid.tsx +85 -0
  22. package/src/components/bento-grid/index.ts +2 -0
  23. package/src/components/button/Button.tsx +66 -0
  24. package/src/components/button/button.styles.ts +41 -0
  25. package/src/components/button/index.ts +4 -0
  26. package/src/components/card/Card.tsx +87 -0
  27. package/src/components/card/card.styles.ts +35 -0
  28. package/src/components/card/index.ts +4 -0
  29. package/src/components/card-3d/Card3D.tsx +84 -0
  30. package/src/components/card-3d/index.ts +2 -0
  31. package/src/components/card-hover-effect/CardHoverEffect.tsx +83 -0
  32. package/src/components/card-hover-effect/index.ts +2 -0
  33. package/src/components/card-spotlight/CardSpotlight.tsx +63 -0
  34. package/src/components/card-spotlight/index.ts +2 -0
  35. package/src/components/carousel/Carousel.tsx +79 -0
  36. package/src/components/carousel/index.ts +2 -0
  37. package/src/components/code-block/CodeBlock.tsx +153 -0
  38. package/src/components/code-block/index.ts +2 -0
  39. package/src/components/comet-card/CometCard.tsx +106 -0
  40. package/src/components/comet-card/index.ts +2 -0
  41. package/src/components/compare/Compare.tsx +128 -0
  42. package/src/components/compare/index.ts +2 -0
  43. package/src/components/dialog/Dialog.tsx +79 -0
  44. package/src/components/dialog/index.ts +10 -0
  45. package/src/components/dropdown-menu/DropdownMenu.tsx +76 -0
  46. package/src/components/dropdown-menu/index.ts +8 -0
  47. package/src/components/empty-state/EmptyState.tsx +78 -0
  48. package/src/components/empty-state/index.ts +2 -0
  49. package/src/components/encrypt-text/EncryptText.tsx +102 -0
  50. package/src/components/encrypt-text/index.ts +2 -0
  51. package/src/components/evervault-card/EvervaultCard.tsx +114 -0
  52. package/src/components/evervault-card/index.ts +2 -0
  53. package/src/components/expandable-card/ExpandableCard.tsx +91 -0
  54. package/src/components/expandable-card/index.ts +2 -0
  55. package/src/components/file-upload/FileUpload.tsx +171 -0
  56. package/src/components/file-upload/index.ts +2 -0
  57. package/src/components/flip-words/FlipWords.tsx +70 -0
  58. package/src/components/flip-words/index.ts +2 -0
  59. package/src/components/floating-navbar/FloatingNavbar.tsx +82 -0
  60. package/src/components/floating-navbar/index.ts +2 -0
  61. package/src/components/focus-cards/FocusCards.tsx +93 -0
  62. package/src/components/focus-cards/index.ts +2 -0
  63. package/src/components/glare-card/GlareCard.tsx +66 -0
  64. package/src/components/glare-card/index.ts +2 -0
  65. package/src/components/glowing-effect/GlowingEffect.tsx +68 -0
  66. package/src/components/glowing-effect/index.ts +2 -0
  67. package/src/components/hover-border-gradient/HoverBorderGradient.tsx +80 -0
  68. package/src/components/hover-border-gradient/index.ts +2 -0
  69. package/src/components/index.ts +71 -0
  70. package/src/components/infinite-moving-cards/InfiniteMovingCards.tsx +84 -0
  71. package/src/components/infinite-moving-cards/index.ts +2 -0
  72. package/src/components/input/Input.tsx +35 -0
  73. package/src/components/input/index.ts +4 -0
  74. package/src/components/input/input.styles.ts +34 -0
  75. package/src/components/keyboard/Keyboard.tsx +38 -0
  76. package/src/components/keyboard/index.ts +2 -0
  77. package/src/components/layout-grid/LayoutGrid.tsx +121 -0
  78. package/src/components/layout-grid/index.ts +2 -0
  79. package/src/components/link-preview/LinkPreview.tsx +121 -0
  80. package/src/components/link-preview/index.ts +2 -0
  81. package/src/components/loader/Loader.tsx +130 -0
  82. package/src/components/loader/index.ts +2 -0
  83. package/src/components/magnetic-button/MagneticButton.tsx +79 -0
  84. package/src/components/magnetic-button/index.ts +2 -0
  85. package/src/components/moving-border/MovingBorder.tsx +72 -0
  86. package/src/components/moving-border/index.ts +2 -0
  87. package/src/components/multi-step-loader/MultiStepLoader.tsx +95 -0
  88. package/src/components/multi-step-loader/index.ts +2 -0
  89. package/src/components/notch/Notch.tsx +47 -0
  90. package/src/components/notch/index.ts +2 -0
  91. package/src/components/select/Select.tsx +100 -0
  92. package/src/components/select/index.ts +10 -0
  93. package/src/components/skeleton/Skeleton.tsx +34 -0
  94. package/src/components/skeleton/index.ts +2 -0
  95. package/src/components/stateful-button/StatefulButton.tsx +115 -0
  96. package/src/components/stateful-button/index.ts +2 -0
  97. package/src/components/sticky-banner/StickyBanner.tsx +74 -0
  98. package/src/components/sticky-banner/index.ts +2 -0
  99. package/src/components/sticky-scroll-reveal/StickyScrollReveal.tsx +141 -0
  100. package/src/components/sticky-scroll-reveal/index.ts +2 -0
  101. package/src/components/terminal/Terminal.tsx +55 -0
  102. package/src/components/terminal/index.ts +2 -0
  103. package/src/components/text-generate/TextGenerate.tsx +59 -0
  104. package/src/components/text-generate/index.ts +2 -0
  105. package/src/components/text-hover-effect/TextHoverEffect.tsx +90 -0
  106. package/src/components/text-hover-effect/index.ts +2 -0
  107. package/src/components/textarea/Textarea.tsx +39 -0
  108. package/src/components/textarea/index.ts +2 -0
  109. package/src/components/timeline/Timeline.tsx +106 -0
  110. package/src/components/timeline/index.ts +3 -0
  111. package/src/components/tooltip/Tooltip.tsx +51 -0
  112. package/src/components/tooltip/index.ts +2 -0
  113. package/src/components/tooltip-card/TooltipCard.tsx +130 -0
  114. package/src/components/tooltip-card/index.ts +2 -0
  115. package/src/components/tracing-beam/TracingBeam.tsx +110 -0
  116. package/src/components/tracing-beam/index.ts +2 -0
  117. package/src/components/typewriter/Typewriter.tsx +69 -0
  118. package/src/components/typewriter/index.ts +2 -0
  119. package/src/components/vanish-input/VanishInput.tsx +170 -0
  120. package/src/components/vanish-input/index.ts +2 -0
  121. package/src/components/wobble-card/WobbleCard.tsx +77 -0
  122. package/src/components/wobble-card/index.ts +2 -0
  123. package/src/index.ts +21 -0
  124. package/src/tokens/fonts.css +42 -0
  125. package/src/tokens/theme.css +120 -0
  126. package/src/utils/cn.ts +13 -0
  127. package/src/utils/index.ts +2 -0
  128. package/src/utils/motion.ts +47 -0
  129. package/src/vite-env.d.ts +6 -0
@@ -0,0 +1,171 @@
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ useCallback,
7
+ type ReactNode,
8
+ type DragEvent,
9
+ type ChangeEvent,
10
+ } from "react";
11
+ import { motion } from "framer-motion";
12
+ import { cn } from "../../utils/cn";
13
+
14
+ export interface FileUploadProps {
15
+ /** Called with each file selected/dropped */
16
+ onChange: (file: File) => void;
17
+ /** Accepted file types, e.g. ".pdf,image/*" */
18
+ accept?: string;
19
+ /** Allow multiple file selection (onChange still fires per file) */
20
+ multiple?: boolean;
21
+ className?: string;
22
+ /** Custom inner content; defaults to the upload prompt */
23
+ children?: ReactNode;
24
+ }
25
+
26
+ /**
27
+ * FileUpload — drag-and-drop upload zone with a dotted grid backdrop.
28
+ *
29
+ * Renders a clickable drop area. Supports drag-over highlight and
30
+ * click-to-browse via a hidden `<input type="file">`. Based on Aceternity's
31
+ * File Upload, adapted to the zmzai monochrome palette.
32
+ *
33
+ * @example
34
+ * <FileUpload accept="image/*" onChange={(f) => upload(f)}>
35
+ * <span>拖拽或点击上传</span>
36
+ * </FileUpload>
37
+ */
38
+ export function FileUpload({
39
+ onChange,
40
+ accept,
41
+ multiple,
42
+ className,
43
+ children,
44
+ }: FileUploadProps) {
45
+ const inputRef = useRef<HTMLInputElement>(null);
46
+ const [dragging, setDragging] = useState(false);
47
+
48
+ const handleFiles = useCallback(
49
+ (files: FileList | null) => {
50
+ if (!files || files.length === 0) return;
51
+ if (multiple) {
52
+ Array.from(files).forEach((f) => onChange(f));
53
+ } else {
54
+ onChange(files[0]);
55
+ }
56
+ },
57
+ [onChange, multiple]
58
+ );
59
+
60
+ const handleDragOver = useCallback((e: DragEvent<HTMLDivElement>) => {
61
+ e.preventDefault();
62
+ e.stopPropagation();
63
+ setDragging(true);
64
+ }, []);
65
+
66
+ const handleDragLeave = useCallback((e: DragEvent<HTMLDivElement>) => {
67
+ e.preventDefault();
68
+ e.stopPropagation();
69
+ setDragging(false);
70
+ }, []);
71
+
72
+ const handleDrop = useCallback(
73
+ (e: DragEvent<HTMLDivElement>) => {
74
+ e.preventDefault();
75
+ e.stopPropagation();
76
+ setDragging(false);
77
+ handleFiles(e.dataTransfer.files);
78
+ },
79
+ [handleFiles]
80
+ );
81
+
82
+ const handleChange = useCallback(
83
+ (e: ChangeEvent<HTMLInputElement>) => {
84
+ handleFiles(e.target.files);
85
+ // reset so selecting the same file again still fires onChange
86
+ e.target.value = "";
87
+ },
88
+ [handleFiles]
89
+ );
90
+
91
+ return (
92
+ <motion.div
93
+ role="button"
94
+ tabIndex={0}
95
+ onClick={() => inputRef.current?.click()}
96
+ onKeyDown={(e) => {
97
+ if (e.key === "Enter" || e.key === " ") {
98
+ e.preventDefault();
99
+ inputRef.current?.click();
100
+ }
101
+ }}
102
+ onDragOver={handleDragOver}
103
+ onDragLeave={handleDragLeave}
104
+ onDrop={handleDrop}
105
+ whileHover={{ scale: 1.005 }}
106
+ whileTap={{ scale: 0.995 }}
107
+ className={cn(
108
+ "relative flex min-h-48 w-full cursor-pointer flex-col items-center justify-center gap-4 overflow-hidden rounded-xl border-2 border-dashed p-8 text-center transition-colors duration-200",
109
+ "border-line bg-surface hover:border-ink/40 hover:bg-surface-2",
110
+ dragging && "border-ink bg-surface-2",
111
+ className
112
+ )}
113
+ >
114
+ {/* Dotted grid background */}
115
+ <div
116
+ className="pointer-events-none absolute inset-0 opacity-60"
117
+ style={{
118
+ backgroundImage:
119
+ "radial-gradient(currentColor 1px, transparent 1px)",
120
+ backgroundSize: "16px 16px",
121
+ color: "rgba(0,0,0,0.05)",
122
+ }}
123
+ />
124
+
125
+ {/* Content */}
126
+ <div className="relative z-10 flex flex-col items-center gap-3">
127
+ {children ?? (
128
+ <>
129
+ <UploadIcon />
130
+ <div className="space-y-1">
131
+ <p className="text-sm font-medium text-ink">
132
+ 拖拽文件到此处,或点击浏览
133
+ </p>
134
+ <p className="text-xs text-ink-3">
135
+ {accept ? `支持 ${accept}` : "支持任意类型文件"}
136
+ </p>
137
+ </div>
138
+ </>
139
+ )}
140
+ </div>
141
+
142
+ <input
143
+ ref={inputRef}
144
+ type="file"
145
+ accept={accept}
146
+ multiple={multiple}
147
+ onChange={handleChange}
148
+ className="hidden"
149
+ />
150
+ </motion.div>
151
+ );
152
+ }
153
+
154
+ function UploadIcon() {
155
+ return (
156
+ <svg
157
+ className="h-8 w-8 text-ink-2"
158
+ viewBox="0 0 24 24"
159
+ fill="none"
160
+ stroke="currentColor"
161
+ strokeWidth={1.5}
162
+ strokeLinecap="round"
163
+ strokeLinejoin="round"
164
+ aria-hidden="true"
165
+ >
166
+ <path d="M12 3v12" />
167
+ <path d="m7 8 5-5 5 5" />
168
+ <path d="M5 21h14" />
169
+ </svg>
170
+ );
171
+ }
@@ -0,0 +1,2 @@
1
+ export { FileUpload } from "./FileUpload";
2
+ export type { FileUploadProps } from "./FileUpload";
@@ -0,0 +1,70 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type HTMLAttributes } from "react";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export interface FlipWordsProps extends HTMLAttributes<HTMLSpanElement> {
8
+ /** Words to cycle through */
9
+ words: string[];
10
+ /** Milliseconds each word stays before flipping */
11
+ duration?: number;
12
+ }
13
+
14
+ /**
15
+ * FlipWords — cycles through words with a 3D flip animation.
16
+ *
17
+ * The current word rotates up and out on the X axis while the next word
18
+ * rotates in from the bottom. Keep this container inline for natural flow.
19
+ *
20
+ * @example
21
+ * <FlipWords words={["faster", "smoother", "minimal"]} duration={3000} />
22
+ */
23
+ export function FlipWords({
24
+ words,
25
+ duration = 3000,
26
+ className,
27
+ ...props
28
+ }: FlipWordsProps) {
29
+ const [index, setIndex] = useState(0);
30
+ const [current, setCurrent] = useState(0);
31
+
32
+ useEffect(() => {
33
+ if (words.length <= 1) return;
34
+ const id = setInterval(() => {
35
+ setCurrent((prev) => (prev + 1) % words.length);
36
+ }, duration);
37
+ return () => clearInterval(id);
38
+ }, [words.length, duration]);
39
+
40
+ // keep `index` in sync with `current` for exit animations
41
+ useEffect(() => {
42
+ setIndex(current);
43
+ }, [current]);
44
+
45
+ return (
46
+ <span
47
+ className={cn("relative inline-block", className)}
48
+ style={{ perspective: "1000px" }}
49
+ {...props}
50
+ >
51
+ <span className="invisible whitespace-pre">
52
+ {words.reduce((a, b) => (a.length >= b.length ? a : b), "")}
53
+ </span>
54
+ <span className="absolute inset-0 inline-flex items-center justify-start">
55
+ <AnimatePresence mode="wait">
56
+ <motion.span
57
+ key={current}
58
+ className="inline-block will-change-transform"
59
+ initial={{ rotateX: -90, opacity: 0, transformOrigin: "bottom" }}
60
+ animate={{ rotateX: 0, opacity: 1 }}
61
+ exit={{ rotateX: 90, opacity: 0, transformOrigin: "top" }}
62
+ transition={{ duration: 0.4, ease: [0.16, 1, 0.3, 1] }}
63
+ >
64
+ {words[index]}
65
+ </motion.span>
66
+ </AnimatePresence>
67
+ </span>
68
+ </span>
69
+ );
70
+ }
@@ -0,0 +1,2 @@
1
+ export { FlipWords } from "./FlipWords";
2
+ export type { FlipWordsProps } from "./FlipWords";
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import {
5
+ motion,
6
+ useMotionValueEvent,
7
+ useScroll,
8
+ type Variants,
9
+ } from "framer-motion";
10
+ import { cn } from "../../utils/cn";
11
+
12
+ export interface FloatingNavbarProps {
13
+ /** Nav items rendered inside the pill container */
14
+ children?: ReactNode;
15
+ className?: string;
16
+ /** Position from top in pixels when visible (default 16) */
17
+ topOffset?: number;
18
+ }
19
+
20
+ const visible: Variants = {
21
+ hidden: {
22
+ y: -100,
23
+ opacity: 0,
24
+ transition: {
25
+ duration: 0.3,
26
+ ease: [0.16, 1, 0.3, 1],
27
+ },
28
+ },
29
+ visible: {
30
+ y: 0,
31
+ opacity: 1,
32
+ transition: {
33
+ duration: 0.3,
34
+ ease: [0.16, 1, 0.3, 1],
35
+ },
36
+ },
37
+ };
38
+
39
+ /**
40
+ * FloatingNavbar — sticky top navbar that hides on scroll down, shows on scroll up.
41
+ *
42
+ * Fixed position, centered, pill-shaped container with backdrop-blur and shadow.
43
+ * Uses framer-motion `useScroll` + `useMotionValueEvent` to detect scroll direction.
44
+ *
45
+ * @example
46
+ * <FloatingNavbar>
47
+ * <a href="#home">Home</a>
48
+ * <a href="#about">About</a>
49
+ * </FloatingNavbar>
50
+ */
51
+ export function FloatingNavbar({
52
+ children,
53
+ className,
54
+ topOffset = 16,
55
+ }: FloatingNavbarProps) {
56
+ const { scrollY } = useScroll();
57
+ const [hidden, setHidden] = useState(false);
58
+
59
+ useMotionValueEvent(scrollY, "change", (latest) => {
60
+ const previous = scrollY.getPrevious() ?? 0;
61
+ // Hide when scrolling down (past a small threshold), show when scrolling up
62
+ if (latest > previous && latest > 80) {
63
+ setHidden(true);
64
+ } else {
65
+ setHidden(false);
66
+ }
67
+ });
68
+
69
+ return (
70
+ <motion.div
71
+ variants={visible}
72
+ animate={hidden ? "hidden" : "visible"}
73
+ style={{ top: topOffset }}
74
+ className={cn(
75
+ "fixed inset-x-0 z-50 mx-auto flex w-fit max-w-[95%] items-center justify-center gap-1 rounded-full border border-line bg-bg/80 px-2 py-2 shadow-lg backdrop-blur-md",
76
+ className
77
+ )}
78
+ >
79
+ {children}
80
+ </motion.div>
81
+ );
82
+ }
@@ -0,0 +1,2 @@
1
+ export { FloatingNavbar } from "./FloatingNavbar";
2
+ export type { FloatingNavbarProps } from "./FloatingNavbar";
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import { useState, type ReactNode } from "react";
4
+ import { motion } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export interface FocusCardItem {
8
+ title: string;
9
+ description: string;
10
+ icon?: ReactNode;
11
+ }
12
+
13
+ export interface FocusCardsProps {
14
+ items: FocusCardItem[];
15
+ className?: string;
16
+ }
17
+
18
+ /**
19
+ * FocusCards — grid of cards where hovering one focuses it (full opacity)
20
+ * and blurs the others (opacity 30%).
21
+ *
22
+ * The hover state is tracked at the container level so siblings react to the
23
+ * hovered card. Uses framer-motion for the opacity transition.
24
+ *
25
+ * @example
26
+ * <FocusCards items={[
27
+ * { title: "速度", description: "毫秒级响应" },
28
+ * { title: "稳定", description: "99.9% 可用" },
29
+ * ]} />
30
+ */
31
+ export function FocusCards({ items, className }: FocusCardsProps) {
32
+ const [hovered, setHovered] = useState<number | null>(null);
33
+
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3",
38
+ className
39
+ )}
40
+ >
41
+ {items.map((item, index) => (
42
+ <FocusCard
43
+ key={index}
44
+ item={item}
45
+ isHovered={hovered === index}
46
+ isAnyHovered={hovered !== null}
47
+ onHover={() => setHovered(index)}
48
+ onLeave={() => setHovered(null)}
49
+ />
50
+ ))}
51
+ </div>
52
+ );
53
+ }
54
+
55
+ function FocusCard({
56
+ item,
57
+ isHovered,
58
+ isAnyHovered,
59
+ onHover,
60
+ onLeave,
61
+ }: {
62
+ item: FocusCardItem;
63
+ isHovered: boolean;
64
+ isAnyHovered: boolean;
65
+ onHover: () => void;
66
+ onLeave: () => void;
67
+ }) {
68
+ // Blurred when another card is hovered and this one is not
69
+ const dimmed = isAnyHovered && !isHovered;
70
+
71
+ return (
72
+ <motion.div
73
+ onMouseEnter={onHover}
74
+ onMouseLeave={onLeave}
75
+ animate={{ opacity: dimmed ? 0.3 : 1, scale: isHovered ? 1.02 : 1 }}
76
+ transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
77
+ className={cn(
78
+ "group flex flex-col rounded-xl border border-line bg-bg p-5 shadow-sm transition-colors",
79
+ isHovered && "border-line-strong shadow-md"
80
+ )}
81
+ >
82
+ {item.icon && (
83
+ <div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-surface-2 text-ink">
84
+ {item.icon}
85
+ </div>
86
+ )}
87
+ <h3 className="text-base font-semibold text-ink">{item.title}</h3>
88
+ <p className="mt-2 text-sm leading-relaxed text-ink-2">
89
+ {item.description}
90
+ </p>
91
+ </motion.div>
92
+ );
93
+ }
@@ -0,0 +1,2 @@
1
+ export { FocusCards } from "./FocusCards";
2
+ export type { FocusCardsProps, FocusCardItem } from "./FocusCards";
@@ -0,0 +1,66 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, type MouseEvent } from "react";
4
+ import { motion, useMotionValue, useMotionTemplate } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export interface GlareCardProps {
8
+ children: ReactNode;
9
+ className?: string;
10
+ /** Color of the glare highlight (default rgba(255,255,255,0.8)) */
11
+ glareColor?: string;
12
+ }
13
+
14
+ /**
15
+ * GlareCard — a dark card with a light "glare" reflection that follows the
16
+ * cursor across its surface.
17
+ *
18
+ * A radial gradient is positioned at the mouse coordinates via
19
+ * `useMotionValue` + `useMotionTemplate`, producing a moving specular
20
+ * highlight. Defaults to the dark palette so the glare reads clearly.
21
+ *
22
+ * @example
23
+ * <GlareCard className="p-8">
24
+ * <h3 className="text-white">悬停查看反光</h3>
25
+ * </GlareCard>
26
+ */
27
+ export function GlareCard({
28
+ children,
29
+ className,
30
+ glareColor = "rgba(255, 255, 255, 0.8)",
31
+ }: GlareCardProps) {
32
+ const mouseX = useMotionValue(-200);
33
+ const mouseY = useMotionValue(-200);
34
+
35
+ const glare = useMotionTemplate`radial-gradient(circle at ${mouseX}px ${mouseY}px, ${glareColor}, transparent 60%)`;
36
+
37
+ function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
38
+ const rect = e.currentTarget.getBoundingClientRect();
39
+ mouseX.set(e.clientX - rect.left);
40
+ mouseY.set(e.clientY - rect.top);
41
+ }
42
+
43
+ function handleMouseLeave() {
44
+ mouseX.set(-200);
45
+ mouseY.set(-200);
46
+ }
47
+
48
+ return (
49
+ <div
50
+ onMouseMove={handleMouseMove}
51
+ onMouseLeave={handleMouseLeave}
52
+ className={cn(
53
+ "group relative overflow-hidden rounded-xl border border-dark-line bg-dark-bg",
54
+ className
55
+ )}
56
+ >
57
+ {/* Glare overlay */}
58
+ <motion.div
59
+ className="pointer-events-none absolute inset-0 z-20 opacity-0 mix-blend-overlay transition-opacity duration-300 group-hover:opacity-100"
60
+ style={{ background: glare }}
61
+ />
62
+ {/* Content sits above the glare */}
63
+ <div className="relative z-10">{children}</div>
64
+ </div>
65
+ );
66
+ }
@@ -0,0 +1,2 @@
1
+ export { GlareCard } from "./GlareCard";
2
+ export type { GlareCardProps } from "./GlareCard";
@@ -0,0 +1,68 @@
1
+ "use client";
2
+
3
+ import { type ReactNode, type MouseEvent } from "react";
4
+ import { motion, useMotionValue, useMotionTemplate } from "framer-motion";
5
+ import { cn } from "../../utils/cn";
6
+
7
+ export interface GlowingEffectProps {
8
+ children: ReactNode;
9
+ className?: string;
10
+ /** Glow color (default "rgba(0,0,0,0.05)") */
11
+ color?: string;
12
+ /** Glow radius in px (default 300) */
13
+ radius?: number;
14
+ }
15
+
16
+ /**
17
+ * GlowingEffect — container with a mouse-following glowing border on hover.
18
+ *
19
+ * A radial gradient glow follows the cursor, lighting up the border region
20
+ * of the container. Unlike CardSpotlight (which tints the surface), this
21
+ * effect emphasizes the border edge with a soft glow. Based on Aceternity's
22
+ * Glowing Effect pattern, adapted to zmzai's monochrome palette.
23
+ *
24
+ * @example
25
+ * <GlowingEffect radius={300}>
26
+ * <div className="p-6">
27
+ * <h3>智能生成</h3>
28
+ * <p>一键创建内容</p>
29
+ * </div>
30
+ * </GlowingEffect>
31
+ */
32
+ export function GlowingEffect({
33
+ children,
34
+ className,
35
+ color = "rgba(0, 0, 0, 0.05)",
36
+ radius = 300,
37
+ }: GlowingEffectProps) {
38
+ const mouseX = useMotionValue(0);
39
+ const mouseY = useMotionValue(0);
40
+
41
+ function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
42
+ const rect = e.currentTarget.getBoundingClientRect();
43
+ mouseX.set(e.clientX - rect.left);
44
+ mouseY.set(e.clientY - rect.top);
45
+ }
46
+
47
+ const glow = useMotionTemplate`radial-gradient(${radius}px circle at ${mouseX}px ${mouseY}px, ${color}, transparent 70%)`;
48
+
49
+ return (
50
+ <div
51
+ onMouseMove={handleMouseMove}
52
+ className={cn(
53
+ "group relative overflow-hidden rounded-xl border border-line bg-bg transition-colors hover:border-line-strong",
54
+ className
55
+ )}
56
+ >
57
+ {/* Glow overlay — follows cursor, reveals on hover */}
58
+ <motion.div
59
+ className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
60
+ style={{ background: glow }}
61
+ />
62
+ {/* Inner border highlight for the "glowing border" feel */}
63
+ <div className="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-inset ring-ink/0 transition-shadow duration-300 group-hover:ring-ink/5" />
64
+ {/* Content */}
65
+ <div className="relative z-10">{children}</div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,2 @@
1
+ export { GlowingEffect } from "./GlowingEffect";
2
+ export type { GlowingEffectProps } from "./GlowingEffect";
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import {
4
+ createElement,
5
+ type ElementType,
6
+ type ReactNode,
7
+ type HTMLAttributes,
8
+ } from "react";
9
+ import { cn } from "../../utils/cn";
10
+
11
+ export interface HoverBorderGradientProps<T extends ElementType = "div">
12
+ extends Omit<HTMLAttributes<HTMLElement>, "children"> {
13
+ children: ReactNode;
14
+ className?: string;
15
+ /** Element type to render (default "div") */
16
+ as?: T;
17
+ /** Outer container className (the element that receives hover) */
18
+ containerClassName?: string;
19
+ }
20
+
21
+ /**
22
+ * HoverBorderGradient — container/badge whose border sweeps a gradient on hover.
23
+ *
24
+ * Pure CSS (no JS animation). The border is a conic-gradient mask that sits
25
+ * transparent until hover, then sweeps from transparent to ink. Based on
26
+ * Aceternity's Hover Border Gradient, adapted to zmzai's monochrome palette —
27
+ * the sweep uses the ink token rather than a rainbow gradient.
28
+ *
29
+ * @example
30
+ * <HoverBorderGradient as="button" className="rounded-full">
31
+ * <span className="px-6 py-2">开始体验</span>
32
+ * </HoverBorderGradient>
33
+ */
34
+ export function HoverBorderGradient<T extends ElementType = "div">({
35
+ children,
36
+ className,
37
+ containerClassName,
38
+ as,
39
+ ...props
40
+ }: HoverBorderGradientProps<T>) {
41
+ const Component = (as ?? "div") as ElementType;
42
+
43
+ return (
44
+ <>
45
+ {/* Scoped keyframes + mask rules (no global @property needed) */}
46
+ <style>{`
47
+ @keyframes hbg-spin {
48
+ to { transform: rotate(360deg); }
49
+ }
50
+ `}</style>
51
+
52
+ <Component
53
+ className={cn(
54
+ "group relative cursor-pointer overflow-hidden rounded-full border border-line bg-bg transition-colors duration-300 hover:border-transparent",
55
+ containerClassName
56
+ )}
57
+ {...props}
58
+ >
59
+ {/* Gradient sweep layer — sits behind content, masked to the border ring.
60
+ Transparent at rest, fades in + spins on hover. */}
61
+ <span
62
+ aria-hidden
63
+ className={cn(
64
+ "pointer-events-none absolute -inset-[100%] opacity-0 transition-opacity duration-300 group-hover:opacity-100",
65
+ className
66
+ )}
67
+ style={{
68
+ background:
69
+ "conic-gradient(from 0deg, transparent 0deg, transparent 270deg, var(--color-ink) 320deg, transparent 360deg)",
70
+ animation: "hbg-spin 2s linear infinite",
71
+ }}
72
+ />
73
+ {/* Inner content with solid background so only the 1px ring shows the sweep */}
74
+ <span className="relative z-10 block rounded-full bg-bg px-6 py-2.5">
75
+ {children}
76
+ </span>
77
+ </Component>
78
+ </>
79
+ );
80
+ }
@@ -0,0 +1,2 @@
1
+ export { HoverBorderGradient } from "./HoverBorderGradient";
2
+ export type { HoverBorderGradientProps } from "./HoverBorderGradient";