@ghatak/slash-ui 1.0.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 (66) hide show
  1. package/README.md +36 -0
  2. package/__registry__/index.ts +493 -0
  3. package/app/(auth)/layout.tsx +18 -0
  4. package/app/(auth)/login/page.tsx +152 -0
  5. package/app/(protected)/component/[id]/page.tsx +48 -0
  6. package/app/(protected)/component/page.tsx +151 -0
  7. package/app/(protected)/docs/page.tsx +222 -0
  8. package/app/account/page.tsx +109 -0
  9. package/app/api/me/route.ts +24 -0
  10. package/app/globals.css +68 -0
  11. package/app/icon.png +0 -0
  12. package/app/layout.tsx +43 -0
  13. package/app/page.tsx +22 -0
  14. package/app/pricing/page.tsx +12 -0
  15. package/bin/intex.ts +19 -0
  16. package/components/smooth-scroll.tsx +26 -0
  17. package/components/toast.tsx +101 -0
  18. package/components/ui/IndustryProof.tsx +159 -0
  19. package/components/ui/ShowcaseContainer.tsx +497 -0
  20. package/components/ui/dot-cursor.tsx +108 -0
  21. package/components/ui/featuredComponents.tsx +126 -0
  22. package/components/ui/footer.tsx +59 -0
  23. package/components/ui/hero.tsx +85 -0
  24. package/components/ui/navbar.tsx +337 -0
  25. package/components/ui/pricing.tsx +163 -0
  26. package/eslint.config.mjs +18 -0
  27. package/hooks/use-component-search.tsx +52 -0
  28. package/lib/actions/auth.action.ts +88 -0
  29. package/lib/auth.ts +18 -0
  30. package/lib/email.ts +46 -0
  31. package/lib/prisma.ts +14 -0
  32. package/lib/registry.ts +17 -0
  33. package/lib/utils.ts +6 -0
  34. package/middleware/middleware.ts +21 -0
  35. package/next.config.ts +7 -0
  36. package/package.json +61 -0
  37. package/postcss.config.mjs +7 -0
  38. package/prisma/migrations/20260303172729_init/migration.sql +21 -0
  39. package/prisma/migrations/migration_lock.toml +3 -0
  40. package/prisma/schema.prisma +22 -0
  41. package/prisma.config.ts +14 -0
  42. package/public/compVideos/neubrutal-button.mp4 +0 -0
  43. package/public/fonts/BeVietnamPro-ExtraBold.otf +0 -0
  44. package/public/fonts/CartographCF-Regular.ttf +0 -0
  45. package/public/fonts/Hoshiko-Satsuki.ttf +0 -0
  46. package/public/fonts/Switzer-Regular.otf +0 -0
  47. package/public/images/PricingSlash.svg +58 -0
  48. package/public/images/slash_1.svg +59 -0
  49. package/public/images/slash_2.svg +18 -0
  50. package/public/video/hero_video.mp4 +0 -0
  51. package/registry/details/buttons/neubrutal-button-details.tsx +146 -0
  52. package/registry/details/cursor/dot-cursor-details.tsx +11 -0
  53. package/registry/details/navbar/floating-navbar-details.tsx +11 -0
  54. package/registry/details/scrollbars/minimal-scrollbar-details.tsx +0 -0
  55. package/registry/index.ts +35 -0
  56. package/registry/ui/buttons/neubrutal-button.tsx +33 -0
  57. package/registry/ui/cursors/dot-cursor.tsx +108 -0
  58. package/registry/ui/navbars/floating-navbar.tsx +99 -0
  59. package/registry/ui/scrollbars/minimal-scrollbar.tsx +203 -0
  60. package/scripts/build-registry.ts +60 -0
  61. package/src/commands/add.ts +40 -0
  62. package/src/commands/init.ts +75 -0
  63. package/src/commands/list.ts +44 -0
  64. package/src/index.ts +35 -0
  65. package/src/utils/get-pkg-manager.ts +7 -0
  66. package/tsconfig.json +34 -0
