@opencosmos/ui 1.3.1

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 (260) hide show
  1. package/.claude/CLAUDE.md +239 -0
  2. package/README.md +161 -0
  3. package/dist/cli.mjs +151 -0
  4. package/dist/dates.d.mts +20 -0
  5. package/dist/dates.d.ts +20 -0
  6. package/dist/dates.js +240 -0
  7. package/dist/dates.js.map +1 -0
  8. package/dist/dates.mjs +203 -0
  9. package/dist/dates.mjs.map +1 -0
  10. package/dist/dnd.d.mts +126 -0
  11. package/dist/dnd.d.ts +126 -0
  12. package/dist/dnd.js +274 -0
  13. package/dist/dnd.js.map +1 -0
  14. package/dist/dnd.mjs +250 -0
  15. package/dist/dnd.mjs.map +1 -0
  16. package/dist/fontThemes-Dh8mtXES.d.mts +868 -0
  17. package/dist/fontThemes-Dh8mtXES.d.ts +868 -0
  18. package/dist/forms.d.mts +38 -0
  19. package/dist/forms.d.ts +38 -0
  20. package/dist/forms.js +198 -0
  21. package/dist/forms.js.map +1 -0
  22. package/dist/forms.mjs +159 -0
  23. package/dist/forms.mjs.map +1 -0
  24. package/dist/hooks-1b8WaQf1.d.mts +225 -0
  25. package/dist/hooks-CKW8vE9H.d.ts +225 -0
  26. package/dist/hooks.d.mts +3 -0
  27. package/dist/hooks.d.ts +3 -0
  28. package/dist/hooks.js +971 -0
  29. package/dist/hooks.js.map +1 -0
  30. package/dist/hooks.mjs +943 -0
  31. package/dist/hooks.mjs.map +1 -0
  32. package/dist/index-DscTIrZ2.d.mts +29 -0
  33. package/dist/index-DscTIrZ2.d.ts +29 -0
  34. package/dist/index.d.mts +3382 -0
  35. package/dist/index.d.ts +3382 -0
  36. package/dist/index.js +15146 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/index.mjs +14802 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/providers-CXPDMsl7.d.mts +30 -0
  41. package/dist/providers-Dn_Msjvz.d.ts +30 -0
  42. package/dist/providers.d.mts +3 -0
  43. package/dist/providers.d.ts +3 -0
  44. package/dist/providers.js +1885 -0
  45. package/dist/providers.js.map +1 -0
  46. package/dist/providers.mjs +1859 -0
  47. package/dist/providers.mjs.map +1 -0
  48. package/dist/tables.d.mts +10 -0
  49. package/dist/tables.d.ts +10 -0
  50. package/dist/tables.js +248 -0
  51. package/dist/tables.js.map +1 -0
  52. package/dist/tables.mjs +218 -0
  53. package/dist/tables.mjs.map +1 -0
  54. package/dist/tokens.d.mts +1065 -0
  55. package/dist/tokens.d.ts +1065 -0
  56. package/dist/tokens.js +2637 -0
  57. package/dist/tokens.js.map +1 -0
  58. package/dist/tokens.mjs +2555 -0
  59. package/dist/tokens.mjs.map +1 -0
  60. package/dist/utils-CIIM7dAC.d.ts +986 -0
  61. package/dist/utils-Cs04sxth.d.mts +986 -0
  62. package/dist/utils.d.mts +4 -0
  63. package/dist/utils.d.ts +4 -0
  64. package/dist/utils.js +874 -0
  65. package/dist/utils.js.map +1 -0
  66. package/dist/utils.mjs +806 -0
  67. package/dist/utils.mjs.map +1 -0
  68. package/dist/validation-Bj1ye-v_.d.mts +114 -0
  69. package/dist/validation-Bj1ye-v_.d.ts +114 -0
  70. package/dist/webgl.d.mts +104 -0
  71. package/dist/webgl.d.ts +104 -0
  72. package/dist/webgl.js +226 -0
  73. package/dist/webgl.js.map +1 -0
  74. package/dist/webgl.mjs +195 -0
  75. package/dist/webgl.mjs.map +1 -0
  76. package/package.json +267 -0
  77. package/src/cli.ts +206 -0
  78. package/src/component-registry.ts +183 -0
  79. package/src/components/actions/Button.test.tsx +61 -0
  80. package/src/components/actions/Button.tsx +70 -0
  81. package/src/components/actions/Link.tsx +78 -0
  82. package/src/components/actions/Magnetic.tsx +68 -0
  83. package/src/components/actions/Toggle.test.tsx +40 -0
  84. package/src/components/actions/Toggle.tsx +47 -0
  85. package/src/components/actions/ToggleGroup.tsx +70 -0
  86. package/src/components/actions/index.ts +5 -0
  87. package/src/components/backgrounds/FaultyTerminal.tsx +426 -0
  88. package/src/components/backgrounds/OrbBackground.tsx +424 -0
  89. package/src/components/backgrounds/WarpBackground.tsx +358 -0
  90. package/src/components/backgrounds/index.ts +3 -0
  91. package/src/components/blocks/Hero.tsx +142 -0
  92. package/src/components/blocks/social/OpenGraphCard.tsx +243 -0
  93. package/src/components/cursor/SplashCursor.tsx +1315 -0
  94. package/src/components/cursor/TargetCursor.tsx +187 -0
  95. package/src/components/cursor/index.ts +2 -0
  96. package/src/components/data-display/AspectImage.tsx +73 -0
  97. package/src/components/data-display/Avatar.test.tsx +35 -0
  98. package/src/components/data-display/Avatar.tsx +55 -0
  99. package/src/components/data-display/Badge.test.tsx +43 -0
  100. package/src/components/data-display/Badge.tsx +84 -0
  101. package/src/components/data-display/Brand.tsx +123 -0
  102. package/src/components/data-display/Calendar.tsx +70 -0
  103. package/src/components/data-display/Card.test.tsx +92 -0
  104. package/src/components/data-display/Card.tsx +115 -0
  105. package/src/components/data-display/Code.tsx +210 -0
  106. package/src/components/data-display/CollapsibleCodeBlock.tsx +238 -0
  107. package/src/components/data-display/DataTable.tsx +119 -0
  108. package/src/components/data-display/DescriptionList.tsx +41 -0
  109. package/src/components/data-display/GitHubIcon.tsx +44 -0
  110. package/src/components/data-display/Heading.test.tsx +36 -0
  111. package/src/components/data-display/Heading.tsx +83 -0
  112. package/src/components/data-display/StatCard.tsx +195 -0
  113. package/src/components/data-display/Table.tsx +133 -0
  114. package/src/components/data-display/Text.test.tsx +48 -0
  115. package/src/components/data-display/Text.tsx +144 -0
  116. package/src/components/data-display/Timeline.tsx +194 -0
  117. package/src/components/data-display/TreeView.tsx +226 -0
  118. package/src/components/data-display/Typewriter.tsx +119 -0
  119. package/src/components/data-display/VariableWeightText.tsx +130 -0
  120. package/src/components/data-display/index.ts +19 -0
  121. package/src/components/feedback/Alert.test.tsx +44 -0
  122. package/src/components/feedback/Alert.tsx +65 -0
  123. package/src/components/feedback/EmptyState.tsx +113 -0
  124. package/src/components/feedback/Progress.test.tsx +60 -0
  125. package/src/components/feedback/Progress.tsx +30 -0
  126. package/src/components/feedback/ProgressBar.tsx +158 -0
  127. package/src/components/feedback/Skeleton.test.tsx +39 -0
  128. package/src/components/feedback/Skeleton.tsx +45 -0
  129. package/src/components/feedback/Sonner.tsx +28 -0
  130. package/src/components/feedback/Spinner.test.tsx +33 -0
  131. package/src/components/feedback/Spinner.tsx +99 -0
  132. package/src/components/feedback/Stepper.tsx +307 -0
  133. package/src/components/feedback/Toast/Toast.tsx +243 -0
  134. package/src/components/feedback/Toast/index.ts +2 -0
  135. package/src/components/feedback/index.ts +9 -0
  136. package/src/components/forms/Checkbox.test.tsx +40 -0
  137. package/src/components/forms/Checkbox.tsx +31 -0
  138. package/src/components/forms/ColorPicker.tsx +118 -0
  139. package/src/components/forms/Combobox.tsx +96 -0
  140. package/src/components/forms/DragDrop.tsx +440 -0
  141. package/src/components/forms/FileUpload.tsx +252 -0
  142. package/src/components/forms/FilterButton.tsx +65 -0
  143. package/src/components/forms/Form.tsx +197 -0
  144. package/src/components/forms/Input.test.tsx +46 -0
  145. package/src/components/forms/Input.tsx +43 -0
  146. package/src/components/forms/InputOTP.tsx +81 -0
  147. package/src/components/forms/Label.test.tsx +20 -0
  148. package/src/components/forms/Label.tsx +25 -0
  149. package/src/components/forms/RadioGroup.tsx +51 -0
  150. package/src/components/forms/SearchBar.tsx +215 -0
  151. package/src/components/forms/Select.test.tsx +118 -0
  152. package/src/components/forms/Select.tsx +274 -0
  153. package/src/components/forms/Slider.tsx +29 -0
  154. package/src/components/forms/Switch.test.tsx +76 -0
  155. package/src/components/forms/Switch.tsx +30 -0
  156. package/src/components/forms/TextField.tsx +152 -0
  157. package/src/components/forms/Textarea.test.tsx +41 -0
  158. package/src/components/forms/Textarea.tsx +29 -0
  159. package/src/components/forms/ThemeSwitcher.tsx +290 -0
  160. package/src/components/forms/ThemeToggle.tsx +151 -0
  161. package/src/components/forms/index.ts +19 -0
  162. package/src/components/layout/Accordion.test.tsx +66 -0
  163. package/src/components/layout/Accordion.tsx +64 -0
  164. package/src/components/layout/AspectRatio.tsx +7 -0
  165. package/src/components/layout/Carousel.tsx +277 -0
  166. package/src/components/layout/Collapsible.test.tsx +40 -0
  167. package/src/components/layout/Collapsible.tsx +31 -0
  168. package/src/components/layout/Container.test.tsx +45 -0
  169. package/src/components/layout/Container.tsx +99 -0
  170. package/src/components/layout/CustomizerPanel.tsx +400 -0
  171. package/src/components/layout/DatePicker.tsx +57 -0
  172. package/src/components/layout/Footer/Footer.tsx +175 -0
  173. package/src/components/layout/Footer/index.ts +2 -0
  174. package/src/components/layout/GlassSurface.tsx +82 -0
  175. package/src/components/layout/Grid.test.tsx +31 -0
  176. package/src/components/layout/Grid.tsx +130 -0
  177. package/src/components/layout/Header/Header.tsx +450 -0
  178. package/src/components/layout/Header/index.ts +2 -0
  179. package/src/components/layout/PageLayout.tsx +180 -0
  180. package/src/components/layout/PageTemplate.tsx +158 -0
  181. package/src/components/layout/Resizable.tsx +48 -0
  182. package/src/components/layout/ScrollArea.tsx +53 -0
  183. package/src/components/layout/Separator.test.tsx +28 -0
  184. package/src/components/layout/Separator.tsx +29 -0
  185. package/src/components/layout/Sidebar.tsx +171 -0
  186. package/src/components/layout/Stack.test.tsx +41 -0
  187. package/src/components/layout/Stack.tsx +89 -0
  188. package/src/components/layout/glass-surface.css +60 -0
  189. package/src/components/layout/index.ts +18 -0
  190. package/src/components/motion/AnimatedBeam.tsx +159 -0
  191. package/src/components/navigation/Breadcrumb.test.tsx +57 -0
  192. package/src/components/navigation/Breadcrumb.tsx +119 -0
  193. package/src/components/navigation/Breadcrumbs.tsx +221 -0
  194. package/src/components/navigation/Command.tsx +159 -0
  195. package/src/components/navigation/Menubar.tsx +115 -0
  196. package/src/components/navigation/NavLink.tsx +55 -0
  197. package/src/components/navigation/NavigationMenu.tsx +125 -0
  198. package/src/components/navigation/Pagination.tsx +121 -0
  199. package/src/components/navigation/SecondaryNav.tsx +100 -0
  200. package/src/components/navigation/Tabs.test.tsx +47 -0
  201. package/src/components/navigation/Tabs.tsx +60 -0
  202. package/src/components/navigation/TertiaryNav.tsx +90 -0
  203. package/src/components/navigation/index.ts +10 -0
  204. package/src/components/overlays/AlertDialog.test.tsx +69 -0
  205. package/src/components/overlays/AlertDialog.tsx +166 -0
  206. package/src/components/overlays/ContextMenu.tsx +243 -0
  207. package/src/components/overlays/Dialog.test.tsx +79 -0
  208. package/src/components/overlays/Dialog.tsx +158 -0
  209. package/src/components/overlays/Drawer.tsx +128 -0
  210. package/src/components/overlays/Dropdown.tsx +253 -0
  211. package/src/components/overlays/DropdownMenu.tsx +242 -0
  212. package/src/components/overlays/HoverCard.tsx +32 -0
  213. package/src/components/overlays/Modal.tsx +250 -0
  214. package/src/components/overlays/NotificationCenter.tsx +364 -0
  215. package/src/components/overlays/Popover.test.tsx +40 -0
  216. package/src/components/overlays/Popover.tsx +46 -0
  217. package/src/components/overlays/Sheet.tsx +163 -0
  218. package/src/components/overlays/Tooltip.test.tsx +33 -0
  219. package/src/components/overlays/Tooltip.tsx +32 -0
  220. package/src/components/overlays/index.ts +12 -0
  221. package/src/dates.ts +2 -0
  222. package/src/dnd.ts +1 -0
  223. package/src/forms.ts +1 -0
  224. package/src/globals.css +187 -0
  225. package/src/hooks/index.ts +6 -0
  226. package/src/hooks/useForm.ts +247 -0
  227. package/src/hooks/useMotionPreference.test.ts +102 -0
  228. package/src/hooks/useMotionPreference.ts +78 -0
  229. package/src/hooks/useTheme.ts +58 -0
  230. package/src/hooks.ts +9 -0
  231. package/src/index.ts +168 -0
  232. package/src/lib/animations.ts +356 -0
  233. package/src/lib/breadcrumbs.ts +94 -0
  234. package/src/lib/colors.ts +493 -0
  235. package/src/lib/store/customizer.ts +482 -0
  236. package/src/lib/store/index.ts +3 -0
  237. package/src/lib/store/theme.ts +55 -0
  238. package/src/lib/syntax-parser/index.ts +50 -0
  239. package/src/lib/syntax-parser/patterns.ts +64 -0
  240. package/src/lib/syntax-parser/tokenizer.ts +117 -0
  241. package/src/lib/syntax-parser/types.ts +27 -0
  242. package/src/lib/utils.ts +6 -0
  243. package/src/lib/validation.ts +204 -0
  244. package/src/lib/webgl/Color.ts +11 -0
  245. package/src/lib/webgl/Mesh.ts +41 -0
  246. package/src/lib/webgl/Program.ts +118 -0
  247. package/src/lib/webgl/Renderer.ts +51 -0
  248. package/src/lib/webgl/Triangle.ts +27 -0
  249. package/src/lib/webgl/Vec3.ts +18 -0
  250. package/src/lib/webgl/index.ts +13 -0
  251. package/src/nativewind-env.d.ts +1 -0
  252. package/src/providers/ThemeProvider.tsx +461 -0
  253. package/src/providers/index.ts +1 -0
  254. package/src/providers.ts +7 -0
  255. package/src/tables.ts +1 -0
  256. package/src/test/setup.ts +39 -0
  257. package/src/theme.css +158 -0
  258. package/src/tokens.ts +7 -0
  259. package/src/utils.ts +12 -0
  260. package/src/webgl.ts +1 -0
