@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,130 @@
1
+ "use client";
2
+
3
+ import {
4
+ useRef,
5
+ useState,
6
+ type ReactNode,
7
+ type MouseEvent,
8
+ } from "react";
9
+ import {
10
+ motion,
11
+ useMotionValue,
12
+ useSpring,
13
+ AnimatePresence,
14
+ } from "framer-motion";
15
+ import { cn } from "../../utils/cn";
16
+
17
+ export interface TooltipCardProps {
18
+ /** The trigger element */
19
+ children: ReactNode;
20
+ /** Avatar image URL */
21
+ avatarSrc?: string;
22
+ /** Person / entity name */
23
+ name: string;
24
+ /** Role / subtitle (e.g. job title) */
25
+ role: string;
26
+ /** Body description */
27
+ description: string;
28
+ className?: string;
29
+ }
30
+
31
+ /**
32
+ * TooltipCard — a richer hover card with avatar + title + description.
33
+ *
34
+ * On hover, a card appears below the trigger and follows the mouse X
35
+ * position with spring physics. Aceternity-style, adapted to the zmzai
36
+ * monochrome palette.
37
+ *
38
+ * @example
39
+ * <TooltipCard
40
+ * name="张三"
41
+ * role="前端工程师"
42
+ * description="负责组件库与设计系统。"
43
+ * avatarSrc="/avatar.png"
44
+ * >
45
+ * <span className="underline">@zhangsan</span>
46
+ * </TooltipCard>
47
+ */
48
+ export function TooltipCard({
49
+ children,
50
+ avatarSrc,
51
+ name,
52
+ role,
53
+ description,
54
+ className,
55
+ }: TooltipCardProps) {
56
+ const ref = useRef<HTMLDivElement>(null);
57
+ const [hovered, setHovered] = useState(false);
58
+
59
+ const mouseX = useMotionValue(0);
60
+ const springConfig = { stiffness: 150, damping: 20 };
61
+ const translateX = useSpring(mouseX, springConfig);
62
+
63
+ const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
64
+ const rect = e.currentTarget.getBoundingClientRect();
65
+ mouseX.set(e.clientX - rect.left);
66
+ };
67
+
68
+ return (
69
+ <div
70
+ ref={ref}
71
+ onMouseEnter={() => setHovered(true)}
72
+ onMouseLeave={() => setHovered(false)}
73
+ onMouseMove={handleMouseMove}
74
+ className={cn("relative inline-flex", className)}
75
+ >
76
+ {children}
77
+
78
+ <AnimatePresence>
79
+ {hovered && (
80
+ <motion.div
81
+ initial={{ opacity: 0, scale: 0.9, y: -8 }}
82
+ animate={{ opacity: 1, scale: 1, y: 0 }}
83
+ exit={{ opacity: 0, scale: 0.9, y: -8 }}
84
+ transition={{ type: "spring", stiffness: 280, damping: 24 }}
85
+ style={{
86
+ position: "absolute",
87
+ top: "100%",
88
+ left: 0,
89
+ x: translateX,
90
+ translateX: "-50%",
91
+ marginTop: "12px",
92
+ }}
93
+ className="pointer-events-none z-50 w-64 origin-top overflow-hidden rounded-xl border border-line bg-bg shadow-lg"
94
+ >
95
+ {/* accent top border */}
96
+ <div className="h-0.5 w-full bg-ink" />
97
+ <div className="flex gap-3 p-4">
98
+ {/* Avatar */}
99
+ <div className="h-11 w-11 shrink-0 overflow-hidden rounded-full border border-line bg-surface">
100
+ {avatarSrc ? (
101
+ <img
102
+ src={avatarSrc}
103
+ alt={name}
104
+ className="h-full w-full object-cover"
105
+ />
106
+ ) : (
107
+ <div className="flex h-full w-full items-center justify-center text-sm font-semibold text-ink-2">
108
+ {name.charAt(0)}
109
+ </div>
110
+ )}
111
+ </div>
112
+ {/* Text */}
113
+ <div className="min-w-0 space-y-1">
114
+ <div className="flex items-baseline justify-between gap-2">
115
+ <p className="truncate text-sm font-semibold text-ink">
116
+ {name}
117
+ </p>
118
+ <p className="shrink-0 text-[11px] text-ink-3">{role}</p>
119
+ </div>
120
+ <p className="text-xs leading-relaxed text-ink-2">
121
+ {description}
122
+ </p>
123
+ </div>
124
+ </div>
125
+ </motion.div>
126
+ )}
127
+ </AnimatePresence>
128
+ </div>
129
+ );
130
+ }
@@ -0,0 +1,2 @@
1
+ export { TooltipCard } from "./TooltipCard";
2
+ export type { TooltipCardProps } from "./TooltipCard";
@@ -0,0 +1,110 @@
1
+ "use client";
2
+
3
+ import { useRef, useState, type ReactNode } from "react";
4
+ import {
5
+ motion,
6
+ useScroll,
7
+ useSpring,
8
+ useMotionValueEvent,
9
+ } from "framer-motion";
10
+ import { cn } from "../../utils/cn";
11
+
12
+ export interface TracingBeamProps {
13
+ children: ReactNode;
14
+ className?: string;
15
+ }
16
+
17
+ /**
18
+ * TracingBeam — vertical SVG beam that traces scroll progress.
19
+ *
20
+ * A thin vertical line sits on the left of the content. As the user
21
+ * scrolls through the section, the beam fills (gray -> ink) to indicate
22
+ * reading progress. Based on Aceternity's Tracing Beam, adapted to the
23
+ * zmzai monochrome palette.
24
+ *
25
+ * @example
26
+ * <TracingBeam>
27
+ * <article>…long-form content…</article>
28
+ * </TracingBeam>
29
+ */
30
+ export function TracingBeam({ children, className }: TracingBeamProps) {
31
+ const ref = useRef<HTMLDivElement>(null);
32
+ const [svgHeight, setSvgHeight] = useState(0);
33
+ const [progress, setProgress] = useState(0);
34
+
35
+ const { scrollYProgress } = useScroll({
36
+ target: ref,
37
+ offset: ["start 10%", "end 70%"],
38
+ });
39
+
40
+ const scaleY = useSpring(scrollYProgress, {
41
+ stiffness: 120,
42
+ damping: 30,
43
+ restDelta: 0.001,
44
+ });
45
+
46
+ useMotionValueEvent(scrollYProgress, "change", (v) => {
47
+ setProgress(v);
48
+ });
49
+
50
+ useMotionValueEvent(scrollYProgress, "change", () => {
51
+ if (ref.current) {
52
+ setSvgHeight(ref.current.offsetHeight);
53
+ }
54
+ });
55
+
56
+ return (
57
+ <div
58
+ ref={ref}
59
+ className={cn(
60
+ "relative mx-auto h-full w-full max-w-2xl",
61
+ className
62
+ )}
63
+ >
64
+ {/* SVG beam */}
65
+ <div className="absolute -left-8 top-3 hidden h-full w-[2px] md:block">
66
+ <svg
67
+ viewBox={`0 0 20 ${svgHeight}`}
68
+ width="20"
69
+ height={svgHeight}
70
+ className="block"
71
+ aria-hidden="true"
72
+ >
73
+ {/* Track */}
74
+ <path
75
+ d={`M 1 0 L 1 ${svgHeight}`}
76
+ fill="none"
77
+ stroke="currentColor"
78
+ strokeWidth={1.5}
79
+ className="text-line"
80
+ strokeOpacity={1}
81
+ />
82
+ {/* Progress */}
83
+ <motion.path
84
+ d={`M 1 0 L 1 ${svgHeight}`}
85
+ fill="none"
86
+ stroke="currentColor"
87
+ strokeWidth={1.5}
88
+ className="text-ink"
89
+ style={{ scaleY, transformOrigin: "top" }}
90
+ />
91
+ </svg>
92
+ {/* Dot at the top of the beam */}
93
+ <motion.div
94
+ className="absolute -left-[7px] top-0 h-4 w-4 rounded-full border border-line bg-surface"
95
+ animate={{
96
+ boxShadow:
97
+ progress > 0
98
+ ? "0 0 0 2px rgba(0,0,0,0.05)"
99
+ : "none",
100
+ }}
101
+ >
102
+ <div className="h-full w-full rounded-full bg-ink" />
103
+ </motion.div>
104
+ </div>
105
+
106
+ {/* Content */}
107
+ <div>{children}</div>
108
+ </div>
109
+ );
110
+ }
@@ -0,0 +1,2 @@
1
+ export { TracingBeam } from "./TracingBeam";
2
+ export type { TracingBeamProps } from "./TracingBeam";
@@ -0,0 +1,69 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, type HTMLAttributes } from "react";
4
+ import { cn } from "../../utils/cn";
5
+
6
+ export interface TypewriterProps extends HTMLAttributes<HTMLSpanElement> {
7
+ /** Text to type out */
8
+ text: string;
9
+ /** Milliseconds per character */
10
+ speed?: number;
11
+ /** Show a blinking caret while typing */
12
+ cursor?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Typewriter — classic character-by-character typing effect.
17
+ *
18
+ * Types out `text` one character at a time at `speed` ms per char.
19
+ * An optional blinking block cursor is shown while typing.
20
+ *
21
+ * @example
22
+ * <Typewriter text="$ npm install @zmzai/theme" speed={50} />
23
+ * <Typewriter text="No cursor" cursor={false} />
24
+ */
25
+ export function Typewriter({
26
+ text,
27
+ speed = 50,
28
+ cursor = true,
29
+ className,
30
+ ...props
31
+ }: TypewriterProps) {
32
+ const [count, setCount] = useState(0);
33
+
34
+ useEffect(() => {
35
+ setCount(0);
36
+ if (!text) return;
37
+
38
+ const id = setInterval(() => {
39
+ setCount((c) => {
40
+ if (c >= text.length) {
41
+ clearInterval(id);
42
+ return c;
43
+ }
44
+ return c + 1;
45
+ });
46
+ }, speed);
47
+
48
+ return () => clearInterval(id);
49
+ }, [text, speed]);
50
+
51
+ const done = count >= text.length;
52
+
53
+ return (
54
+ <span className={cn("font-mono", className)} {...props}>
55
+ {text.slice(0, count)}
56
+ {cursor && (
57
+ <span
58
+ className={cn(
59
+ "ml-0.5 inline-block w-[2px] self-stretch bg-ink align-middle",
60
+ !done && "animate-pulse"
61
+ )}
62
+ style={{ height: "1em" }}
63
+ >
64
+ &nbsp;
65
+ </span>
66
+ )}
67
+ </span>
68
+ );
69
+ }
@@ -0,0 +1,2 @@
1
+ export { Typewriter } from "./Typewriter";
2
+ export type { TypewriterProps } from "./Typewriter";
@@ -0,0 +1,170 @@
1
+ "use client";
2
+
3
+ import {
4
+ useState,
5
+ useRef,
6
+ useEffect,
7
+ useCallback,
8
+ type FormEvent,
9
+ } from "react";
10
+ import { motion, AnimatePresence, useAnimate } from "framer-motion";
11
+ import { cn } from "../../utils/cn";
12
+
13
+ export interface VanishInputProps {
14
+ /** Rotating placeholder strings */
15
+ placeholders: string[];
16
+ /** Called on submit (Enter) */
17
+ onSubmit: (value: string) => void;
18
+ className?: string;
19
+ }
20
+
21
+ /**
22
+ * VanishInput — search input with a rotating placeholder that "vanishes".
23
+ *
24
+ * While idle, placeholders cycle. Each character of the current placeholder
25
+ * animates upward and fades out (the vanish effect) before the next
26
+ * placeholder appears. Typing hides the placeholder. Based on Aceternity's
27
+ * Vanish Input, adapted to the zmzai palette.
28
+ *
29
+ * @example
30
+ * <VanishInput
31
+ * placeholders={["搜索文档…", "输入关键词…"]}
32
+ * onSubmit={(v) => search(v)}
33
+ * />
34
+ */
35
+ export function VanishInput({
36
+ placeholders,
37
+ onSubmit,
38
+ className,
39
+ }: VanishInputProps) {
40
+ const [value, setValue] = useState("");
41
+ const [currentPlaceholder, setCurrentPlaceholder] = useState(0);
42
+ const [scope, animate] = useAnimate<HTMLDivElement>();
43
+ const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
44
+ const cyclingRef = useRef(true);
45
+
46
+ const nextPlaceholder = useCallback(() => {
47
+ setCurrentPlaceholder((prev) =>
48
+ prev === placeholders.length - 1 ? 0 : prev + 1
49
+ );
50
+ }, [placeholders.length]);
51
+
52
+ const startVanishing = useCallback(async () => {
53
+ if (!cyclingRef.current || placeholders.length === 0) return;
54
+ cyclingRef.current = false;
55
+ try {
56
+ // vanish: each char moves up & fades out
57
+ await animate(
58
+ "span",
59
+ { y: -20, opacity: 0 },
60
+ { duration: 0.3, delay: staggerDelay() }
61
+ );
62
+ // swap to next placeholder
63
+ nextPlaceholder();
64
+ // reset chars below + hidden, then animate in
65
+ await animate(
66
+ "span",
67
+ { y: 20, opacity: 0 },
68
+ { duration: 0 }
69
+ );
70
+ await animate(
71
+ "span",
72
+ { y: 0, opacity: 1 },
73
+ { duration: 0.3, delay: staggerDelay() }
74
+ );
75
+ } finally {
76
+ cyclingRef.current = true;
77
+ }
78
+ }, [animate, nextPlaceholder, placeholders.length]);
79
+
80
+ // cycle placeholders while idle
81
+ useEffect(() => {
82
+ if (!placeholders.length) return;
83
+ intervalRef.current = setInterval(() => {
84
+ if (value.length === 0) {
85
+ startVanishing();
86
+ }
87
+ }, 3000);
88
+ return () => {
89
+ if (intervalRef.current) clearInterval(intervalRef.current);
90
+ };
91
+ }, [placeholders.length, value.length, startVanishing]);
92
+
93
+ const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
94
+ e.preventDefault();
95
+ if (value.trim().length === 0) return;
96
+ onSubmit(value);
97
+ setValue("");
98
+ };
99
+
100
+ const showPlaceholder = value.length === 0 && placeholders.length > 0;
101
+
102
+ return (
103
+ <form
104
+ onSubmit={handleSubmit}
105
+ className={cn(
106
+ "relative flex w-full max-w-xl items-center overflow-hidden rounded-full border border-line bg-surface px-5 py-2.5 transition-colors focus-within:border-ink/50",
107
+ className
108
+ )}
109
+ >
110
+ {/* Placeholder */}
111
+ {showPlaceholder && (
112
+ <div className="pointer-events-none absolute inset-0 flex items-center overflow-hidden px-5">
113
+ <AnimatePresence mode="wait">
114
+ <motion.div
115
+ key={currentPlaceholder}
116
+ ref={scope}
117
+ className="truncate text-sm text-ink-3"
118
+ >
119
+ {placeholders[currentPlaceholder]
120
+ .split("")
121
+ .map((ch, i) => (
122
+ <motion.span
123
+ key={`ch-${i}`}
124
+ initial={{ opacity: 1, y: 0 }}
125
+ animate={{ opacity: 1, y: 0 }}
126
+ className="inline-block whitespace-pre"
127
+ >
128
+ {ch}
129
+ </motion.span>
130
+ ))}
131
+ </motion.div>
132
+ </AnimatePresence>
133
+ </div>
134
+ )}
135
+
136
+ {/* Input */}
137
+ <input
138
+ value={value}
139
+ onChange={(e) => setValue(e.target.value)}
140
+ type="text"
141
+ className="relative z-10 w-full bg-transparent text-sm text-ink outline-none placeholder:text-transparent"
142
+ />
143
+
144
+ {/* Submit button */}
145
+ <button
146
+ type="submit"
147
+ aria-label="提交"
148
+ className="ml-2 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ink text-white transition-transform hover:scale-105 active:scale-95"
149
+ >
150
+ <svg
151
+ className="h-3.5 w-3.5"
152
+ viewBox="0 0 24 24"
153
+ fill="none"
154
+ stroke="currentColor"
155
+ strokeWidth={2}
156
+ strokeLinecap="round"
157
+ strokeLinejoin="round"
158
+ >
159
+ <path d="M5 12h14" />
160
+ <path d="m12 5 7 7-7 7" />
161
+ </svg>
162
+ </button>
163
+ </form>
164
+ );
165
+ }
166
+
167
+ /** Stagger helper: returns a stagger function usable as the `delay` option. */
168
+ function staggerDelay() {
169
+ return (i: number) => i * 0.03;
170
+ }
@@ -0,0 +1,2 @@
1
+ export { VanishInput } from "./VanishInput";
2
+ export type { VanishInputProps } from "./VanishInput";
@@ -0,0 +1,77 @@
1
+ "use client";
2
+
3
+ import { useRef, type ReactNode, type MouseEvent } from "react";
4
+ import {
5
+ motion,
6
+ useMotionValue,
7
+ useSpring,
8
+ useMotionTemplate,
9
+ useTransform,
10
+ } from "framer-motion";
11
+ import { cn } from "../../utils/cn";
12
+
13
+ export interface WobbleCardProps {
14
+ children: ReactNode;
15
+ className?: string;
16
+ }
17
+
18
+ const MAX_ROTATE = 6; // deg
19
+ const MAX_TRANSLATE = 10; // px
20
+ const SPRING = { stiffness: 150, damping: 15, mass: 0.1 };
21
+
22
+ /**
23
+ * WobbleCard — a card that wobbles (translates + rotates + skews slightly)
24
+ * based on the cursor position.
25
+ *
26
+ * Two `useMotionValue`s track the normalized mouse position (-1..1). They
27
+ * are fed through `useTransform` into rotation / translation targets, then
28
+ * smoothed with `useSpring` and composed via `useMotionTemplate` into the
29
+ * final transform string.
30
+ *
31
+ * @example
32
+ * <WobbleCard className="rounded-xl bg-surface p-8">
33
+ * <h3>悬停我</h3>
34
+ * </WobbleCard>
35
+ */
36
+ export function WobbleCard({ children, className }: WobbleCardProps) {
37
+ const ref = useRef<HTMLDivElement>(null);
38
+ const mx = useMotionValue(0);
39
+ const my = useMotionValue(0);
40
+
41
+ const rotateXRaw = useSpring(useTransform(my, [-0.5, 0.5], [MAX_ROTATE, -MAX_ROTATE]), SPRING);
42
+ const rotateYRaw = useSpring(useTransform(mx, [-0.5, 0.5], [-MAX_ROTATE, MAX_ROTATE]), SPRING);
43
+ const translateXRaw = useSpring(useTransform(mx, [-0.5, 0.5], [-MAX_TRANSLATE, MAX_TRANSLATE]), SPRING);
44
+ const translateYRaw = useSpring(useTransform(my, [-0.5, 0.5], [-MAX_TRANSLATE, MAX_TRANSLATE]), SPRING);
45
+
46
+ const transform = useMotionTemplate`perspective(1000px) rotateX(${rotateXRaw}deg) rotateY(${rotateYRaw}deg) translateX(${translateXRaw}px) translateY(${translateYRaw}px)`;
47
+
48
+ function handleMouseMove(e: MouseEvent<HTMLDivElement>) {
49
+ const el = ref.current;
50
+ if (!el) return;
51
+ const rect = el.getBoundingClientRect();
52
+ const relX = (e.clientX - rect.left) / rect.width - 0.5;
53
+ const relY = (e.clientY - rect.top) / rect.height - 0.5;
54
+ mx.set(relX);
55
+ my.set(relY);
56
+ }
57
+
58
+ function handleMouseLeave() {
59
+ mx.set(0);
60
+ my.set(0);
61
+ }
62
+
63
+ return (
64
+ <motion.div
65
+ ref={ref}
66
+ onMouseMove={handleMouseMove}
67
+ onMouseLeave={handleMouseLeave}
68
+ style={{ transform }}
69
+ className={cn(
70
+ "relative rounded-xl border border-line bg-surface p-8 transition-colors duration-200 hover:border-line-strong",
71
+ className
72
+ )}
73
+ >
74
+ {children}
75
+ </motion.div>
76
+ );
77
+ }
@@ -0,0 +1,2 @@
1
+ export { WobbleCard } from "./WobbleCard";
2
+ export type { WobbleCardProps } from "./WobbleCard";
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @zmzai/theme — zmzai 全品牌设计系统
3
+ *
4
+ * Usage in your app's globals.css:
5
+ * @import "tailwindcss";
6
+ * @import "@zmzai/theme/tokens";
7
+ * @import "@zmzai/theme/fonts";
8
+ *
9
+ * Usage in components:
10
+ * import { Button, Card, Logo, Wordmark } from "@zmzai/theme";
11
+ */
12
+
13
+ // Components
14
+ export * from "./components";
15
+
16
+ // Brand assets
17
+ export * from "./brand";
18
+
19
+ // Utils
20
+ export { cn } from "./utils/cn";
21
+ export * as motionPresets from "./utils/motion";
@@ -0,0 +1,42 @@
1
+ /*
2
+ * @zmzai/theme — MiSans Font Faces
3
+ *
4
+ * 小米开源字体,免费商用(OFL 协议)。
5
+ * 简体中文全子集,通过 jsDelivr CDN 加载 woff2。
6
+ * font-display: swap 保证加载期间用 fallback,加载完无缝切换。
7
+ *
8
+ * Import in your app's globals.css:
9
+ * @import "@zmzai/theme/fonts";
10
+ */
11
+
12
+ @font-face {
13
+ font-family: 'MiSans';
14
+ src: url('https://cdn.jsdelivr.net/gh/dsrkafuu/misans@main/raw/Normal/woff2/MiSans-Regular.woff2') format('woff2');
15
+ font-weight: 400;
16
+ font-style: normal;
17
+ font-display: swap;
18
+ }
19
+
20
+ @font-face {
21
+ font-family: 'MiSans';
22
+ src: url('https://cdn.jsdelivr.net/gh/dsrkafuu/misans@main/raw/Normal/woff2/MiSans-Medium.woff2') format('woff2');
23
+ font-weight: 500;
24
+ font-style: normal;
25
+ font-display: swap;
26
+ }
27
+
28
+ @font-face {
29
+ font-family: 'MiSans';
30
+ src: url('https://cdn.jsdelivr.net/gh/dsrkafuu/misans@main/raw/Normal/woff2/MiSans-Semibold.woff2') format('woff2');
31
+ font-weight: 600;
32
+ font-style: normal;
33
+ font-display: swap;
34
+ }
35
+
36
+ @font-face {
37
+ font-family: 'MiSans';
38
+ src: url('https://cdn.jsdelivr.net/gh/dsrkafuu/misans@main/raw/Normal/woff2/MiSans-Bold.woff2') format('woff2');
39
+ font-weight: 700;
40
+ font-style: normal;
41
+ font-display: swap;
42
+ }