@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.
Files changed (99) hide show
  1. package/dist/index.mjs +215 -214
  2. package/package.json +4 -2
  3. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  4. package/src/__tests__/use-local-storage.test.tsx +174 -0
  5. package/src/__tests__/use-pro-access.test.tsx +183 -0
  6. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  7. package/src/components/advanced-chart/index.tsx +412 -0
  8. package/src/components/advanced-forms/index.tsx +431 -0
  9. package/src/components/animated-button/index.tsx +202 -0
  10. package/src/components/calendar/event-dialog.tsx +372 -0
  11. package/src/components/calendar/index.tsx +557 -0
  12. package/src/components/color-picker/index.tsx +434 -0
  13. package/src/components/dashboard/index.tsx +334 -0
  14. package/src/components/data-table/data-table.test.tsx +187 -0
  15. package/src/components/data-table/index.tsx +368 -0
  16. package/src/components/draggable-list/index.tsx +100 -0
  17. package/src/components/enhanced/button.tsx +360 -0
  18. package/src/components/enhanced/card.tsx +272 -0
  19. package/src/components/enhanced/dialog.tsx +248 -0
  20. package/src/components/enhanced/index.ts +3 -0
  21. package/src/components/error-boundary/index.tsx +111 -0
  22. package/src/components/file-upload/file-upload.test.tsx +242 -0
  23. package/src/components/file-upload/index.tsx +362 -0
  24. package/src/components/floating-action-button/index.tsx +209 -0
  25. package/src/components/github-stars/index.tsx +414 -0
  26. package/src/components/health-check/index.tsx +441 -0
  27. package/src/components/hover-card-3d/index.tsx +170 -0
  28. package/src/components/index.ts +76 -0
  29. package/src/components/kanban/index.tsx +436 -0
  30. package/src/components/lazy-component/index.tsx +342 -0
  31. package/src/components/magnetic-button/index.tsx +170 -0
  32. package/src/components/memory-efficient-data/index.tsx +352 -0
  33. package/src/components/optimized-image/index.tsx +427 -0
  34. package/src/components/performance-debugger/index.tsx +591 -0
  35. package/src/components/performance-monitor/index.tsx +775 -0
  36. package/src/components/pinch-zoom/index.tsx +172 -0
  37. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  38. package/src/components/rich-text-editor/index.tsx +1537 -0
  39. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  40. package/src/components/rich-text-editor/slash-commands.css +35 -0
  41. package/src/components/rich-text-editor/table-styles.css +65 -0
  42. package/src/components/spotlight-card/index.tsx +194 -0
  43. package/src/components/swipeable-card/index.tsx +100 -0
  44. package/src/components/timeline/index.tsx +333 -0
  45. package/src/components/ui/animated-button.tsx +185 -0
  46. package/src/components/ui/avatar.tsx +135 -0
  47. package/src/components/ui/badge.tsx +225 -0
  48. package/src/components/ui/button.tsx +221 -0
  49. package/src/components/ui/card.tsx +141 -0
  50. package/src/components/ui/checkbox.tsx +256 -0
  51. package/src/components/ui/color-picker.tsx +95 -0
  52. package/src/components/ui/dialog.tsx +332 -0
  53. package/src/components/ui/dropdown-menu.tsx +200 -0
  54. package/src/components/ui/hover-card-3d.tsx +103 -0
  55. package/src/components/ui/index.ts +33 -0
  56. package/src/components/ui/input.tsx +219 -0
  57. package/src/components/ui/label.tsx +26 -0
  58. package/src/components/ui/magnetic-button.tsx +129 -0
  59. package/src/components/ui/popover.tsx +183 -0
  60. package/src/components/ui/select.tsx +273 -0
  61. package/src/components/ui/separator.tsx +140 -0
  62. package/src/components/ui/slider.tsx +351 -0
  63. package/src/components/ui/spotlight-card.tsx +119 -0
  64. package/src/components/ui/switch.tsx +83 -0
  65. package/src/components/ui/tabs.tsx +195 -0
  66. package/src/components/ui/textarea.tsx +25 -0
  67. package/src/components/ui/toast.tsx +313 -0
  68. package/src/components/ui/tooltip.tsx +152 -0
  69. package/src/components/virtual-list/index.tsx +369 -0
  70. package/src/hooks/use-chart.ts +205 -0
  71. package/src/hooks/use-data-table.ts +182 -0
  72. package/src/hooks/use-docs-pro-access.ts +13 -0
  73. package/src/hooks/use-license-check.ts +65 -0
  74. package/src/hooks/use-subscription.ts +19 -0
  75. package/src/index.ts +14 -0
  76. package/src/lib/micro-interactions.ts +255 -0
  77. package/src/lib/utils.ts +6 -0
  78. package/src/patterns/login-form/index.tsx +276 -0
  79. package/src/patterns/login-form/types.ts +67 -0
  80. package/src/setupTests.ts +41 -0
  81. package/src/styles/design-system.css +365 -0
  82. package/src/styles/index.css +4 -0
  83. package/src/styles/tailwind.css +6 -0
  84. package/src/styles/tokens.css +453 -0
  85. package/src/types/moonui.d.ts +22 -0
  86. package/src/use-intersection-observer.tsx +154 -0
  87. package/src/use-local-storage.tsx +71 -0
  88. package/src/use-paddle.ts +138 -0
  89. package/src/use-performance-optimizer.ts +379 -0
  90. package/src/use-pro-access.ts +141 -0
  91. package/src/use-scroll-animation.ts +221 -0
  92. package/src/use-subscription.ts +37 -0
  93. package/src/use-toast.ts +32 -0
  94. package/src/utils/chart-helpers.ts +257 -0
  95. package/src/utils/cn.ts +69 -0
  96. package/src/utils/data-processing.ts +151 -0
  97. package/src/utils/license-guard.tsx +177 -0
  98. package/src/utils/license-validator.tsx +183 -0
  99. package/src/utils/package-guard.ts +60 -0