@@ -0,0 +1,497 @@
1
+ 'use client';
2
+
3
+ import { atomDark as theme } from 'react-syntax-highlighter/dist/esm/styles/prism';
4
+ import ReactMarkdown from 'react-markdown';
5
+ import remarkGfm from 'remark-gfm';
6
+ import React, { useState, Suspense, useEffect } from 'react';
7
+ import Link from 'next/link';
8
+ import { useParams } from 'next/navigation';
9
+ import {
10
+ Code2,
11
+ Command,
12
+ PanelLeft,
13
+ Info,
14
+ Check,
15
+ X,
16
+ Search,
17
+ Box,
18
+ Copy,
19
+ Loader2,
20
+ Maximize,
21
+ Home,
22
+ LoaderCircle,
23
+ MousePointer2,
24
+ Rocket,
25
+ } from 'lucide-react';
26
+ import { AnimatePresence, motion } from 'framer-motion';
27
+ import { Index } from '@/__registry__';
28
+ import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
29
+ import { getComponentSource } from '@/lib/registry';
30
+
31
+ export default function ShowcaseContainer({
32
+ children,
33
+ title,
34
+ code: propsCode,
35
+ description: propsDescription,
36
+ install: propsInstall,
37
+ }: {
38
+ children: React.ReactNode;
39
+ title: string;
40
+ code?: string;
41
+ description?: string;
42
+ install?: string;
43
+ }) {
44
+ const [isSidebarOpen, setSidebarOpen] = useState(false);
45
+ const [activePanel, setActivePanel] = useState<'code' | 'info' | null>(null);
46
+ const [copied, setCopied] = useState(false);
47
+ const [searchQuery, setSearchQuery] = useState('');
48
+ const { id } = useParams();
49
+ const [sourceCode, setSourceCode] = useState<string | null>(null);
50
+ const [isLoadingCode, setIsLoadingCode] = useState(false);
51
+ const [isFullscreen, setIsFullscreen] = useState(false);
52
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
53
+
54
+ const componentsList = Object.values(Index['default']);
55
+ const activeItem = Index['default'][id as string] as any;
56
+
57
+ // Sync state if user exits via ESC or uses Cmd+K
58
+ useEffect(() => {
59
+ const handleKeyDown = (e: KeyboardEvent) => {
60
+ if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
61
+ e.preventDefault();
62
+ setIsSearchOpen((prev) => !prev);
63
+ }
64
+ if (e.key === 'Escape') {
65
+ setIsSearchOpen(false);
66
+ setActivePanel(null);
67
+ }
68
+ };
69
+
70
+ const handleFullscreenChange = () => {
71
+ setIsFullscreen(!!document.fullscreenElement);
72
+ };
73
+
74
+ document.addEventListener('keydown', handleKeyDown);
75
+ document.addEventListener('fullscreenchange', handleFullscreenChange);
76
+ return () => {
77
+ document.removeEventListener('keydown', handleKeyDown);
78
+ document.removeEventListener('fullscreenchange', handleFullscreenChange);
79
+ };
80
+ }, []);
81
+
82
+ useEffect(() => {
83
+ async function fetchSource() {
84
+ if (activePanel === 'code' && activeItem?.files) {
85
+ setIsLoadingCode(true);
86
+ const code = await getComponentSource(activeItem.files);
87
+ setSourceCode(code ?? '// Error: Source code not found.');
88
+ setIsLoadingCode(false);
89
+ }
90
+ }
91
+ fetchSource();
92
+ }, [activePanel, activeItem, id]);
93
+
94
+ const getContainerStyle = () => {
95
+ switch (id) {
96
+ case 'neubrutal-button':
97
+ return 'bg-[#538F37]';
98
+ case 'dot-cursor':
99
+ case 'flaoting-navbar':
100
+ return '';
101
+ default:
102
+ return 'bg-[#0a0908]';
103
+ }
104
+ };
105
+
106
+ const toggleFullscreen = () => {
107
+ if (!document.fullscreenElement) {
108
+ document.documentElement
109
+ .requestFullscreen()
110
+ .then(() => setIsFullscreen(true));
111
+ } else {
112
+ document.exitFullscreen().then(() => setIsFullscreen(false));
113
+ }
114
+ };
115
+
116
+ const copyToClipboard = async (text: string) => {
117
+ await navigator.clipboard.writeText(text);
118
+ setCopied(true);
119
+ setTimeout(() => setCopied(false), 2000);
120
+ };
121
+
122
+ const dynamicCode =
123
+ propsCode ||
124
+ sourceCode ||
125
+ activeItem?.content ||
126
+ '// No source code found.';
127
+ const dynamicDescription =
128
+ propsDescription ||
129
+ activeItem?.description ||
130
+ `Premium ${title} component.`;
131
+ const dynamicInstall =
132
+ propsInstall ||
133
+ activeItem?.install ||
134
+ 'npm install framer-motion lucide-react';
135
+
136
+ // Filter logic for the search modal
137
+ const filteredComponents = componentsList.filter((comp: any) =>
138
+ comp.name.toLowerCase().includes(searchQuery.toLowerCase()),
139
+ );
140
+
141
+ const staticSearchItems = [
142
+ { icon: <Home size={16} />, label: 'Home', category: 'Pages', path: '/' },
143
+ {
144
+ icon: <LoaderCircle size={16} />,
145
+ label: 'Loader',
146
+ category: 'Pages',
147
+ path: '/loader',
148
+ },
149
+ {
150
+ icon: <MousePointer2 size={16} />,
151
+ label: 'Cursor',
152
+ category: 'Pages',
153
+ path: '/cursor',
154
+ },
155
+ {
156
+ icon: <Box size={16} />,
157
+ label: 'All Components',
158
+ category: 'Pages',
159
+ path: '/component',
160
+ },
161
+ {
162
+ icon: <Rocket size={16} />,
163
+ label: 'Quick Start',
164
+ category: 'Get Started',
165
+ path: '/docs',
166
+ },
167
+ ];
168
+
169
+ return (
170
+ <div className='h-screen w-screen bg-black p-2 overflow-hidden text-white font-sans'>
171
+ <div className='h-full w-full rounded-[30px] border border-white/5 overflow-hidden flex relative bg-[#0A0A0A]'>
172
+ {/* LEFT NAV SIDEBAR */}
173
+ <aside
174
+ className={`absolute top-0 left-0 z-[150] h-full w-[320px] transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] bg-[#0A0A0A]/80 backdrop-blur-2xl border-r border-white/5 ${isSidebarOpen ? 'translate-x-0 opacity-100' : '-translate-x-full opacity-0 pointer-events-none'}`}
175
+ >
176
+ <div className='flex flex-col h-full p-10'>
177
+ <div className='flex items-center justify-between mb-12'>
178
+ <div className='text-sm text-zinc-500'>Navigation</div>
179
+ <button
180
+ onClick={() => setSidebarOpen(false)}
181
+ className='p-2 text-zinc-500 hover:text-white transition-colors cursor-pointer'
182
+ >
183
+ <X size={16} />
184
+ </button>
185
+ </div>
186
+ <nav className='flex-1 overflow-y-auto custom-scrollbar space-y-2'>
187
+ {componentsList.map((comp: any, index: number) => (
188
+ <Link
189
+ key={comp.name}
190
+ href={`/component/${comp.name}`}
191
+ onClick={() => setSidebarOpen(false)}
192
+ className='group flex items-center gap-4 py-2'
193
+ >
194
+ <div
195
+ className={`h-px transition-all duration-300 ${id === comp.name ? 'w-6 bg-white' : 'w-3 bg-zinc-800 group-hover:bg-zinc-500'}`}
196
+ />
197
+ <span
198
+ className={`text-[13px] transition-colors ${id === comp.name ? 'text-white font-medium' : 'text-zinc-500 group-hover:text-zinc-300'}`}
199
+ >
200
+ <span className='font-mono mr-2 opacity-30'>
201
+ {String(index + 1).padStart(2, '0')}
202
+ </span>
203
+ {comp.name}
204
+ </span>
205
+ </Link>
206
+ ))}
207
+ </nav>
208
+ </div>
209
+ </aside>
210
+
211
+ <AnimatePresence>
212
+ {isSidebarOpen && (
213
+ <motion.div
214
+ initial={{ opacity: 0 }}
215
+ animate={{ opacity: 1 }}
216
+ exit={{ opacity: 0 }}
217
+ onClick={() => setSidebarOpen(false)}
218
+ className='absolute inset-0 bg-black/40 z-[140] backdrop-blur-sm cursor-pointer'
219
+ />
220
+ )}
221
+ </AnimatePresence>
222
+
223
+ <div className='flex w-full h-full relative'>
224
+ <main
225
+ className={`flex-1 relative flex flex-col items-center justify-center transition-all duration-700 ${getContainerStyle()} ease-in-out ${activePanel || isSearchOpen ? 'scale-[0.9] opacity-50' : 'scale-100 opacity-100'}`}
226
+ >
227
+ {!isFullscreen && (
228
+ <>
229
+ <button
230
+ onClick={(e) => {
231
+ e.stopPropagation();
232
+ setSidebarOpen(true);
233
+ }}
234
+ className='absolute top-8 left-8 z-[110] p-3 bg-[#161616]/80 backdrop-blur-md border border-white/10 rounded-2xl text-zinc-400 hover:text-white transition-all cursor-pointer shadow-xl'
235
+ >
236
+ <PanelLeft size={20} />
237
+ </button>
238
+ <div className='absolute top-10 right-10 text-right pointer-events-none'>
239
+ <h2 className='text-2xl text-white font-black uppercase tracking-tighter'>
240
+ {title}
241
+ </h2>
242
+ </div>
243
+ </>
244
+ )}
245
+
246
+ <div className='transition-transform duration-700'>{children}</div>
247
+
248
+ {/* FLOATING TOOLBAR */}
249
+ <div className='absolute bottom-10 left-1/2 -translate-x-1/2 flex items-center gap-1 bg-[#161616]/90 border border-white/10 p-1.5 rounded-2xl shadow-2xl backdrop-blur-xl z-[120]'>
250
+ <button
251
+ onClick={toggleFullscreen}
252
+ className='p-2.5 rounded-xl cursor-pointer text-zinc-500 hover:text-white hover:bg-white/5 transition-all'
253
+ >
254
+ <Maximize size={18} />
255
+ </button>
256
+ <button
257
+ onClick={() =>
258
+ setActivePanel(activePanel === 'info' ? null : 'info')
259
+ }
260
+ className={`p-2.5 rounded-xl cursor-pointer transition-all ${activePanel === 'info' ? 'bg-white text-black' : 'text-zinc-500 hover:text-white hover:bg-white/5'}`}
261
+ >
262
+ <Info size={18} />
263
+ </button>
264
+ <div className='w-px h-4 bg-white/10 mx-1' />
265
+ <button
266
+ onClick={() =>
267
+ setActivePanel(activePanel === 'code' ? null : 'code')
268
+ }
269
+ className={`p-2.5 rounded-xl transition-all cursor-pointer ${activePanel === 'code' ? 'bg-white text-black' : 'text-zinc-500 hover:text-white hover:bg-white/5'}`}
270
+ >
271
+ <Code2 size={18} />
272
+ </button>
273
+ <button
274
+ onClick={() => setIsSearchOpen(true)}
275
+ className={`p-2.5 rounded-xl transition-all cursor-pointer text-zinc-500 hover:text-white hover:bg-white/5`}
276
+ >
277
+ <Command size={18} />
278
+ </button>
279
+ </div>
280
+ </main>
281
+
282
+ {/* RIGHT SIDE PANEL (Code/Info) */}
283
+ <aside
284
+ className={`h-full bg-[#080808] border-l border-white/5 transition-all duration-500 ease-[cubic-bezier(0.23,1,0.32,1)] overflow-hidden relative z-[130] ${activePanel ? 'w-[45%] opacity-100' : 'w-0 opacity-0'}`}
285
+ >
286
+ <div className='p-12 h-full flex flex-col min-w-[450px]'>
287
+ <div className='flex items-center justify-between mb-12'>
288
+ <h3 className='text-sm uppercase font-beVietnamPro text-zinc-500'>
289
+ {activePanel === 'code' ? 'Source Code' : 'Documentation'}
290
+ </h3>
291
+ <button
292
+ onClick={() => setActivePanel(null)}
293
+ className='p-2 hover:bg-white/5 rounded-lg text-zinc-500 hover:text-white cursor-pointer transition-colors'
294
+ >
295
+ <X size={18} />
296
+ </button>
297
+ </div>
298
+
299
+ <div className='flex-1 overflow-y-auto custom-scrollbar pr-4'>
300
+ <AnimatePresence mode='wait'>
301
+ {activePanel === 'code' ? (
302
+ <motion.div
303
+ key='code'
304
+ initial={{ opacity: 0, x: 20 }}
305
+ animate={{ opacity: 1, x: 0 }}
306
+ exit={{ opacity: 0, x: 20 }}
307
+ className='relative rounded-2xl bg-black border border-white/10 overflow-hidden'
308
+ >
309
+ {isLoadingCode ? (
310
+ <div className='flex flex-col items-center justify-center py-32 text-zinc-600 gap-4'>
311
+ <Loader2 className='animate-spin' size={24} />
312
+ <span className='text-[10px] uppercase tracking-widest font-bold'>
313
+ Fetching Code
314
+ </span>
315
+ </div>
316
+ ) : (
317
+ <>
318
+ <button
319
+ onClick={() => copyToClipboard(dynamicCode)}
320
+ className='absolute top-4 right-4 p-2 bg-white/5 rounded-md text-zinc-400 cursor-pointer z-10 hover:bg-white/10'
321
+ >
322
+ {copied ? (
323
+ <Check size={14} className='text-white' />
324
+ ) : (
325
+ <Copy size={14} />
326
+ )}
327
+ </button>
328
+ <SyntaxHighlighter
329
+ language='tsx'
330
+ style={theme}
331
+ customStyle={{
332
+ margin: 0,
333
+ padding: '32px',
334
+ fontSize: '13px',
335
+ background: 'transparent',
336
+ lineHeight: '1.7',
337
+ }}
338
+ >
339
+ {dynamicCode}
340
+ </SyntaxHighlighter>
341
+ </>
342
+ )}
343
+ </motion.div>
344
+ ) : activePanel === 'info' ? (
345
+ <motion.div
346
+ key='info'
347
+ initial={{ opacity: 0, x: 20 }}
348
+ animate={{ opacity: 1, x: 0 }}
349
+ exit={{ opacity: 0, x: 20 }}
350
+ >
351
+ <section className='space-y-12'>
352
+ <div>
353
+ <h4 className='text-[10px] font-black uppercase tracking-[0.4em] text-zinc-600 mb-6'>
354
+ Description
355
+ </h4>
356
+ <div className='prose prose-invert prose-sm text-zinc-400'>
357
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>
358
+ {dynamicDescription}
359
+ </ReactMarkdown>
360
+ </div>
361
+ </div>
362
+ <div>
363
+ <h4 className='text-[10px] font-black uppercase tracking-[0.4em] text-zinc-600 mb-6'>
364
+ Installation
365
+ </h4>
366
+ <div className='bg-black p-6 rounded-2xl border border-white/10 font-mono text-xs flex items-center justify-between'>
367
+ <span className='text-zinc-300'>
368
+ {dynamicInstall}
369
+ </span>
370
+ <button
371
+ onClick={() => copyToClipboard(dynamicInstall)}
372
+ className='p-2 hover:bg-white/5 rounded-md text-zinc-500'
373
+ >
374
+ {copied ? (
375
+ <Check size={14} />
376
+ ) : (
377
+ <Copy size={14} />
378
+ )}
379
+ </button>
380
+ </div>
381
+ </div>
382
+ </section>
383
+ </motion.div>
384
+ ) : null}
385
+ </AnimatePresence>
386
+ </div>
387
+ </div>
388
+ </aside>
389
+ </div>
390
+
391
+ {/* SEARCH OVERLAY */}
392
+ <AnimatePresence>
393
+ {isSearchOpen && (
394
+ <div className='fixed inset-0 z-[200] flex items-start justify-center pt-[18vh] px-4'>
395
+ <motion.div
396
+ initial={{ opacity: 0 }}
397
+ animate={{ opacity: 1 }}
398
+ exit={{ opacity: 0 }}
399
+ onClick={() => setIsSearchOpen(false)}
400
+ className='absolute inset-0 bg-black/60 backdrop-blur-md'
401
+ />
402
+
403
+ <motion.div
404
+ initial={{ opacity: 0, scale: 0.95, y: -20 }}
405
+ animate={{ opacity: 1, scale: 1, y: 0 }}
406
+ exit={{ opacity: 0, scale: 0.95, y: -20 }}
407
+ className='relative w-full max-w-[600px] bg-zinc-950 border border-zinc-800 rounded-xl shadow-2xl overflow-hidden'
408
+ >
409
+ <div className='flex items-center px-4 border-b border-zinc-800'>
410
+ <Search className='text-zinc-500' size={18} />
411
+ <input
412
+ autoFocus
413
+ value={searchQuery}
414
+ onChange={(e) => setSearchQuery(e.target.value)}
415
+ placeholder='Search components or pages...'
416
+ className='w-full h-14 bg-transparent border-none outline-none px-4 text-white text-sm placeholder:text-zinc-600'
417
+ />
418
+ </div>
419
+
420
+ <div className='max-h-[400px] overflow-y-auto p-2 custom-scrollbar'>
421
+ {searchQuery.length > 0 ? (
422
+ <div className='p-2'>
423
+ <p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
424
+ Component Results
425
+ </p>
426
+ {filteredComponents.length > 0 ? (
427
+ filteredComponents.map((comp: any) => (
428
+ <Link
429
+ key={comp.name}
430
+ href={`/component/${comp.name}`}
431
+ onClick={() => {
432
+ setIsSearchOpen(false);
433
+ setSearchQuery('');
434
+ }}
435
+ className='flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
436
+ >
437
+ <Box
438
+ size={16}
439
+ className='text-zinc-500 group-hover:text-white'
440
+ />
441
+ <span className='text-sm text-zinc-300 group-hover:text-white'>
442
+ {comp.name}
443
+ </span>
444
+ </Link>
445
+ ))
446
+ ) : (
447
+ <p className='px-3 py-4 text-sm text-zinc-600'>
448
+ No components found...
449
+ </p>
450
+ )}
451
+ </div>
452
+ ) : (
453
+ ['Pages', 'Get Started'].map((category) => (
454
+ <div key={category} className='mb-2'>
455
+ <p className='px-3 py-2 text-[10px] font-semibold text-zinc-500 uppercase tracking-wider'>
456
+ {category}
457
+ </p>
458
+ {staticSearchItems
459
+ .filter((item) => item.category === category)
460
+ .map((item) => (
461
+ <Link
462
+ key={item.label}
463
+ href={item.path}
464
+ onClick={() => setIsSearchOpen(false)}
465
+ className='flex items-center gap-3 px-3 py-2.5 rounded-lg hover:bg-white/5 transition-all group'
466
+ >
467
+ <div className='text-zinc-500 group-hover:text-white'>
468
+ {item.icon}
469
+ </div>
470
+ <span className='text-sm text-zinc-300 group-hover:text-white'>
471
+ {item.label}
472
+ </span>
473
+ </Link>
474
+ ))}
475
+ </div>
476
+ ))
477
+ )}
478
+ </div>
479
+ <div className='px-4 py-3 border-t border-zinc-800 bg-zinc-900/30 flex justify-between items-center text-[10px] text-zinc-500 font-medium'>
480
+ <div className='flex gap-3'>
481
+ <span className='flex items-center gap-1'>
482
+ <Command size={10} /> to select
483
+ </span>
484
+ <span className='flex items-center gap-1'>
485
+ Enter to open
486
+ </span>
487
+ </div>
488
+ <span>ESC to close</span>
489
+ </div>
490
+ </motion.div>
491
+ </div>
492
+ )}
493
+ </AnimatePresence>
494
+ </div>
495
+ </div>
496
+ );
497
+ }
@@ -0,0 +1,108 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect } from 'react';
4
+
5
+ const CustomCursor = () => {
6
+ const [position, setPosition] = useState({ x: 0, y: 0 });
7
+ const [isHovering, setIsHovering] = useState(false);
8
+ const [isDarkBackground, setIsDarkBackground] = useState(false);
9
+
10
+ useEffect(() => {
11
+
12
+ // document.body.style.cursor = 'none';
13
+
14
+ const updatePosition = (e: MouseEvent) => {
15
+ setPosition({ x: e.clientX, y: e.clientY });
16
+
17
+ const element = document.elementFromPoint(e.clientX, e.clientY);
18
+ if (element) {
19
+ const bgColor = window.getComputedStyle(element).backgroundColor;
20
+ const brightness = getBrightness(bgColor);
21
+ setIsDarkBackground(brightness < 128);
22
+ }
23
+ };
24
+
25
+ const handleMouseOver = (e: MouseEvent) => {
26
+ const target = e.target as HTMLElement;
27
+ if (
28
+ target.tagName === 'A' ||
29
+ target.tagName === 'BUTTON' ||
30
+ target.onclick !== null ||
31
+ window.getComputedStyle(target).cursor === 'pointer'
32
+ ) {
33
+ setIsHovering(true);
34
+ }
35
+ };
36
+
37
+ const handleMouseOut = () => {
38
+ setIsHovering(false);
39
+ };
40
+
41
+ window.addEventListener('mousemove', updatePosition);
42
+ document.addEventListener('mouseover', handleMouseOver);
43
+ document.addEventListener('mouseout', handleMouseOut);
44
+
45
+ return () => {
46
+ window.removeEventListener('mousemove', updatePosition);
47
+ document.removeEventListener('mouseover', handleMouseOver);
48
+ document.removeEventListener('mouseout', handleMouseOut);
49
+
50
+
51
+ };
52
+ }, []);
53
+
54
+ const getBrightness = (color: string) => {
55
+ const rgb = color.match(/d+/g);
56
+ if (!rgb) return 255;
57
+ const r = parseInt(rgb[0]);
58
+ const g = parseInt(rgb[1]);
59
+ const b = parseInt(rgb[2]);
60
+ return (r * 299 + g * 587 + b * 114) / 1000;
61
+ };
62
+
63
+ return (
64
+ <div
65
+ style={{
66
+ cursor: 'none',
67
+ width: '100vw',
68
+ height: '100vh',
69
+ position: 'fixed',
70
+ top: 0,
71
+ left: 0,
72
+ pointerEvents: 'none'
73
+ }}>
74
+
75
+ <div
76
+ style={{
77
+ position: 'fixed',
78
+ left: `${position.x}px`,
79
+ top: `${position.y}px`,
80
+ pointerEvents: 'none',
81
+ transform: `translate(-50%, -50%) scale(${isHovering ? 1.5 : 1})`,
82
+ transition: 'transform 0.2s ease',
83
+ zIndex: 9999,
84
+ }}
85
+ >
86
+ <svg
87
+ xmlns="http://www.w3.org/2000/svg"
88
+ width="24"
89
+ height="24"
90
+ viewBox="0 0 24 24"
91
+ style={{
92
+ display: 'block',
93
+ transition: 'all 0.2s ease',
94
+ }}
95
+ >
96
+ <path
97
+ fill={isDarkBackground ? '#ffffff' : '#000000'}
98
+ stroke={isDarkBackground ? '#000000' : '#ffffff'}
99
+ strokeWidth="2"
100
+ d="M5.5 3.21V20.8c0 .45.54.67.85.35l4.86-4.86a.5.5 0 0 1 .35-.15h6.87a.5.5 0 0 0 .35-.85L6.35 2.85a.5.5 0 0 0-.85.35Z"
101
+ />
102
+ </svg>
103
+ </div>
104
+ </div>
105
+ );
106
+ };
107
+
108
+ export default CustomCursor;