@@ -0,0 +1,61 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { createRef } from 'react'
5
+ import { Button } from './Button'
6
+
7
+ describe('Button', () => {
8
+ it('renders with default variant', () => {
9
+ render(<Button>Click me</Button>)
10
+ const button = screen.getByRole('button', { name: /click me/i })
11
+ expect(button).toBeInTheDocument()
12
+ expect(button).toHaveClass('bg-primary')
13
+ })
14
+
15
+ it('renders different variants', () => {
16
+ const { rerender } = render(<Button variant="destructive">Delete</Button>)
17
+ expect(screen.getByRole('button')).toHaveClass('bg-destructive')
18
+
19
+ rerender(<Button variant="outline">Outline</Button>)
20
+ expect(screen.getByRole('button')).toHaveClass('border')
21
+
22
+ rerender(<Button variant="ghost">Ghost</Button>)
23
+ expect(screen.getByRole('button')).toHaveClass('hover:text-accent-foreground')
24
+
25
+ rerender(<Button variant="link">Link</Button>)
26
+ expect(screen.getByRole('button')).toHaveClass('underline-offset-4')
27
+ })
28
+
29
+ it('handles click events', async () => {
30
+ const user = userEvent.setup()
31
+ const handleClick = vi.fn()
32
+ render(<Button onClick={handleClick}>Click me</Button>)
33
+
34
+ await user.click(screen.getByRole('button'))
35
+ expect(handleClick).toHaveBeenCalledTimes(1)
36
+ })
37
+
38
+ it('renders as child when asChild is true', () => {
39
+ render(
40
+ <Button asChild>
41
+ <a href="/test">Link Button</a>
42
+ </Button>
43
+ )
44
+ const link = screen.getByRole('link', { name: /link button/i })
45
+ expect(link).toBeInTheDocument()
46
+ expect(link).toHaveAttribute('href', '/test')
47
+ expect(screen.queryByRole('button')).not.toBeInTheDocument()
48
+ })
49
+
50
+ it('forwards ref', () => {
51
+ const ref = createRef<HTMLButtonElement>()
52
+ render(<Button ref={ref}>Ref Button</Button>)
53
+ expect(ref.current).toBeInstanceOf(HTMLButtonElement)
54
+ })
55
+
56
+ it('applies custom className', () => {
57
+ render(<Button className="custom-class">Styled</Button>)
58
+ const button = screen.getByRole('button')
59
+ expect(button).toHaveClass('custom-class')
60
+ })
61
+ })
@@ -0,0 +1,70 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../../lib/utils';
4
+ import { Slot } from '@radix-ui/react-slot';
5
+
6
+ const buttonVariants = cva(
7
+ 'inline-flex items-center justify-center 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 sage-interactive [&_svg]:transition-transform [&_svg]:duration-300 hover:[&_svg]:translate-x-1',
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: 'bg-primary text-primary-foreground shadow-sm',
12
+ primary: 'bg-primary text-primary-foreground shadow-sm', // Alias for default
13
+ destructive: 'bg-destructive text-destructive-foreground shadow-xs',
14
+ outline: 'border border-input bg-transparent shadow-xs hover:bg-primary hover:text-primary-foreground hover:border-primary',
15
+ secondary: 'bg-black/5 dark:bg-white/10 backdrop-blur-md border border-black/5 dark:border-white/10 text-secondary-foreground shadow-xs hover:bg-primary hover:text-primary-foreground dark:hover:bg-primary dark:hover:text-primary-foreground',
16
+ ghost: 'hover:text-accent-foreground',
17
+ link: 'text-primary underline-offset-4 hover:underline',
18
+ },
19
+ size: {
20
+ default: 'h-9 px-4 py-2',
21
+ sm: 'h-8 rounded-md px-3 text-xs',
22
+ lg: 'h-10 rounded-md px-8',
23
+ icon: 'h-9 w-9',
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ variant: 'default',
28
+ size: 'default',
29
+ },
30
+ }
31
+ );
32
+
33
+ export interface ButtonProps
34
+ extends React.ButtonHTMLAttributes<HTMLButtonElement>,
35
+ VariantProps<typeof buttonVariants> {
36
+ asChild?: boolean;
37
+ }
38
+
39
+ const Button = (
40
+ {
41
+ ref,
42
+ className,
43
+ variant,
44
+ size,
45
+ asChild = false,
46
+ children,
47
+ ...props
48
+ }: ButtonProps & {
49
+ ref?: React.Ref<HTMLButtonElement>;
50
+ }
51
+ ) => {
52
+ const Comp = asChild ? Slot : "button"
53
+ return (
54
+ <Comp
55
+ className={cn(buttonVariants({ variant, size, className }))}
56
+ ref={ref}
57
+ {...props}
58
+ >
59
+ {asChild ? (
60
+ children
61
+ ) : (
62
+ <span className="relative z-20 flex items-center justify-center gap-2">
63
+ {children}
64
+ </span>
65
+ )}
66
+ </Comp>
67
+ )
68
+ }
69
+
70
+ export { Button, buttonVariants };
@@ -0,0 +1,78 @@
1
+ import React from 'react';
2
+
3
+ export interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
4
+ children: React.ReactNode;
5
+ /**
6
+ * Visual style variant for the link
7
+ * @default 'default'
8
+ */
9
+ variant?: 'default' | 'inline';
10
+ /**
11
+ * Whether to apply hover effect (only for 'default' variant).
12
+ * @default true
13
+ */
14
+ hoverEffect?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Link Component
19
+ *
20
+ * A theme-aware link with multiple style variants.
21
+ *
22
+ * Variants:
23
+ * - **default**: Background highlight on hover, good for standalone links
24
+ * - **inline**: Underlined text link, good for inline links within paragraphs
25
+ *
26
+ * Features:
27
+ * - Theme-aware colors using CSS variables
28
+ * - Smooth transition respecting motion preferences
29
+ * - Accessible focus states
30
+ * - Works with Next.js Link or standard anchor tags
31
+ *
32
+ * Usage:
33
+ * ```tsx
34
+ * <Link href="/about">About</Link>
35
+ * <Link variant="inline" href="/contact">Contact</Link>
36
+ * <Link href="https://example.com" target="_blank">External</Link>
37
+ * ```
38
+ */
39
+ export const Link = (
40
+ {
41
+ ref,
42
+ children,
43
+ className = '',
44
+ variant = 'default',
45
+ hoverEffect = true,
46
+ ...props
47
+ }: LinkProps & {
48
+ ref?: React.Ref<HTMLAnchorElement>;
49
+ }
50
+ ) => {
51
+ const variantStyles = {
52
+ default: `
53
+ px-2 py-1 -mx-2 -my-1 rounded
54
+ text-[var(--color-text-primary)]
55
+ ${hoverEffect ? 'hover:bg-[var(--color-link-hover)] hover:text-[var(--color-link-hover-foreground)]' : ''}
56
+ `,
57
+ inline: `
58
+ text-[var(--color-primary)]
59
+ underline decoration-[var(--color-primary)]/40 decoration-1 underline-offset-2
60
+ hover:decoration-[var(--color-primary)] hover:decoration-2
61
+ `,
62
+ };
63
+
64
+ return (
65
+ <a
66
+ ref={ref}
67
+ className={`
68
+ transition-all duration-200
69
+ focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus)] focus-visible:ring-offset-2
70
+ ${variantStyles[variant]}
71
+ ${className}
72
+ `}
73
+ {...props}
74
+ >
75
+ {children}
76
+ </a>
77
+ );
78
+ };
@@ -0,0 +1,68 @@
1
+ 'use client';
2
+
3
+ import { motion, useMotionValue, useSpring, useTransform } from 'framer-motion';
4
+ import React, { useRef } from 'react';
5
+ import { cn } from '../../lib/utils';
6
+
7
+ export interface MagneticProps {
8
+ children: React.ReactNode;
9
+ /**
10
+ * The strength of the magnetic pull. Higher numbers = strong pull.
11
+ * @default 0.2
12
+ */
13
+ strength?: number;
14
+ /**
15
+ * The active area padding around the element in pixels.
16
+ * @default 100
17
+ */
18
+ range?: number;
19
+ className?: string;
20
+ }
21
+
22
+ /**
23
+ * Wraps an element to give it a magnetic attraction to the cursor.
24
+ */
25
+ import { useMotionPreference } from '../../hooks/useMotionPreference';
26
+
27
+ export function Magnetic({ children, strength = 0.2, range = 100, className }: MagneticProps) {
28
+ const { shouldAnimate, scale } = useMotionPreference();
29
+ const ref = useRef<HTMLDivElement>(null);
30
+ const position = { x: useMotionValue(0), y: useMotionValue(0) };
31
+
32
+ const springOptions = { damping: 15, stiffness: 150, mass: 0.1 };
33
+ const smoothX = useSpring(position.x, springOptions);
34
+ const smoothY = useSpring(position.y, springOptions);
35
+
36
+ const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
37
+ if (!shouldAnimate) return;
38
+
39
+ const { clientX, clientY } = e;
40
+ const { height, width, left, top } = ref.current?.getBoundingClientRect() || { height: 0, width: 0, left: 0, top: 0 };
41
+
42
+ const middleX = clientX - (left + width / 2);
43
+ const middleY = clientY - (top + height / 2);
44
+
45
+ // Scale strength based on motion intensity (baseline 5)
46
+ const effectiveStrength = strength * (scale / 5);
47
+
48
+ position.x.set(middleX * effectiveStrength);
49
+ position.y.set(middleY * effectiveStrength);
50
+ };
51
+
52
+ const handleMouseLeave = () => {
53
+ position.x.set(0);
54
+ position.y.set(0);
55
+ };
56
+
57
+ return (
58
+ <motion.div
59
+ ref={ref}
60
+ onMouseMove={handleMouseMove}
61
+ onMouseLeave={handleMouseLeave}
62
+ style={{ x: smoothX, y: smoothY }}
63
+ className={cn("inline-block", className)}
64
+ >
65
+ {children}
66
+ </motion.div>
67
+ );
68
+ }
@@ -0,0 +1,40 @@
1
+ import { render, screen } from '@testing-library/react'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { describe, it, expect, vi } from 'vitest'
4
+ import { Toggle } from './Toggle'
5
+
6
+ describe('Toggle', () => {
7
+ it('renders with default variant', () => {
8
+ render(<Toggle aria-label="Bold">B</Toggle>)
9
+ expect(screen.getByRole('button', { name: /bold/i })).toBeInTheDocument()
10
+ })
11
+
12
+ it('handles pressed state changes', async () => {
13
+ const user = userEvent.setup()
14
+ const onPressedChange = vi.fn()
15
+ render(<Toggle onPressedChange={onPressedChange} aria-label="Bold">B</Toggle>)
16
+
17
+ await user.click(screen.getByRole('button'))
18
+ expect(onPressedChange).toHaveBeenCalledWith(true)
19
+ })
20
+
21
+ it('renders pressed state', () => {
22
+ render(<Toggle pressed aria-label="Bold">B</Toggle>)
23
+ expect(screen.getByRole('button')).toHaveAttribute('data-state', 'on')
24
+ })
25
+
26
+ it('renders unpressed state', () => {
27
+ render(<Toggle pressed={false} aria-label="Bold">B</Toggle>)
28
+ expect(screen.getByRole('button')).toHaveAttribute('data-state', 'off')
29
+ })
30
+
31
+ it('applies custom className', () => {
32
+ render(<Toggle className="custom-toggle" aria-label="Bold">B</Toggle>)
33
+ expect(screen.getByRole('button')).toHaveClass('custom-toggle')
34
+ })
35
+
36
+ it('can be disabled', () => {
37
+ render(<Toggle disabled aria-label="Bold">B</Toggle>)
38
+ expect(screen.getByRole('button')).toBeDisabled()
39
+ })
40
+ })
@@ -0,0 +1,47 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as TogglePrimitive from "@radix-ui/react-toggle"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+
8
+ const toggleVariants = cva(
9
+ "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground",
10
+ {
11
+ variants: {
12
+ variant: {
13
+ default: "bg-transparent",
14
+ outline:
15
+ "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
16
+ },
17
+ size: {
18
+ default: "h-9 px-3",
19
+ sm: "h-8 px-2",
20
+ lg: "h-10 px-3",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ size: "default",
26
+ },
27
+ }
28
+ )
29
+
30
+ const Toggle = (
31
+ {
32
+ ref,
33
+ className,
34
+ variant,
35
+ size,
36
+ ...props
37
+ }: React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
38
+ VariantProps<typeof toggleVariants> & {
39
+ ref?: React.Ref<React.ElementRef<typeof TogglePrimitive.Root>>;
40
+ }
41
+ ) => (<TogglePrimitive.Root
42
+ ref={ref}
43
+ className={cn(toggleVariants({ variant, size, className }))}
44
+ {...props}
45
+ />)
46
+
47
+ export { Toggle, toggleVariants }
@@ -0,0 +1,70 @@
1
+ "use client";
2
+ import * as React from "react"
3
+ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
4
+ import { type VariantProps } from "class-variance-authority"
5
+
6
+ import { cn } from "../../lib/utils"
7
+ import { toggleVariants } from "./Toggle"
8
+
9
+ const ToggleGroupContext = React.createContext<
10
+ VariantProps<typeof toggleVariants>
11
+ >({
12
+ size: "default",
13
+ variant: "default",
14
+ })
15
+
16
+ const ToggleGroup = (
17
+ {
18
+ ref,
19
+ className,
20
+ variant,
21
+ size,
22
+ children,
23
+ ...props
24
+ }: React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
25
+ VariantProps<typeof toggleVariants> & {
26
+ ref?: React.Ref<React.ElementRef<typeof ToggleGroupPrimitive.Root>>;
27
+ }
28
+ ) => (<ToggleGroupPrimitive.Root
29
+ ref={ref}
30
+ className={cn("flex items-center justify-center gap-1", className)}
31
+ {...props}
32
+ >
33
+ <ToggleGroupContext.Provider value={{ variant, size }}>
34
+ {children}
35
+ </ToggleGroupContext.Provider>
36
+ </ToggleGroupPrimitive.Root>)
37
+
38
+ const ToggleGroupItem = (
39
+ {
40
+ ref,
41
+ className,
42
+ children,
43
+ variant,
44
+ size,
45
+ ...props
46
+ }: React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
47
+ VariantProps<typeof toggleVariants> & {
48
+ ref?: React.Ref<React.ElementRef<typeof ToggleGroupPrimitive.Item>>;
49
+ }
50
+ ) => {
51
+ const context = React.useContext(ToggleGroupContext)
52
+
53
+ return (
54
+ <ToggleGroupPrimitive.Item
55
+ ref={ref}
56
+ className={cn(
57
+ toggleVariants({
58
+ variant: context.variant || variant,
59
+ size: context.size || size,
60
+ }),
61
+ className
62
+ )}
63
+ {...props}
64
+ >
65
+ {children}
66
+ </ToggleGroupPrimitive.Item>
67
+ )
68
+ }
69
+
70
+ export { ToggleGroup, ToggleGroupItem }
@@ -0,0 +1,5 @@
1
+ export * from './Button';
2
+ export * from './Link';
3
+ export * from './Toggle';
4
+ export * from './ToggleGroup';
5
+ export * from './Magnetic';