@moontra/moonui-pro 2.0.22 → 2.0.23

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 (96) hide show
  1. package/package.json +2 -1
  2. package/src/__tests__/use-intersection-observer.test.tsx +216 -0
  3. package/src/__tests__/use-local-storage.test.tsx +174 -0
  4. package/src/__tests__/use-pro-access.test.tsx +183 -0
  5. package/src/components/advanced-chart/advanced-chart.test.tsx +281 -0
  6. package/src/components/advanced-chart/index.tsx +412 -0
  7. package/src/components/advanced-forms/index.tsx +431 -0
  8. package/src/components/animated-button/index.tsx +202 -0
  9. package/src/components/calendar/event-dialog.tsx +372 -0
  10. package/src/components/calendar/index.tsx +531 -0
  11. package/src/components/color-picker/index.tsx +434 -0
  12. package/src/components/dashboard/index.tsx +334 -0
  13. package/src/components/data-table/data-table.test.tsx +187 -0
  14. package/src/components/data-table/index.tsx +368 -0
  15. package/src/components/draggable-list/index.tsx +100 -0
  16. package/src/components/enhanced/button.tsx +360 -0
  17. package/src/components/enhanced/card.tsx +272 -0
  18. package/src/components/enhanced/dialog.tsx +248 -0
  19. package/src/components/enhanced/index.ts +3 -0
  20. package/src/components/error-boundary/index.tsx +111 -0
  21. package/src/components/file-upload/file-upload.test.tsx +242 -0
  22. package/src/components/file-upload/index.tsx +362 -0
  23. package/src/components/floating-action-button/index.tsx +209 -0
  24. package/src/components/github-stars/index.tsx +414 -0
  25. package/src/components/health-check/index.tsx +441 -0
  26. package/src/components/hover-card-3d/index.tsx +170 -0
  27. package/src/components/index.ts +76 -0
  28. package/src/components/kanban/index.tsx +436 -0
  29. package/src/components/lazy-component/index.tsx +342 -0
  30. package/src/components/magnetic-button/index.tsx +170 -0
  31. package/src/components/memory-efficient-data/index.tsx +352 -0
  32. package/src/components/optimized-image/index.tsx +427 -0
  33. package/src/components/performance-debugger/index.tsx +591 -0
  34. package/src/components/performance-monitor/index.tsx +775 -0
  35. package/src/components/pinch-zoom/index.tsx +172 -0
  36. package/src/components/rich-text-editor/index-old-backup.tsx +443 -0
  37. package/src/components/rich-text-editor/index.tsx +1537 -0
  38. package/src/components/rich-text-editor/slash-commands-extension.ts +220 -0
  39. package/src/components/rich-text-editor/slash-commands.css +35 -0
  40. package/src/components/rich-text-editor/table-styles.css +65 -0
  41. package/src/components/spotlight-card/index.tsx +194 -0
  42. package/src/components/swipeable-card/index.tsx +100 -0
  43. package/src/components/timeline/index.tsx +333 -0
  44. package/src/components/ui/animated-button.tsx +185 -0
  45. package/src/components/ui/avatar.tsx +135 -0
  46. package/src/components/ui/badge.tsx +225 -0
  47. package/src/components/ui/button.tsx +221 -0
  48. package/src/components/ui/card.tsx +141 -0
  49. package/src/components/ui/checkbox.tsx +256 -0
  50. package/src/components/ui/color-picker.tsx +95 -0
  51. package/src/components/ui/dialog.tsx +332 -0
  52. package/src/components/ui/dropdown-menu.tsx +200 -0
  53. package/src/components/ui/hover-card-3d.tsx +103 -0
  54. package/src/components/ui/index.ts +33 -0
  55. package/src/components/ui/input.tsx +219 -0
  56. package/src/components/ui/label.tsx +26 -0
  57. package/src/components/ui/magnetic-button.tsx +129 -0
  58. package/src/components/ui/popover.tsx +183 -0
  59. package/src/components/ui/select.tsx +273 -0
  60. package/src/components/ui/separator.tsx +140 -0
  61. package/src/components/ui/slider.tsx +351 -0
  62. package/src/components/ui/spotlight-card.tsx +119 -0
  63. package/src/components/ui/switch.tsx +83 -0
  64. package/src/components/ui/tabs.tsx +195 -0
  65. package/src/components/ui/textarea.tsx +25 -0
  66. package/src/components/ui/toast.tsx +313 -0
  67. package/src/components/ui/tooltip.tsx +152 -0
  68. package/src/components/virtual-list/index.tsx +369 -0
  69. package/src/hooks/use-chart.ts +205 -0
  70. package/src/hooks/use-data-table.ts +182 -0
  71. package/src/hooks/use-docs-pro-access.ts +13 -0
  72. package/src/hooks/use-license-check.ts +65 -0
  73. package/src/hooks/use-subscription.ts +19 -0
  74. package/src/index.ts +11 -0
  75. package/src/lib/micro-interactions.ts +255 -0
  76. package/src/lib/utils.ts +6 -0
  77. package/src/patterns/login-form/index.tsx +276 -0
  78. package/src/patterns/login-form/types.ts +67 -0
  79. package/src/setupTests.ts +41 -0
  80. package/src/styles/design-system.css +365 -0
  81. package/src/styles/index.css +4 -0
  82. package/src/styles/tailwind.css +6 -0
  83. package/src/styles/tokens.css +453 -0
  84. package/src/types/moonui.d.ts +22 -0
  85. package/src/use-intersection-observer.tsx +154 -0
  86. package/src/use-local-storage.tsx +71 -0
  87. package/src/use-paddle.ts +138 -0
  88. package/src/use-performance-optimizer.ts +379 -0
  89. package/src/use-pro-access.ts +141 -0
  90. package/src/use-scroll-animation.ts +221 -0
  91. package/src/use-subscription.ts +37 -0
  92. package/src/use-toast.ts +32 -0
  93. package/src/utils/chart-helpers.ts +257 -0
  94. package/src/utils/cn.ts +69 -0
  95. package/src/utils/data-processing.ts +151 -0
  96. package/src/utils/license-validator.tsx +183 -0
