@moontra/moonui-pro 2.0.22 → 2.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.
- package/dist/index.mjs +215 -214
- package/package.json +4 -2
- package/src/__tests__/use-intersection-observer.test.tsx +216 -0
- package/src/__tests__/use-local-storage.test.tsx +174 -0
- package/src/__tests__/use-pro-access.test.tsx +183 -0
- package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
- package/src/components/advanced-chart/index.tsx +412 -0
- package/src/components/advanced-forms/index.tsx +431 -0
- package/src/components/animated-button/index.tsx +202 -0
- package/src/components/calendar/event-dialog.tsx +372 -0
- package/src/components/calendar/index.tsx +557 -0
- package/src/components/color-picker/index.tsx +434 -0
- package/src/components/dashboard/index.tsx +334 -0
- package/src/components/data-table/data-table.test.tsx +187 -0
- package/src/components/data-table/index.tsx +368 -0
- package/src/components/draggable-list/index.tsx +100 -0
- package/src/components/enhanced/button.tsx +360 -0
- package/src/components/enhanced/card.tsx +272 -0
- package/src/components/enhanced/dialog.tsx +248 -0
- package/src/components/enhanced/index.ts +3 -0
- package/src/components/error-boundary/index.tsx +111 -0
- package/src/components/file-upload/file-upload.test.tsx +242 -0
- package/src/components/file-upload/index.tsx +362 -0
- package/src/components/floating-action-button/index.tsx +209 -0
- package/src/components/github-stars/index.tsx +414 -0
- package/src/components/health-check/index.tsx +441 -0
- package/src/components/hover-card-3d/index.tsx +170 -0
- package/src/components/index.ts +76 -0
- package/src/components/kanban/index.tsx +436 -0
- package/src/components/lazy-component/index.tsx +342 -0
- package/src/components/magnetic-button/index.tsx +170 -0
- package/src/components/memory-efficient-data/index.tsx +352 -0
- package/src/components/optimized-image/index.tsx +427 -0
- package/src/components/performance-debugger/index.tsx +591 -0
- package/src/components/performance-monitor/index.tsx +775 -0
- package/src/components/pinch-zoom/index.tsx +172 -0
- package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
- package/src/components/rich-text-editor/index.tsx +1537 -0
- package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
- package/src/components/rich-text-editor/slash-commands.css +35 -0
- package/src/components/rich-text-editor/table-styles.css +65 -0
- package/src/components/spotlight-card/index.tsx +194 -0
- package/src/components/swipeable-card/index.tsx +100 -0
- package/src/components/timeline/index.tsx +333 -0
- package/src/components/ui/animated-button.tsx +185 -0
- package/src/components/ui/avatar.tsx +135 -0
- package/src/components/ui/badge.tsx +225 -0
- package/src/components/ui/button.tsx +221 -0
- package/src/components/ui/card.tsx +141 -0
- package/src/components/ui/checkbox.tsx +256 -0
- package/src/components/ui/color-picker.tsx +95 -0
- package/src/components/ui/dialog.tsx +332 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/hover-card-3d.tsx +103 -0
- package/src/components/ui/index.ts +33 -0
- package/src/components/ui/input.tsx +219 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/magnetic-button.tsx +129 -0
- package/src/components/ui/popover.tsx +183 -0
- package/src/components/ui/select.tsx +273 -0
- package/src/components/ui/separator.tsx +140 -0
- package/src/components/ui/slider.tsx +351 -0
- package/src/components/ui/spotlight-card.tsx +119 -0
- package/src/components/ui/switch.tsx +83 -0
- package/src/components/ui/tabs.tsx +195 -0
- package/src/components/ui/textarea.tsx +25 -0
- package/src/components/ui/toast.tsx +313 -0
- package/src/components/ui/tooltip.tsx +152 -0
- package/src/components/virtual-list/index.tsx +369 -0
- package/src/hooks/use-chart.ts +205 -0
- package/src/hooks/use-data-table.ts +182 -0
- package/src/hooks/use-docs-pro-access.ts +13 -0
- package/src/hooks/use-license-check.ts +65 -0
- package/src/hooks/use-subscription.ts +19 -0
- package/src/index.ts +14 -0
- package/src/lib/micro-interactions.ts +255 -0
- package/src/lib/utils.ts +6 -0
- package/src/patterns/login-form/index.tsx +276 -0
- package/src/patterns/login-form/types.ts +67 -0
- package/src/setupTests.ts +41 -0
- package/src/styles/design-system.css +365 -0
- package/src/styles/index.css +4 -0
- package/src/styles/tailwind.css +6 -0
- package/src/styles/tokens.css +453 -0
- package/src/types/moonui.d.ts +22 -0
- package/src/use-intersection-observer.tsx +154 -0
- package/src/use-local-storage.tsx +71 -0
- package/src/use-paddle.ts +138 -0
- package/src/use-performance-optimizer.ts +379 -0
- package/src/use-pro-access.ts +141 -0
- package/src/use-scroll-animation.ts +221 -0
- package/src/use-subscription.ts +37 -0
- package/src/use-toast.ts +32 -0
- package/src/utils/chart-helpers.ts +257 -0
- package/src/utils/cn.ts +69 -0
- package/src/utils/data-processing.ts +151 -0
- package/src/utils/license-guard.tsx +177 -0
- package/src/utils/license-validator.tsx +183 -0
- package/src/utils/package-guard.ts +60 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef, Suspense } from "react"
|
|
4
|
+
import { motion } from "framer-motion"
|
|
5
|
+
import { Card, CardContent } from "../ui/card"
|
|
6
|
+
import { Button } from "../ui/button"
|
|
7
|
+
import { Skeleton } from "@moontra/moonui"
|
|
8
|
+
import { cn } from "../../lib/utils"
|
|
9
|
+
import { Eye, Loader2, Lock, Sparkles, RefreshCw } from "lucide-react"
|
|
10
|
+
import { useSubscription } from "../../hooks/use-subscription"
|
|
11
|
+
|
|
12
|
+
export interface LazyComponentProps {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
fallback?: React.ReactNode
|
|
15
|
+
threshold?: number
|
|
16
|
+
rootMargin?: string
|
|
17
|
+
triggerOnce?: boolean
|
|
18
|
+
disabled?: boolean
|
|
19
|
+
onLoad?: () => void
|
|
20
|
+
onVisible?: () => void
|
|
21
|
+
showLoadingState?: boolean
|
|
22
|
+
delay?: number
|
|
23
|
+
className?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const LazyComponentInternal: React.FC<LazyComponentProps> = ({
|
|
27
|
+
children,
|
|
28
|
+
fallback,
|
|
29
|
+
threshold = 0.1,
|
|
30
|
+
rootMargin = "50px",
|
|
31
|
+
triggerOnce = true,
|
|
32
|
+
disabled = false,
|
|
33
|
+
onLoad,
|
|
34
|
+
onVisible,
|
|
35
|
+
showLoadingState = true,
|
|
36
|
+
delay = 0,
|
|
37
|
+
className
|
|
38
|
+
}) => {
|
|
39
|
+
const [isVisible, setIsVisible] = useState(disabled)
|
|
40
|
+
const [isLoaded, setIsLoaded] = useState(disabled)
|
|
41
|
+
const [hasTriggered, setHasTriggered] = useState(false)
|
|
42
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (disabled) {
|
|
46
|
+
setIsVisible(true)
|
|
47
|
+
setIsLoaded(true)
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const observer = new IntersectionObserver(
|
|
52
|
+
([entry]) => {
|
|
53
|
+
if (entry.isIntersecting && (!triggerOnce || !hasTriggered)) {
|
|
54
|
+
if (delay > 0) {
|
|
55
|
+
setTimeout(() => {
|
|
56
|
+
setIsVisible(true)
|
|
57
|
+
setHasTriggered(true)
|
|
58
|
+
onVisible?.()
|
|
59
|
+
|
|
60
|
+
// Simulate loading delay
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
setIsLoaded(true)
|
|
63
|
+
onLoad?.()
|
|
64
|
+
}, delay)
|
|
65
|
+
}, 100)
|
|
66
|
+
} else {
|
|
67
|
+
setIsVisible(true)
|
|
68
|
+
setIsLoaded(true)
|
|
69
|
+
setHasTriggered(true)
|
|
70
|
+
onVisible?.()
|
|
71
|
+
onLoad?.()
|
|
72
|
+
}
|
|
73
|
+
} else if (!entry.isIntersecting && !triggerOnce) {
|
|
74
|
+
setIsVisible(false)
|
|
75
|
+
setIsLoaded(false)
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
threshold,
|
|
80
|
+
rootMargin
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (containerRef.current) {
|
|
85
|
+
observer.observe(containerRef.current)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return () => {
|
|
89
|
+
if (containerRef.current) {
|
|
90
|
+
observer.unobserve(containerRef.current)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}, [threshold, rootMargin, triggerOnce, delay, disabled, hasTriggered, onLoad, onVisible])
|
|
94
|
+
|
|
95
|
+
const renderFallback = () => {
|
|
96
|
+
if (fallback) {
|
|
97
|
+
return fallback
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (showLoadingState) {
|
|
101
|
+
return (
|
|
102
|
+
<motion.div
|
|
103
|
+
initial={{ opacity: 0 }}
|
|
104
|
+
animate={{ opacity: 1 }}
|
|
105
|
+
className="flex items-center justify-center p-8"
|
|
106
|
+
>
|
|
107
|
+
<div className="text-center space-y-2">
|
|
108
|
+
{delay > 0 && isVisible && !isLoaded ? (
|
|
109
|
+
<Loader2 className="h-6 w-6 animate-spin mx-auto text-muted-foreground" />
|
|
110
|
+
) : (
|
|
111
|
+
<Eye className="h-6 w-6 mx-auto text-muted-foreground" />
|
|
112
|
+
)}
|
|
113
|
+
<p className="text-sm text-muted-foreground">
|
|
114
|
+
{isVisible && !isLoaded ? "Loading..." : "Scroll to load content"}
|
|
115
|
+
</p>
|
|
116
|
+
</div>
|
|
117
|
+
</motion.div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return <Skeleton className="w-full h-32" />
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<div ref={containerRef} className={cn("w-full", className)}>
|
|
126
|
+
{isLoaded ? (
|
|
127
|
+
<motion.div
|
|
128
|
+
initial={{ opacity: 0, y: 20 }}
|
|
129
|
+
animate={{ opacity: 1, y: 0 }}
|
|
130
|
+
transition={{ duration: 0.3 }}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</motion.div>
|
|
134
|
+
) : (
|
|
135
|
+
renderFallback()
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Lazy Image Component
|
|
142
|
+
export interface LazyImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
|
|
143
|
+
src: string
|
|
144
|
+
alt: string
|
|
145
|
+
fallbackSrc?: string
|
|
146
|
+
showPlaceholder?: boolean
|
|
147
|
+
threshold?: number
|
|
148
|
+
rootMargin?: string
|
|
149
|
+
onLoad?: () => void
|
|
150
|
+
onError?: () => void
|
|
151
|
+
className?: string
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const LazyImage: React.FC<LazyImageProps> = ({
|
|
155
|
+
src,
|
|
156
|
+
alt,
|
|
157
|
+
fallbackSrc,
|
|
158
|
+
showPlaceholder = true,
|
|
159
|
+
threshold = 0.1,
|
|
160
|
+
rootMargin = "50px",
|
|
161
|
+
onLoad,
|
|
162
|
+
onError,
|
|
163
|
+
className,
|
|
164
|
+
...props
|
|
165
|
+
}) => {
|
|
166
|
+
const [imageSrc, setImageSrc] = useState<string | null>(null)
|
|
167
|
+
const [imageError, setImageError] = useState(false)
|
|
168
|
+
const [isVisible, setIsVisible] = useState(false)
|
|
169
|
+
const imgRef = useRef<HTMLImageElement>(null)
|
|
170
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
171
|
+
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const observer = new IntersectionObserver(
|
|
174
|
+
([entry]) => {
|
|
175
|
+
if (entry.isIntersecting) {
|
|
176
|
+
setIsVisible(true)
|
|
177
|
+
observer.disconnect()
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
{ threshold, rootMargin }
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if (containerRef.current) {
|
|
184
|
+
observer.observe(containerRef.current)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return () => observer.disconnect()
|
|
188
|
+
}, [threshold, rootMargin])
|
|
189
|
+
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (!isVisible) return
|
|
192
|
+
|
|
193
|
+
const img = new Image()
|
|
194
|
+
img.onload = () => {
|
|
195
|
+
setImageSrc(src)
|
|
196
|
+
onLoad?.()
|
|
197
|
+
}
|
|
198
|
+
img.onerror = () => {
|
|
199
|
+
setImageError(true)
|
|
200
|
+
if (fallbackSrc) {
|
|
201
|
+
setImageSrc(fallbackSrc)
|
|
202
|
+
}
|
|
203
|
+
onError?.()
|
|
204
|
+
}
|
|
205
|
+
img.src = src
|
|
206
|
+
}, [isVisible, src, fallbackSrc, onLoad, onError])
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div ref={containerRef} className={cn("relative overflow-hidden", className)}>
|
|
210
|
+
{imageSrc ? (
|
|
211
|
+
<motion.img
|
|
212
|
+
ref={imgRef}
|
|
213
|
+
src={imageSrc}
|
|
214
|
+
alt={alt}
|
|
215
|
+
initial={{ opacity: 0 }}
|
|
216
|
+
animate={{ opacity: 1 }}
|
|
217
|
+
transition={{ duration: 0.3 }}
|
|
218
|
+
className="w-full h-full object-cover"
|
|
219
|
+
{...props}
|
|
220
|
+
/>
|
|
221
|
+
) : showPlaceholder ? (
|
|
222
|
+
<Skeleton className="w-full h-full min-h-32" />
|
|
223
|
+
) : null}
|
|
224
|
+
|
|
225
|
+
{imageError && !fallbackSrc && (
|
|
226
|
+
<div className="absolute inset-0 flex items-center justify-center bg-muted">
|
|
227
|
+
<p className="text-sm text-muted-foreground">Failed to load image</p>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Lazy List Component
|
|
235
|
+
export interface LazyListProps<T> {
|
|
236
|
+
items: T[]
|
|
237
|
+
renderItem: (item: T, index: number) => React.ReactNode
|
|
238
|
+
itemHeight?: number
|
|
239
|
+
batchSize?: number
|
|
240
|
+
threshold?: number
|
|
241
|
+
className?: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function LazyList<T>({
|
|
245
|
+
items,
|
|
246
|
+
renderItem,
|
|
247
|
+
itemHeight = 100,
|
|
248
|
+
batchSize = 10,
|
|
249
|
+
threshold = 0.5,
|
|
250
|
+
className
|
|
251
|
+
}: LazyListProps<T>) {
|
|
252
|
+
const [visibleItems, setVisibleItems] = useState<T[]>([])
|
|
253
|
+
const [currentBatch, setCurrentBatch] = useState(0)
|
|
254
|
+
const loadingRef = useRef<HTMLDivElement>(null)
|
|
255
|
+
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
setVisibleItems(items.slice(0, batchSize))
|
|
258
|
+
setCurrentBatch(1)
|
|
259
|
+
}, [items, batchSize])
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
const observer = new IntersectionObserver(
|
|
263
|
+
([entry]) => {
|
|
264
|
+
if (entry.isIntersecting && currentBatch * batchSize < items.length) {
|
|
265
|
+
const nextBatch = currentBatch + 1
|
|
266
|
+
const newItems = items.slice(0, nextBatch * batchSize)
|
|
267
|
+
setVisibleItems(newItems)
|
|
268
|
+
setCurrentBatch(nextBatch)
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
{ threshold }
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if (loadingRef.current) {
|
|
275
|
+
observer.observe(loadingRef.current)
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return () => observer.disconnect()
|
|
279
|
+
}, [currentBatch, items, batchSize, threshold])
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className={cn("space-y-2", className)}>
|
|
283
|
+
{visibleItems.map((item, index) => (
|
|
284
|
+
<motion.div
|
|
285
|
+
key={index}
|
|
286
|
+
initial={{ opacity: 0, y: 20 }}
|
|
287
|
+
animate={{ opacity: 1, y: 0 }}
|
|
288
|
+
transition={{ delay: (index % batchSize) * 0.05 }}
|
|
289
|
+
style={{ minHeight: itemHeight }}
|
|
290
|
+
>
|
|
291
|
+
{renderItem(item, index)}
|
|
292
|
+
</motion.div>
|
|
293
|
+
))}
|
|
294
|
+
|
|
295
|
+
{currentBatch * batchSize < items.length && (
|
|
296
|
+
<div ref={loadingRef} className="flex justify-center py-4">
|
|
297
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</div>
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export const LazyComponent: React.FC<LazyComponentProps> = ({ className, ...props }) => {
|
|
305
|
+
// Check if we're in docs mode or have pro access
|
|
306
|
+
const docsProAccess = { hasAccess: true } // Pro access assumed in package
|
|
307
|
+
const { hasProAccess, isLoading } = useSubscription()
|
|
308
|
+
|
|
309
|
+
// In docs mode, always show the component
|
|
310
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess
|
|
311
|
+
|
|
312
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
313
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
314
|
+
return (
|
|
315
|
+
<Card className={cn("w-fit", className)}>
|
|
316
|
+
<CardContent className="py-6 text-center">
|
|
317
|
+
<div className="space-y-4">
|
|
318
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
319
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
320
|
+
</div>
|
|
321
|
+
<div>
|
|
322
|
+
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
323
|
+
<p className="text-muted-foreground text-xs mb-4">
|
|
324
|
+
Lazy Component is available exclusively to MoonUI Pro subscribers.
|
|
325
|
+
</p>
|
|
326
|
+
<a href="/pricing">
|
|
327
|
+
<Button size="sm">
|
|
328
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
329
|
+
Upgrade to Pro
|
|
330
|
+
</Button>
|
|
331
|
+
</a>
|
|
332
|
+
</div>
|
|
333
|
+
</div>
|
|
334
|
+
</CardContent>
|
|
335
|
+
</Card>
|
|
336
|
+
)
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return <LazyComponentInternal className={className} {...props} />
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export type { LazyComponentProps, LazyImageProps, LazyListProps }
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useRef, useState } from "react"
|
|
4
|
+
import { motion, useMotionValue, useSpring, useTransform } from "framer-motion"
|
|
5
|
+
import { cn } from "../../lib/utils"
|
|
6
|
+
import { Card, CardContent } from "../ui/card"
|
|
7
|
+
import { Button } from "../ui/button"
|
|
8
|
+
import { Lock, Sparkles } from "lucide-react"
|
|
9
|
+
import { useSubscription } from "../../hooks/use-subscription"
|
|
10
|
+
|
|
11
|
+
export interface MagneticButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
12
|
+
strength?: number
|
|
13
|
+
range?: number
|
|
14
|
+
springConfig?: {
|
|
15
|
+
stiffness?: number
|
|
16
|
+
damping?: number
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const MagneticButtonInternal = React.forwardRef<HTMLButtonElement, MagneticButtonProps>(
|
|
21
|
+
({
|
|
22
|
+
children,
|
|
23
|
+
className,
|
|
24
|
+
strength = 0.3,
|
|
25
|
+
range = 100,
|
|
26
|
+
springConfig = { stiffness: 200, damping: 15 },
|
|
27
|
+
...props
|
|
28
|
+
}, ref) => {
|
|
29
|
+
const buttonRef = useRef<HTMLButtonElement>(null)
|
|
30
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
31
|
+
|
|
32
|
+
const x = useMotionValue(0)
|
|
33
|
+
const y = useMotionValue(0)
|
|
34
|
+
|
|
35
|
+
const springX = useSpring(x, springConfig)
|
|
36
|
+
const springY = useSpring(y, springConfig)
|
|
37
|
+
|
|
38
|
+
const rotateX = useTransform(springY, [-range, range], [5, -5])
|
|
39
|
+
const rotateY = useTransform(springX, [-range, range], [-5, 5])
|
|
40
|
+
|
|
41
|
+
const handleMouseMove = (e: React.MouseEvent) => {
|
|
42
|
+
if (!buttonRef.current) return
|
|
43
|
+
|
|
44
|
+
const rect = buttonRef.current.getBoundingClientRect()
|
|
45
|
+
const centerX = rect.left + rect.width / 2
|
|
46
|
+
const centerY = rect.top + rect.height / 2
|
|
47
|
+
|
|
48
|
+
const deltaX = e.clientX - centerX
|
|
49
|
+
const deltaY = e.clientY - centerY
|
|
50
|
+
|
|
51
|
+
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
|
|
52
|
+
|
|
53
|
+
if (distance < range) {
|
|
54
|
+
x.set(deltaX * strength)
|
|
55
|
+
y.set(deltaY * strength)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const handleMouseLeave = () => {
|
|
60
|
+
setIsHovered(false)
|
|
61
|
+
x.set(0)
|
|
62
|
+
y.set(0)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const handleMouseEnter = () => {
|
|
66
|
+
setIsHovered(true)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<motion.button
|
|
71
|
+
ref={(node) => {
|
|
72
|
+
buttonRef.current = node
|
|
73
|
+
if (typeof ref === "function") {
|
|
74
|
+
ref(node)
|
|
75
|
+
} else if (ref) {
|
|
76
|
+
ref.current = node
|
|
77
|
+
}
|
|
78
|
+
}}
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
|
81
|
+
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
|
82
|
+
"h-9 px-4 py-2",
|
|
83
|
+
className
|
|
84
|
+
)}
|
|
85
|
+
onMouseMove={handleMouseMove}
|
|
86
|
+
onMouseLeave={handleMouseLeave}
|
|
87
|
+
onMouseEnter={handleMouseEnter}
|
|
88
|
+
style={{
|
|
89
|
+
x: springX,
|
|
90
|
+
y: springY,
|
|
91
|
+
rotateX,
|
|
92
|
+
rotateY,
|
|
93
|
+
transformPerspective: 1000,
|
|
94
|
+
}}
|
|
95
|
+
whileHover={{ scale: 1.05 }}
|
|
96
|
+
whileTap={{ scale: 0.95 }}
|
|
97
|
+
animate={{
|
|
98
|
+
rotateX: isHovered ? rotateX : 0,
|
|
99
|
+
rotateY: isHovered ? rotateY : 0,
|
|
100
|
+
}}
|
|
101
|
+
transition={{ duration: 0.3 }}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
<motion.div
|
|
105
|
+
className="relative z-10"
|
|
106
|
+
animate={{
|
|
107
|
+
scale: isHovered ? 1.02 : 1,
|
|
108
|
+
}}
|
|
109
|
+
transition={{ duration: 0.2 }}
|
|
110
|
+
>
|
|
111
|
+
{children}
|
|
112
|
+
</motion.div>
|
|
113
|
+
|
|
114
|
+
{/* Glow effect */}
|
|
115
|
+
<motion.div
|
|
116
|
+
className="absolute inset-0 rounded-md bg-primary/20 blur-md"
|
|
117
|
+
animate={{
|
|
118
|
+
opacity: isHovered ? 0.8 : 0,
|
|
119
|
+
scale: isHovered ? 1.1 : 1,
|
|
120
|
+
}}
|
|
121
|
+
transition={{ duration: 0.3 }}
|
|
122
|
+
/>
|
|
123
|
+
</motion.button>
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
MagneticButtonInternal.displayName = "MagneticButtonInternal"
|
|
129
|
+
|
|
130
|
+
export const MagneticButton = React.forwardRef<HTMLButtonElement, MagneticButtonProps>(
|
|
131
|
+
({ className, ...props }, ref) => {
|
|
132
|
+
// Check if we're in docs mode or have pro access
|
|
133
|
+
const docsProAccess = { hasAccess: true } // Pro access assumed in package
|
|
134
|
+
const { hasProAccess, isLoading } = useSubscription()
|
|
135
|
+
|
|
136
|
+
// In docs mode, always show the component
|
|
137
|
+
const canShowComponent = docsProAccess.isDocsMode || hasProAccess
|
|
138
|
+
|
|
139
|
+
// If not in docs mode and no pro access, show upgrade prompt
|
|
140
|
+
if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
|
|
141
|
+
return (
|
|
142
|
+
<Card className={cn("w-fit", className)}>
|
|
143
|
+
<CardContent className="py-6 text-center">
|
|
144
|
+
<div className="space-y-4">
|
|
145
|
+
<div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
|
|
146
|
+
<Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
|
|
147
|
+
</div>
|
|
148
|
+
<div>
|
|
149
|
+
<h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
|
|
150
|
+
<p className="text-muted-foreground text-xs mb-4">
|
|
151
|
+
Magnetic Button is available exclusively to MoonUI Pro subscribers.
|
|
152
|
+
</p>
|
|
153
|
+
<a href="/pricing">
|
|
154
|
+
<Button size="sm">
|
|
155
|
+
<Sparkles className="mr-2 h-4 w-4" />
|
|
156
|
+
Upgrade to Pro
|
|
157
|
+
</Button>
|
|
158
|
+
</a>
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</CardContent>
|
|
162
|
+
</Card>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return <MagneticButtonInternal className={className} ref={ref} {...props} />
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
MagneticButton.displayName = "MagneticButton"
|