@@ -0,0 +1,431 @@
1
+ "use client"
2
+
3
+ import React, { useState } from "react"
4
+ import { motion, AnimatePresence } from "framer-motion"
5
+ import { useForm } from "react-hook-form"
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
7
+ import { Button } from "../ui/button"
8
+ import { Input } from "../ui/input"
9
+ import { Label } from "../ui/label"
10
+ import { Textarea } from "../ui/textarea"
11
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select"
12
+ import { Checkbox } from "../ui/checkbox"
13
+ import { Switch } from "../ui/switch"
14
+ import { Badge } from "../ui/badge"
15
+ import { cn } from "../../lib/utils"
16
+ import { Lock, Sparkles, CheckCircle, AlertCircle, Eye, EyeOff, Upload, X } from "lucide-react"
17
+ import { useSubscription } from "../../hooks/use-subscription"
18
+
19
+ export interface AdvancedFormField {
20
+ name: string
21
+ label: string
22
+ type: "text" | "email" | "password" | "textarea" | "select" | "checkbox" | "switch" | "file" | "number" | "url" | "tel"
23
+ placeholder?: string
24
+ required?: boolean
25
+ options?: Array<{ value: string; label: string }>
26
+ validation?: {
27
+ minLength?: number
28
+ maxLength?: number
29
+ pattern?: RegExp
30
+ custom?: (value: any) => string | true
31
+ }
32
+ description?: string
33
+ defaultValue?: any
34
+ }
35
+
36
+ export interface AdvancedFormsProps {
37
+ fields: AdvancedFormField[]
38
+ onSubmit: (data: any) => void | Promise<void>
39
+ title?: string
40
+ description?: string
41
+ submitText?: string
42
+ enableAutoSave?: boolean
43
+ showProgress?: boolean
44
+ className?: string
45
+ layout?: "vertical" | "horizontal" | "grid"
46
+ columns?: number
47
+ }
48
+
49
+ const AdvancedFormsInternal: React.FC<AdvancedFormsProps> = ({
50
+ fields,
51
+ onSubmit,
52
+ title = "Advanced Form",
53
+ description,
54
+ submitText = "Submit",
55
+ enableAutoSave = false,
56
+ showProgress = true,
57
+ className,
58
+ layout = "vertical",
59
+ columns = 2
60
+ }) => {
61
+ const [isSubmitting, setIsSubmitting] = useState(false)
62
+ const [showPasswords, setShowPasswords] = useState<Record<string, boolean>>({})
63
+ const [uploadedFiles, setUploadedFiles] = useState<Record<string, File[]>>({})
64
+
65
+ const {
66
+ register,
67
+ handleSubmit,
68
+ watch,
69
+ formState: { errors, isValid, touchedFields },
70
+ setValue,
71
+ getValues
72
+ } = useForm<any>({
73
+ mode: "onChange",
74
+ defaultValues: fields.reduce((acc, field) => ({
75
+ ...acc,
76
+ [field.name]: field.defaultValue || (field.type === "checkbox" ? false : "")
77
+ }), {})
78
+ })
79
+
80
+ const watchedValues = watch()
81
+ const completedFields = Object.keys(touchedFields).length
82
+ const progress = fields.length > 0 ? (completedFields / fields.length) * 100 : 0
83
+
84
+ const handleFormSubmit = async (data: any) => {
85
+ setIsSubmitting(true)
86
+ try {
87
+ await onSubmit(data)
88
+ } finally {
89
+ setIsSubmitting(false)
90
+ }
91
+ }
92
+
93
+ const togglePasswordVisibility = (fieldName: string) => {
94
+ setShowPasswords(prev => ({
95
+ ...prev,
96
+ [fieldName]: !prev[fieldName]
97
+ }))
98
+ }
99
+
100
+ const handleFileUpload = (fieldName: string, files: FileList | null) => {
101
+ if (files) {
102
+ const fileArray = Array.from(files)
103
+ setUploadedFiles(prev => ({
104
+ ...prev,
105
+ [fieldName]: fileArray
106
+ }))
107
+ setValue(fieldName as any, fileArray as any)
108
+ }
109
+ }
110
+
111
+ const removeFile = (fieldName: string, index: number) => {
112
+ setUploadedFiles(prev => {
113
+ const newFiles = [...(prev[fieldName] || [])]
114
+ newFiles.splice(index, 1)
115
+ return {
116
+ ...prev,
117
+ [fieldName]: newFiles
118
+ }
119
+ })
120
+ setValue(fieldName as any, uploadedFiles[fieldName]?.filter((_, i) => i !== index) || [] as any)
121
+ }
122
+
123
+ const renderField = (field: AdvancedFormField, index: number) => {
124
+ const error = errors[field.name]
125
+ const isTouched = touchedFields[field.name]
126
+
127
+ return (
128
+ <motion.div
129
+ key={field.name}
130
+ initial={{ opacity: 0, y: 20 }}
131
+ animate={{ opacity: 1, y: 0 }}
132
+ transition={{ delay: index * 0.1 }}
133
+ className="space-y-2"
134
+ >
135
+ <div className="flex items-center justify-between">
136
+ <Label htmlFor={field.name} className="flex items-center gap-2">
137
+ {field.label}
138
+ {field.required && <span className="text-destructive">*</span>}
139
+ {isTouched && !error && (
140
+ <CheckCircle className="h-4 w-4 text-green-500" />
141
+ )}
142
+ {error && <AlertCircle className="h-4 w-4 text-destructive" />}
143
+ </Label>
144
+ </div>
145
+
146
+ <div className="relative">
147
+ {field.type === "text" || field.type === "email" || field.type === "number" || field.type === "url" || field.type === "tel" && (
148
+ <Input
149
+ id={field.name}
150
+ type={field.type}
151
+ placeholder={field.placeholder}
152
+ className={cn(error && "border-destructive")}
153
+ {...register(field.name, {
154
+ required: field.required ? `${field.label} is required` : false,
155
+ minLength: field.validation?.minLength ? {
156
+ value: field.validation.minLength,
157
+ message: `Minimum ${field.validation.minLength} characters required`
158
+ } : undefined,
159
+ maxLength: field.validation?.maxLength ? {
160
+ value: field.validation.maxLength,
161
+ message: `Maximum ${field.validation.maxLength} characters allowed`
162
+ } : undefined,
163
+ pattern: field.validation?.pattern ? {
164
+ value: field.validation.pattern,
165
+ message: "Invalid format"
166
+ } : undefined,
167
+ validate: field.validation?.custom
168
+ })}
169
+ />
170
+ )}
171
+
172
+ {field.type === "password" && (
173
+ <div className="relative">
174
+ <Input
175
+ id={field.name}
176
+ type={showPasswords[field.name] ? "text" : "password"}
177
+ placeholder={field.placeholder}
178
+ className={cn("pr-10", error && "border-destructive")}
179
+ {...register(field.name, {
180
+ required: field.required ? `${field.label} is required` : false,
181
+ minLength: field.validation?.minLength ? {
182
+ value: field.validation.minLength,
183
+ message: `Minimum ${field.validation.minLength} characters required`
184
+ } : undefined
185
+ })}
186
+ />
187
+ <Button
188
+ type="button"
189
+ variant="ghost"
190
+ size="sm"
191
+ className="absolute right-1 top-1/2 -translate-y-1/2 h-8 w-8 p-0"
192
+ onClick={() => togglePasswordVisibility(field.name)}
193
+ >
194
+ {showPasswords[field.name] ? (
195
+ <EyeOff className="h-4 w-4" />
196
+ ) : (
197
+ <Eye className="h-4 w-4" />
198
+ )}
199
+ </Button>
200
+ </div>
201
+ )}
202
+
203
+ {field.type === "textarea" && (
204
+ <Textarea
205
+ id={field.name}
206
+ placeholder={field.placeholder}
207
+ className={cn(error && "border-destructive")}
208
+ rows={3}
209
+ {...register(field.name, {
210
+ required: field.required ? `${field.label} is required` : false,
211
+ maxLength: field.validation?.maxLength ? {
212
+ value: field.validation.maxLength,
213
+ message: `Maximum ${field.validation.maxLength} characters allowed`
214
+ } : undefined
215
+ })}
216
+ />
217
+ )}
218
+
219
+ {field.type === "select" && (
220
+ <Select
221
+ onValueChange={(value) => setValue(field.name, value)}
222
+ defaultValue={field.defaultValue}
223
+ >
224
+ <SelectTrigger className={cn(error && "border-destructive")}>
225
+ <SelectValue placeholder={field.placeholder} />
226
+ </SelectTrigger>
227
+ <SelectContent>
228
+ {field.options?.map((option) => (
229
+ <SelectItem key={option.value} value={option.value}>
230
+ {option.label}
231
+ </SelectItem>
232
+ ))}
233
+ </SelectContent>
234
+ </Select>
235
+ )}
236
+
237
+ {field.type === "checkbox" && (
238
+ <div className="flex items-center space-x-2">
239
+ <Checkbox
240
+ id={field.name}
241
+ {...register(field.name)}
242
+ onCheckedChange={(checked) => setValue(field.name, checked)}
243
+ />
244
+ <Label htmlFor={field.name} className="text-sm">
245
+ {field.placeholder || "Check this option"}
246
+ </Label>
247
+ </div>
248
+ )}
249
+
250
+ {field.type === "switch" && (
251
+ <div className="flex items-center space-x-2">
252
+ <Switch
253
+ id={field.name}
254
+ {...register(field.name)}
255
+ onCheckedChange={(checked) => setValue(field.name, checked)}
256
+ />
257
+ <Label htmlFor={field.name} className="text-sm">
258
+ {field.placeholder || "Toggle this option"}
259
+ </Label>
260
+ </div>
261
+ )}
262
+
263
+ {field.type === "file" && (
264
+ <div className="space-y-2">
265
+ <div className="flex items-center justify-center w-full">
266
+ <label
267
+ htmlFor={field.name}
268
+ className="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-lg cursor-pointer bg-muted/50 hover:bg-muted/80 transition-colors"
269
+ >
270
+ <div className="flex flex-col items-center justify-center pt-5 pb-6">
271
+ <Upload className="w-8 h-8 mb-4 text-muted-foreground" />
272
+ <p className="mb-2 text-sm text-muted-foreground">
273
+ <span className="font-semibold">Click to upload</span> or drag and drop
274
+ </p>
275
+ </div>
276
+ <input
277
+ id={field.name}
278
+ type="file"
279
+ multiple
280
+ className="hidden"
281
+ onChange={(e) => handleFileUpload(field.name, e.target.files)}
282
+ />
283
+ </label>
284
+ </div>
285
+
286
+ {uploadedFiles[field.name] && uploadedFiles[field.name].length > 0 && (
287
+ <div className="space-y-2">
288
+ {uploadedFiles[field.name].map((file, index) => (
289
+ <div key={index} className="flex items-center justify-between p-2 bg-muted rounded-md">
290
+ <span className="text-sm truncate">{file.name}</span>
291
+ <Button
292
+ type="button"
293
+ variant="ghost"
294
+ size="sm"
295
+ onClick={() => removeFile(field.name, index)}
296
+ >
297
+ <X className="h-4 w-4" />
298
+ </Button>
299
+ </div>
300
+ ))}
301
+ </div>
302
+ )}
303
+ </div>
304
+ )}
305
+ </div>
306
+
307
+ {field.description && (
308
+ <p className="text-sm text-muted-foreground">{field.description}</p>
309
+ )}
310
+
311
+ <AnimatePresence>
312
+ {error && (
313
+ <motion.p
314
+ initial={{ opacity: 0, height: 0 }}
315
+ animate={{ opacity: 1, height: "auto" }}
316
+ exit={{ opacity: 0, height: 0 }}
317
+ className="text-sm text-destructive"
318
+ >
319
+ {error.message as string}
320
+ </motion.p>
321
+ )}
322
+ </AnimatePresence>
323
+ </motion.div>
324
+ )
325
+ }
326
+
327
+ const gridClassName = layout === "grid" ? `grid grid-cols-1 md:grid-cols-${columns} gap-6` : "space-y-6"
328
+
329
+ return (
330
+ <Card className={cn("w-full max-w-4xl", className)}>
331
+ <CardHeader>
332
+ <div className="flex items-center justify-between">
333
+ <div>
334
+ <CardTitle>{title}</CardTitle>
335
+ {description && <CardDescription>{description}</CardDescription>}
336
+ </div>
337
+ <Badge variant="pro" className="ml-2">
338
+ PRO
339
+ </Badge>
340
+ </div>
341
+
342
+ {showProgress && (
343
+ <div className="space-y-2">
344
+ <div className="flex justify-between text-sm">
345
+ <span>Progress</span>
346
+ <span>{Math.round(progress)}%</span>
347
+ </div>
348
+ <div className="w-full bg-muted rounded-full h-2">
349
+ <motion.div
350
+ className="bg-primary h-2 rounded-full"
351
+ initial={{ width: 0 }}
352
+ animate={{ width: `${progress}%` }}
353
+ transition={{ duration: 0.3 }}
354
+ />
355
+ </div>
356
+ </div>
357
+ )}
358
+ </CardHeader>
359
+
360
+ <CardContent>
361
+ <form onSubmit={handleSubmit(handleFormSubmit)} className="space-y-6">
362
+ <div className={gridClassName}>
363
+ {fields.map((field, index) => renderField(field, index))}
364
+ </div>
365
+
366
+ <div className="flex items-center justify-between pt-6">
367
+ <div className="text-sm text-muted-foreground">
368
+ {completedFields}/{fields.length} fields completed
369
+ </div>
370
+
371
+ <Button
372
+ type="submit"
373
+ disabled={isSubmitting || !isValid}
374
+ className="min-w-32"
375
+ >
376
+ {isSubmitting ? (
377
+ <motion.div
378
+ animate={{ rotate: 360 }}
379
+ transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
380
+ className="w-4 h-4 border-2 border-white border-t-transparent rounded-full"
381
+ />
382
+ ) : (
383
+ submitText
384
+ )}
385
+ </Button>
386
+ </div>
387
+ </form>
388
+ </CardContent>
389
+ </Card>
390
+ )
391
+ }
392
+
393
+ export const AdvancedForms: React.FC<AdvancedFormsProps> = ({ className, ...props }) => {
394
+ // Check if we're in docs mode or have pro access
395
+ const docsProAccess = { hasAccess: true } // Pro access assumed in package
396
+ const { hasProAccess, isLoading } = useSubscription()
397
+
398
+ // In docs mode, always show the component
399
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
400
+
401
+ // If not in docs mode and no pro access, show upgrade prompt
402
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
403
+ return (
404
+ <Card className={cn("w-fit", className)}>
405
+ <CardContent className="py-6 text-center">
406
+ <div className="space-y-4">
407
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
408
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
409
+ </div>
410
+ <div>
411
+ <h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
412
+ <p className="text-muted-foreground text-xs mb-4">
413
+ Advanced Forms is available exclusively to MoonUI Pro subscribers.
414
+ </p>
415
+ <a href="/pricing">
416
+ <Button size="sm">
417
+ <Sparkles className="mr-2 h-4 w-4" />
418
+ Upgrade to Pro
419
+ </Button>
420
+ </a>
421
+ </div>
422
+ </div>
423
+ </CardContent>
424
+ </Card>
425
+ )
426
+ }
427
+
428
+ return <AdvancedFormsInternal className={className} {...props} />
429
+ }
430
+
431
+ export type { AdvancedFormField, AdvancedFormsProps }
@@ -0,0 +1,202 @@
1
+ "use client"
2
+
3
+ import React, { useState } from "react"
4
+ import { motion } from "framer-motion"
5
+ import { Loader2, Check, X, Lock, Sparkles } from "lucide-react"
6
+ import { cn } from "../../lib/utils"
7
+ import { cva, type VariantProps } from "class-variance-authority"
8
+ import { Card, CardContent } from "../ui/card"
9
+ import { Button } from "../ui/button"
10
+ import { useSubscription } from "../../hooks/use-subscription"
11
+
12
+ const animatedButtonVariants = cva(
13
+ "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 overflow-hidden",
14
+ {
15
+ variants: {
16
+ variant: {
17
+ default: "bg-primary text-primary-foreground shadow hover:bg-primary/90",
18
+ destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
19
+ outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
20
+ secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
21
+ ghost: "hover:bg-accent hover:text-accent-foreground",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2",
26
+ sm: "h-8 rounded-md px-3 text-xs",
27
+ lg: "h-10 rounded-md px-8",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ },
34
+ }
35
+ )
36
+
37
+ export interface AnimatedButtonProps
38
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
+ VariantProps<typeof animatedButtonVariants> {
40
+ state?: "idle" | "loading" | "success" | "error"
41
+ onStateChange?: (state: "idle" | "loading" | "success" | "error") => void
42
+ }
43
+
44
+ const AnimatedButtonInternal = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
45
+ ({ className, variant, size, state = "idle", onStateChange, children, onClick, ...props }, ref) => {
46
+ const [internalState, setInternalState] = useState<"idle" | "loading" | "success" | "error">("idle")
47
+ const currentState = state !== "idle" ? state : internalState
48
+
49
+ const handleClick = async (e: React.MouseEvent<HTMLButtonElement>) => {
50
+ if (currentState === "loading") return
51
+
52
+ setInternalState("loading")
53
+ onStateChange?.("loading")
54
+
55
+ if (onClick) {
56
+ try {
57
+ await onClick(e)
58
+ setInternalState("success")
59
+ onStateChange?.("success")
60
+
61
+ setTimeout(() => {
62
+ setInternalState("idle")
63
+ onStateChange?.("idle")
64
+ }, 2000)
65
+ } catch (error) {
66
+ setInternalState("error")
67
+ onStateChange?.("error")
68
+
69
+ setTimeout(() => {
70
+ setInternalState("idle")
71
+ onStateChange?.("idle")
72
+ }, 2000)
73
+ }
74
+ }
75
+ }
76
+
77
+ const getIcon = () => {
78
+ switch (currentState) {
79
+ case "loading":
80
+ return <Loader2 className="h-4 w-4 animate-spin" />
81
+ case "success":
82
+ return <Check className="h-4 w-4" />
83
+ case "error":
84
+ return <X className="h-4 w-4" />
85
+ default:
86
+ return null
87
+ }
88
+ }
89
+
90
+ const getBackgroundColor = () => {
91
+ switch (currentState) {
92
+ case "success":
93
+ return "bg-green-600"
94
+ case "error":
95
+ return "bg-red-600"
96
+ default:
97
+ return ""
98
+ }
99
+ }
100
+
101
+ return (
102
+ <button
103
+ className={cn(animatedButtonVariants({ variant, size }), className)}
104
+ ref={ref}
105
+ onClick={handleClick}
106
+ disabled={currentState === "loading" || props.disabled}
107
+ {...props}
108
+ >
109
+ <motion.div
110
+ className="flex items-center gap-2"
111
+ animate={{
112
+ width: currentState !== "idle" ? "auto" : "100%"
113
+ }}
114
+ transition={{ duration: 0.2 }}
115
+ >
116
+ <motion.div
117
+ animate={{
118
+ scale: currentState !== "idle" ? 1 : 0,
119
+ width: currentState !== "idle" ? "auto" : 0
120
+ }}
121
+ transition={{ duration: 0.2 }}
122
+ className="flex items-center"
123
+ >
124
+ {getIcon()}
125
+ </motion.div>
126
+
127
+ <motion.span
128
+ animate={{
129
+ opacity: currentState === "idle" ? 1 : 0,
130
+ x: currentState !== "idle" ? -20 : 0
131
+ }}
132
+ transition={{ duration: 0.2 }}
133
+ >
134
+ {children}
135
+ </motion.span>
136
+ </motion.div>
137
+
138
+ {/* Background animation */}
139
+ <motion.div
140
+ className={cn(
141
+ "absolute inset-0 rounded-md",
142
+ getBackgroundColor()
143
+ )}
144
+ initial={{ scale: 0, opacity: 0 }}
145
+ animate={{
146
+ scale: currentState === "success" || currentState === "error" ? 1 : 0,
147
+ opacity: currentState === "success" || currentState === "error" ? 1 : 0
148
+ }}
149
+ transition={{ duration: 0.3 }}
150
+ style={{ zIndex: -1 }}
151
+ />
152
+ </button>
153
+ )
154
+ }
155
+ )
156
+
157
+ AnimatedButtonInternal.displayName = "AnimatedButtonInternal"
158
+
159
+ export const AnimatedButton = React.forwardRef<HTMLButtonElement, AnimatedButtonProps>(
160
+ ({ className, ...props }, ref) => {
161
+ // Check if we're in docs mode or have pro access
162
+ const docsProAccess = { hasAccess: true } // Pro access assumed in package
163
+ const { hasProAccess, isLoading } = useSubscription()
164
+
165
+ // In docs mode, always show the component
166
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
167
+
168
+ // If not in docs mode and no pro access, show upgrade prompt
169
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
170
+ return (
171
+ <Card className={cn("w-fit", className)}>
172
+ <CardContent className="py-6 text-center">
173
+ <div className="space-y-4">
174
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
175
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
176
+ </div>
177
+ <div>
178
+ <h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
179
+ <p className="text-muted-foreground text-xs mb-4">
180
+ Animated Button is available exclusively to MoonUI Pro subscribers.
181
+ </p>
182
+ <a href="/pricing">
183
+ <Button size="sm">
184
+ <Sparkles className="mr-2 h-4 w-4" />
185
+ Upgrade to Pro
186
+ </Button>
187
+ </a>
188
+ </div>
189
+ </div>
190
+ </CardContent>
191
+ </Card>
192
+ )
193
+ }
194
+
195
+ return <AnimatedButtonInternal className={className} ref={ref} {...props} />
196
+ }
197
+ )
198
+
199
+ AnimatedButton.displayName = "AnimatedButton"
200
+
201
+ export { animatedButtonVariants }
202
+ export type { AnimatedButtonProps }