@@ -0,0 +1,362 @@
1
+ "use client"
2
+
3
+ import React from 'react'
4
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './card'
5
+ import { Button } from './button'
6
+ import { Progress } from './progress'
7
+ import { Badge } from './badge'
8
+ import {
9
+ Upload,
10
+ File,
11
+ Image,
12
+ X,
13
+ Download,
14
+ FileText,
15
+ Video,
16
+ Music,
17
+ Archive,
18
+ AlertCircle,
19
+ CheckCircle2,
20
+ Loader2,
21
+ Lock,
22
+ Sparkles
23
+ } from 'lucide-react'
24
+ import { cn } from '../../lib/utils'
25
+ import { useDocsProAccess } from '@/components/docs/docs-pro-provider'
26
+ import { useSubscription } from '../../hooks/use-subscription'
27
+
28
+ export interface FileUploadProps {
29
+ accept?: string
30
+ multiple?: boolean
31
+ maxSize?: number // in MB
32
+ maxFiles?: number
33
+ onUpload?: (files: File[]) => Promise<void>
34
+ onRemove?: (file: File) => void
35
+ className?: string
36
+ disabled?: boolean
37
+ showPreview?: boolean
38
+ allowedTypes?: string[]
39
+ }
40
+
41
+ export interface UploadedFile {
42
+ file: File
43
+ id: string
44
+ status: 'uploading' | 'success' | 'error'
45
+ progress: number
46
+ error?: string
47
+ url?: string
48
+ }
49
+
50
+ const getFileIcon = (type: string) => {
51
+ if (type.startsWith('image/')) return <Image className="h-4 w-4" />
52
+ if (type.startsWith('video/')) return <Video className="h-4 w-4" />
53
+ if (type.startsWith('audio/')) return <Music className="h-4 w-4" />
54
+ if (type.includes('pdf')) return <FileText className="h-4 w-4" />
55
+ if (type.includes('zip') || type.includes('rar')) return <Archive className="h-4 w-4" />
56
+ return <File className="h-4 w-4" />
57
+ }
58
+
59
+ const formatFileSize = (bytes: number): string => {
60
+ if (bytes === 0) return '0 Bytes'
61
+ const k = 1024
62
+ const sizes = ['Bytes', 'KB', 'MB', 'GB']
63
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
64
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
65
+ }
66
+
67
+ export function FileUpload({
68
+ accept = '*',
69
+ multiple = false,
70
+ maxSize = 10, // 10MB default
71
+ maxFiles = 5,
72
+ onUpload,
73
+ onRemove,
74
+ className,
75
+ disabled = false,
76
+ showPreview = true,
77
+ allowedTypes = []
78
+ }: FileUploadProps) {
79
+ // Check if we're in docs mode or have pro access
80
+ const docsProAccess = useDocsProAccess()
81
+ const { hasProAccess, isLoading } = useSubscription()
82
+
83
+ // In docs mode, always show the component
84
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
85
+
86
+ // If not in docs mode and no pro access, show upgrade prompt
87
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
88
+ return (
89
+ <Card className={cn("w-full", className)}>
90
+ <CardContent className="py-12 text-center">
91
+ <div className="max-w-md mx-auto space-y-4">
92
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
93
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
94
+ </div>
95
+ <div>
96
+ <h3 className="font-semibold text-lg mb-2">Pro Feature</h3>
97
+ <p className="text-muted-foreground text-sm mb-4">
98
+ File Upload is available exclusively to MoonUI Pro subscribers.
99
+ </p>
100
+ <div className="flex gap-3 justify-center">
101
+ <a href="/pricing">
102
+ <Button size="sm">
103
+ <Sparkles className="mr-2 h-4 w-4" />
104
+ Upgrade to Pro
105
+ </Button>
106
+ </a>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </CardContent>
111
+ </Card>
112
+ )
113
+ }
114
+
115
+ const [files, setFiles] = React.useState<UploadedFile[]>([])
116
+ const [isDragOver, setIsDragOver] = React.useState(false)
117
+ const [isUploading, setIsUploading] = React.useState(false)
118
+ const fileInputRef = React.useRef<HTMLInputElement>(null)
119
+
120
+ const validateFile = (file: File): string | null => {
121
+ // Check file size
122
+ if (file.size > maxSize * 1024 * 1024) {
123
+ return `File size must be less than ${maxSize}MB`
124
+ }
125
+
126
+ // Check file type
127
+ if (allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
128
+ return `File type ${file.type} is not allowed`
129
+ }
130
+
131
+ return null
132
+ }
133
+
134
+ const handleFileSelect = async (selectedFiles: FileList | null) => {
135
+ if (!selectedFiles || disabled) return
136
+
137
+ const fileArray = Array.from(selectedFiles)
138
+
139
+ // Check max files limit
140
+ if (files.length + fileArray.length > maxFiles) {
141
+ alert(`Maximum ${maxFiles} files allowed`)
142
+ return
143
+ }
144
+
145
+ const validFiles: UploadedFile[] = []
146
+ const invalidFiles: string[] = []
147
+
148
+ fileArray.forEach(file => {
149
+ const error = validateFile(file)
150
+ if (error) {
151
+ invalidFiles.push(`${file.name}: ${error}`)
152
+ } else {
153
+ validFiles.push({
154
+ file,
155
+ id: Math.random().toString(36).substr(2, 9),
156
+ status: 'uploading',
157
+ progress: 0
158
+ })
159
+ }
160
+ })
161
+
162
+ if (invalidFiles.length > 0) {
163
+ alert(invalidFiles.join('\n'))
164
+ }
165
+
166
+ if (validFiles.length === 0) return
167
+
168
+ setFiles(prev => [...prev, ...validFiles])
169
+ setIsUploading(true)
170
+
171
+ // Simulate upload process
172
+ if (onUpload) {
173
+ try {
174
+ await onUpload(validFiles.map(f => f.file))
175
+
176
+ // Update all files to success
177
+ setFiles(prev => prev.map(f =>
178
+ validFiles.find(vf => vf.id === f.id)
179
+ ? { ...f, status: 'success' as const, progress: 100 }
180
+ : f
181
+ ))
182
+ } catch (error) {
183
+ // Update all files to error
184
+ setFiles(prev => prev.map(f =>
185
+ validFiles.find(vf => vf.id === f.id)
186
+ ? { ...f, status: 'error' as const, error: 'Upload failed' }
187
+ : f
188
+ ))
189
+ }
190
+ } else {
191
+ // Simulate progress
192
+ for (const validFile of validFiles) {
193
+ const interval = setInterval(() => {
194
+ setFiles(prev => prev.map(f =>
195
+ f.id === validFile.id && f.progress < 100
196
+ ? { ...f, progress: f.progress + 10 }
197
+ : f
198
+ ))
199
+ }, 100)
200
+
201
+ setTimeout(() => {
202
+ clearInterval(interval)
203
+ setFiles(prev => prev.map(f =>
204
+ f.id === validFile.id
205
+ ? { ...f, status: 'success' as const, progress: 100 }
206
+ : f
207
+ ))
208
+ }, 1000)
209
+ }
210
+ }
211
+
212
+ setIsUploading(false)
213
+ }
214
+
215
+ const handleRemove = (fileToRemove: UploadedFile) => {
216
+ setFiles(prev => prev.filter(f => f.id !== fileToRemove.id))
217
+ if (onRemove) {
218
+ onRemove(fileToRemove.file)
219
+ }
220
+ }
221
+
222
+ const handleDrop = (e: React.DragEvent<HTMLDivElement>) => {
223
+ e.preventDefault()
224
+ setIsDragOver(false)
225
+ handleFileSelect(e.dataTransfer.files)
226
+ }
227
+
228
+ const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
229
+ e.preventDefault()
230
+ setIsDragOver(true)
231
+ }
232
+
233
+ const handleDragLeave = (e: React.DragEvent<HTMLDivElement>) => {
234
+ e.preventDefault()
235
+ setIsDragOver(false)
236
+ }
237
+
238
+ const handleClick = () => {
239
+ if (!disabled) {
240
+ fileInputRef.current?.click()
241
+ }
242
+ }
243
+
244
+ return (
245
+ <Card className={cn("w-full", className)}>
246
+ <CardHeader>
247
+ <CardTitle className="flex items-center gap-2">
248
+ <Upload className="h-5 w-5" />
249
+ File Upload
250
+ </CardTitle>
251
+ <CardDescription>
252
+ {multiple ? `Upload up to ${maxFiles} files` : 'Upload a file'} (max {maxSize}MB each)
253
+ </CardDescription>
254
+ </CardHeader>
255
+ <CardContent className="space-y-4">
256
+ {/* Drop Zone */}
257
+ <div
258
+ className={cn(
259
+ "border-2 border-dashed rounded-lg p-8 text-center cursor-pointer transition-colors",
260
+ isDragOver ? "border-primary bg-primary/5" : "border-muted-foreground/25",
261
+ disabled && "cursor-not-allowed opacity-50"
262
+ )}
263
+ onDrop={handleDrop}
264
+ onDragOver={handleDragOver}
265
+ onDragLeave={handleDragLeave}
266
+ onClick={handleClick}
267
+ >
268
+ <Upload className="h-10 w-10 mx-auto mb-4 text-muted-foreground" />
269
+ <p className="text-sm text-muted-foreground mb-2">
270
+ Drag and drop files here, or click to select
271
+ </p>
272
+ <p className="text-xs text-muted-foreground">
273
+ {accept === '*' ? 'Any file type' : `Accepted types: ${accept}`}
274
+ </p>
275
+ </div>
276
+
277
+ {/* File Input */}
278
+ <input
279
+ ref={fileInputRef}
280
+ type="file"
281
+ accept={accept}
282
+ multiple={multiple}
283
+ onChange={(e) => handleFileSelect(e.target.files)}
284
+ className="hidden"
285
+ disabled={disabled}
286
+ />
287
+
288
+ {/* File List */}
289
+ {files.length > 0 && (
290
+ <div className="space-y-2">
291
+ <h4 className="text-sm font-medium">Uploaded Files ({files.length})</h4>
292
+ {files.map((uploadedFile) => (
293
+ <div key={uploadedFile.id} className="flex items-center gap-3 p-3 border rounded-lg">
294
+ <div className="flex-shrink-0">
295
+ {getFileIcon(uploadedFile.file.type)}
296
+ </div>
297
+
298
+ <div className="flex-1 min-w-0">
299
+ <div className="flex items-center justify-between">
300
+ <p className="text-sm font-medium truncate">{uploadedFile.file.name}</p>
301
+ <Badge variant={uploadedFile.status === 'success' ? 'success' : uploadedFile.status === 'error' ? 'destructive' : 'secondary'}>
302
+ {uploadedFile.status === 'uploading' && <Loader2 className="h-3 w-3 mr-1 animate-spin" />}
303
+ {uploadedFile.status === 'success' && <CheckCircle2 className="h-3 w-3 mr-1" />}
304
+ {uploadedFile.status === 'error' && <AlertCircle className="h-3 w-3 mr-1" />}
305
+ {uploadedFile.status}
306
+ </Badge>
307
+ </div>
308
+
309
+ <div className="flex items-center justify-between mt-1">
310
+ <p className="text-xs text-muted-foreground">
311
+ {formatFileSize(uploadedFile.file.size)}
312
+ </p>
313
+ {uploadedFile.status === 'uploading' && (
314
+ <span className="text-xs text-muted-foreground">
315
+ {uploadedFile.progress}%
316
+ </span>
317
+ )}
318
+ </div>
319
+
320
+ {uploadedFile.status === 'uploading' && (
321
+ <Progress value={uploadedFile.progress} className="mt-2" />
322
+ )}
323
+
324
+ {uploadedFile.error && (
325
+ <p className="text-xs text-destructive mt-1">{uploadedFile.error}</p>
326
+ )}
327
+ </div>
328
+
329
+ <div className="flex items-center gap-1">
330
+ {uploadedFile.status === 'success' && showPreview && uploadedFile.file.type.startsWith('image/') && (
331
+ <Button variant="ghost" size="sm">
332
+ <Download className="h-4 w-4" />
333
+ </Button>
334
+ )}
335
+
336
+ <Button
337
+ variant="ghost"
338
+ size="sm"
339
+ onClick={() => handleRemove(uploadedFile)}
340
+ disabled={uploadedFile.status === 'uploading'}
341
+ >
342
+ <X className="h-4 w-4" />
343
+ </Button>
344
+ </div>
345
+ </div>
346
+ ))}
347
+ </div>
348
+ )}
349
+
350
+ {/* Upload Status */}
351
+ {isUploading && (
352
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
353
+ <Loader2 className="h-4 w-4 animate-spin" />
354
+ Uploading files...
355
+ </div>
356
+ )}
357
+ </CardContent>
358
+ </Card>
359
+ )
360
+ }
361
+
362
+ export default FileUpload
@@ -0,0 +1,209 @@
1
+ "use client"
2
+
3
+ import React, { useState } from "react"
4
+ import { motion, AnimatePresence } from "framer-motion"
5
+ import { Plus, X, Lock, Sparkles } from "lucide-react"
6
+ import { cn } from "../../lib/utils"
7
+ import { Card, CardContent } from "../ui/card"
8
+ import { Button } from "../ui/button"
9
+ import { useSubscription } from "../../hooks/use-subscription"
10
+
11
+ export interface FloatingActionButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
12
+ actions?: Array<{
13
+ icon: React.ReactNode
14
+ label: string
15
+ onClick: () => void
16
+ }>
17
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left"
18
+ size?: "sm" | "default" | "lg"
19
+ }
20
+
21
+ const FloatingActionButtonInternal = React.forwardRef<HTMLButtonElement, FloatingActionButtonProps>(
22
+ ({
23
+ actions = [],
24
+ position = "bottom-right",
25
+ size = "default",
26
+ className,
27
+ children,
28
+ onClick,
29
+ ...props
30
+ }, ref) => {
31
+ const [isOpen, setIsOpen] = useState(false)
32
+
33
+ const sizeClasses = {
34
+ sm: "h-10 w-10",
35
+ default: "h-12 w-12",
36
+ lg: "h-14 w-14"
37
+ }
38
+
39
+ const positionClasses = {
40
+ "bottom-right": "bottom-4 right-4",
41
+ "bottom-left": "bottom-4 left-4",
42
+ "top-right": "top-4 right-4",
43
+ "top-left": "top-4 left-4"
44
+ }
45
+
46
+ const actionPositions = {
47
+ "bottom-right": { x: -60, y: 0 },
48
+ "bottom-left": { x: 60, y: 0 },
49
+ "top-right": { x: -60, y: 0 },
50
+ "top-left": { x: 60, y: 0 }
51
+ }
52
+
53
+ const handleMainClick = (e: React.MouseEvent<HTMLButtonElement>) => {
54
+ if (actions.length > 0) {
55
+ setIsOpen(!isOpen)
56
+ } else if (onClick) {
57
+ onClick(e)
58
+ }
59
+ }
60
+
61
+ return (
62
+ <div className={cn("fixed z-50", positionClasses[position])}>
63
+ {/* Action buttons */}
64
+ <AnimatePresence>
65
+ {isOpen && actions.length > 0 && (
66
+ <div className="absolute">
67
+ {actions.map((action, index) => (
68
+ <motion.div
69
+ key={index}
70
+ initial={{
71
+ scale: 0,
72
+ opacity: 0,
73
+ x: 0,
74
+ y: 0
75
+ }}
76
+ animate={{
77
+ scale: 1,
78
+ opacity: 1,
79
+ x: actionPositions[position].x,
80
+ y: actionPositions[position].y * (index + 1) - 10
81
+ }}
82
+ exit={{
83
+ scale: 0,
84
+ opacity: 0,
85
+ x: 0,
86
+ y: 0
87
+ }}
88
+ transition={{
89
+ delay: index * 0.05,
90
+ type: "spring",
91
+ stiffness: 200,
92
+ damping: 15
93
+ }}
94
+ className="absolute flex items-center gap-2"
95
+ style={{
96
+ transform: `translate(${position.includes('right') ? '0' : '0'}, ${position.includes('bottom') ? '0' : '0'})`
97
+ }}
98
+ >
99
+ {/* Label */}
100
+ <motion.div
101
+ initial={{ opacity: 0, scale: 0.8 }}
102
+ animate={{ opacity: 1, scale: 1 }}
103
+ exit={{ opacity: 0, scale: 0.8 }}
104
+ transition={{ delay: (index + 1) * 0.05 }}
105
+ className={cn(
106
+ "px-2 py-1 bg-background/90 backdrop-blur-sm text-sm font-medium rounded-md shadow-sm border whitespace-nowrap",
107
+ position.includes('right') ? 'mr-2' : 'ml-2'
108
+ )}
109
+ >
110
+ {action.label}
111
+ </motion.div>
112
+
113
+ {/* Action button */}
114
+ <button
115
+ onClick={() => {
116
+ action.onClick()
117
+ setIsOpen(false)
118
+ }}
119
+ className={cn(
120
+ "inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
121
+ sizeClasses[size]
122
+ )}
123
+ >
124
+ {action.icon}
125
+ </button>
126
+ </motion.div>
127
+ ))}
128
+ </div>
129
+ )}
130
+ </AnimatePresence>
131
+
132
+ {/* Main FAB */}
133
+ <motion.button
134
+ ref={ref}
135
+ className={cn(
136
+ "inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
137
+ sizeClasses[size],
138
+ className
139
+ )}
140
+ onClick={handleMainClick}
141
+ whileHover={{ scale: 1.1 }}
142
+ whileTap={{ scale: 0.9 }}
143
+ animate={{ rotate: isOpen ? 45 : 0 }}
144
+ transition={{ duration: 0.2 }}
145
+ {...props}
146
+ >
147
+ {children || (actions.length > 0 ? <Plus className="h-5 w-5" /> : <Plus className="h-5 w-5" />)}
148
+ </motion.button>
149
+
150
+ {/* Backdrop */}
151
+ <AnimatePresence>
152
+ {isOpen && (
153
+ <motion.div
154
+ initial={{ opacity: 0 }}
155
+ animate={{ opacity: 1 }}
156
+ exit={{ opacity: 0 }}
157
+ className="fixed inset-0 z-[-1]"
158
+ onClick={() => setIsOpen(false)}
159
+ />
160
+ )}
161
+ </AnimatePresence>
162
+ </div>
163
+ )
164
+ }
165
+ )
166
+
167
+ FloatingActionButtonInternal.displayName = "FloatingActionButtonInternal"
168
+
169
+ export const FloatingActionButton = React.forwardRef<HTMLButtonElement, FloatingActionButtonProps>(
170
+ ({ className, ...props }, ref) => {
171
+ // Check if we're in docs mode or have pro access
172
+ const docsProAccess = { hasAccess: true } // Pro access assumed in package
173
+ const { hasProAccess, isLoading } = useSubscription()
174
+
175
+ // In docs mode, always show the component
176
+ const canShowComponent = docsProAccess.isDocsMode || hasProAccess
177
+
178
+ // If not in docs mode and no pro access, show upgrade prompt
179
+ if (!docsProAccess.isDocsMode && !isLoading && !hasProAccess) {
180
+ return (
181
+ <Card className={cn("w-fit", className)}>
182
+ <CardContent className="py-6 text-center">
183
+ <div className="space-y-4">
184
+ <div className="rounded-full bg-purple-100 dark:bg-purple-900/30 p-3 w-fit mx-auto">
185
+ <Lock className="h-6 w-6 text-purple-600 dark:text-purple-400" />
186
+ </div>
187
+ <div>
188
+ <h3 className="font-semibold text-sm mb-2">Pro Feature</h3>
189
+ <p className="text-muted-foreground text-xs mb-4">
190
+ Floating Action Button is available exclusively to MoonUI Pro subscribers.
191
+ </p>
192
+ <a href="/pricing">
193
+ <Button size="sm">
194
+ <Sparkles className="mr-2 h-4 w-4" />
195
+ Upgrade to Pro
196
+ </Button>
197
+ </a>
198
+ </div>
199
+ </div>
200
+ </CardContent>
201
+ </Card>
202
+ )
203
+ }
204
+
205
+ return <FloatingActionButtonInternal className={className} ref={ref} {...props} />
206
+ }
207
+ )
208
+
209
+ FloatingActionButton.displayName = "FloatingActionButton"