@mrigx/ui 0.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 (93) hide show
  1. package/package.json +73 -0
  2. package/src/blocks/ContactPanel.tsx +99 -0
  3. package/src/blocks/FeatureGrid.tsx +38 -0
  4. package/src/blocks/OpticalCTA.tsx +33 -0
  5. package/src/blocks/OpticalHero.tsx +51 -0
  6. package/src/blocks/styles.ts +175 -0
  7. package/src/components/accordion/optical-accordion.tsx +199 -0
  8. package/src/components/alert/optical-alert.tsx +59 -0
  9. package/src/components/alert-dialog/optical-alert-dialog.tsx +143 -0
  10. package/src/components/ambient-light.tsx +27 -0
  11. package/src/components/aspect-ratio/optical-aspect-ratio.tsx +7 -0
  12. package/src/components/attachment/optical-attachment.tsx +25 -0
  13. package/src/components/avatar/optical-avatar.tsx +50 -0
  14. package/src/components/badge/optical-badge.tsx +36 -0
  15. package/src/components/breadcrumb/optical-breadcrumb.tsx +115 -0
  16. package/src/components/bubble/optical-bubble.tsx +25 -0
  17. package/src/components/button/optical-button.tsx +67 -0
  18. package/src/components/button-group/optical-button-group.tsx +83 -0
  19. package/src/components/calendar/optical-calendar.tsx +214 -0
  20. package/src/components/card/optical-card.tsx +44 -0
  21. package/src/components/carousel/optical-carousel.tsx +262 -0
  22. package/src/components/chart/optical-chart.tsx +369 -0
  23. package/src/components/checkbox/optical-checkbox.tsx +42 -0
  24. package/src/components/collapsible/optical-collapsible.tsx +11 -0
  25. package/src/components/combobox/optical-combobox.tsx +25 -0
  26. package/src/components/command/optical-command.tsx +153 -0
  27. package/src/components/context-menu/optical-context-menu.tsx +201 -0
  28. package/src/components/cursor/optical-cursor.tsx +10 -0
  29. package/src/components/data-table/optical-data-table.tsx +25 -0
  30. package/src/components/date-picker/optical-date-picker.tsx +25 -0
  31. package/src/components/dialog/optical-dialog.tsx +110 -0
  32. package/src/components/direction/optical-direction.tsx +25 -0
  33. package/src/components/drawer/optical-drawer.tsx +120 -0
  34. package/src/components/dropdown-menu/optical-dropdown-menu.tsx +194 -0
  35. package/src/components/empty/optical-empty.tsx +104 -0
  36. package/src/components/field/optical-field.tsx +244 -0
  37. package/src/components/glass-card.tsx +13 -0
  38. package/src/components/hover-card/optical-hover-card.tsx +30 -0
  39. package/src/components/icon-button/optical-icon-button.tsx +22 -0
  40. package/src/components/input/optical-input.tsx +29 -0
  41. package/src/components/input-group/optical-input-group.tsx +170 -0
  42. package/src/components/input-otp/optical-input-otp.tsx +71 -0
  43. package/src/components/item/optical-item.tsx +193 -0
  44. package/src/components/kbd/optical-kbd.tsx +28 -0
  45. package/src/components/label/optical-label.tsx +25 -0
  46. package/src/components/marker/optical-marker.tsx +25 -0
  47. package/src/components/menubar/optical-menubar.tsx +257 -0
  48. package/src/components/message/optical-message.tsx +25 -0
  49. package/src/components/message-scroller/optical-message-scroller.tsx +25 -0
  50. package/src/components/native-select/optical-native-select.tsx +25 -0
  51. package/src/components/navigation-menu/optical-navigation-menu.tsx +68 -0
  52. package/src/components/pagination/optical-pagination.tsx +118 -0
  53. package/src/components/panel/optical-panel.tsx +26 -0
  54. package/src/components/popover/optical-popover.tsx +32 -0
  55. package/src/components/prism-button.tsx +17 -0
  56. package/src/components/progress/optical-progress.tsx +28 -0
  57. package/src/components/questionnaire/optical-questionnaire.tsx +25 -0
  58. package/src/components/radio-group/optical-radio-group.tsx +25 -0
  59. package/src/components/resizable/optical-resizable.tsx +45 -0
  60. package/src/components/scroll-area/optical-scroll-area.tsx +48 -0
  61. package/src/components/select/optical-select.tsx +110 -0
  62. package/src/components/separator/optical-separator.tsx +31 -0
  63. package/src/components/sheet/optical-sheet.tsx +143 -0
  64. package/src/components/sidebar/optical-sidebar.tsx +762 -0
  65. package/src/components/skeleton/optical-skeleton.tsx +15 -0
  66. package/src/components/slider/optical-slider.tsx +34 -0
  67. package/src/components/sonner/optical-toast.tsx +45 -0
  68. package/src/components/spinner/optical-spinner.tsx +16 -0
  69. package/src/components/stained-glass-card.tsx +93 -0
  70. package/src/components/switch/optical-switch.tsx +31 -0
  71. package/src/components/table/optical-table.tsx +83 -0
  72. package/src/components/tabs/optical-tabs.tsx +56 -0
  73. package/src/components/text/liquid-tension-text.tsx +65 -0
  74. package/src/components/text/optical-tension-text.tsx +65 -0
  75. package/src/components/text/optical-text.tsx +20 -0
  76. package/src/components/text/prismatic-selection.tsx +5 -0
  77. package/src/components/textarea/optical-textarea.tsx +22 -0
  78. package/src/components/theme-toggle/optical-theme-toggle.tsx +45 -0
  79. package/src/components/toast/optical-toast.tsx +25 -0
  80. package/src/components/toggle/optical-toggle.tsx +45 -0
  81. package/src/components/toggle-group/optical-toggle-group.tsx +61 -0
  82. package/src/components/tooltip/optical-tooltip.tsx +33 -0
  83. package/src/components/typography/optical-typography.tsx +49 -0
  84. package/src/hooks/use-mobile.ts +19 -0
  85. package/src/index.ts +88 -0
  86. package/src/templates/ContactPageTemplate.tsx +42 -0
  87. package/src/templates/LandingPageTemplate.tsx +29 -0
  88. package/src/templates/ProductPageTemplate.tsx +46 -0
  89. package/src/templates/styles.ts +23 -0
  90. package/src/utils/button-variants.ts +26 -0
  91. package/src/utils/cn.ts +6 -0
  92. package/tsconfig.json +27 -0
  93. package/vite.config.ts +42 -0
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@mrigx/ui",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/ui.umd.js",
6
+ "module": "./dist/ui.es.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/ui.es.js",
12
+ "require": "./dist/ui.umd.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "build": "vite build"
17
+ },
18
+ "dependencies": {
19
+ "@mrigx/core": "*",
20
+ "@mrigx/react": "*",
21
+ "@mrigx/fx": "*",
22
+ "clsx": "^2.1.1",
23
+ "tailwind-merge": "^3.7.0",
24
+ "@radix-ui/react-accordion": "^1.1.2",
25
+ "@radix-ui/react-alert-dialog": "^1.1.2",
26
+ "@radix-ui/react-aspect-ratio": "^1.1.0",
27
+ "@radix-ui/react-avatar": "^1.1.1",
28
+ "@radix-ui/react-checkbox": "^1.1.2",
29
+ "@radix-ui/react-collapsible": "^1.1.1",
30
+ "@radix-ui/react-context-menu": "^2.2.2",
31
+ "@radix-ui/react-dialog": "^1.1.2",
32
+ "@radix-ui/react-dropdown-menu": "^2.1.2",
33
+ "@radix-ui/react-hover-card": "^1.1.2",
34
+ "@radix-ui/react-label": "^2.1.0",
35
+ "@radix-ui/react-menubar": "^1.1.2",
36
+ "@radix-ui/react-navigation-menu": "^1.2.1",
37
+ "@radix-ui/react-popover": "^1.1.2",
38
+ "@radix-ui/react-progress": "^1.1.0",
39
+ "@radix-ui/react-radio-group": "^1.2.1",
40
+ "@radix-ui/react-scroll-area": "^1.2.0",
41
+ "@radix-ui/react-select": "^2.1.2",
42
+ "@radix-ui/react-separator": "^1.1.0",
43
+ "@radix-ui/react-slider": "^1.2.1",
44
+ "@radix-ui/react-slot": "^1.1.0",
45
+ "@radix-ui/react-switch": "^1.1.1",
46
+ "@radix-ui/react-tabs": "^1.1.1",
47
+ "@radix-ui/react-toast": "^1.2.2",
48
+ "@radix-ui/react-toggle": "^1.1.0",
49
+ "@radix-ui/react-toggle-group": "^1.1.0",
50
+ "@radix-ui/react-tooltip": "^1.1.3",
51
+ "lucide-react": "^0.453.0",
52
+ "framer-motion": "^11.11.9",
53
+ "cmdk": "^1.0.0",
54
+ "date-fns": "^4.1.0",
55
+ "react-day-picker": "^9.2.0",
56
+ "input-otp": "^1.4.1",
57
+ "react-resizable-panels": "^2.1.4",
58
+ "recharts": "^2.13.0",
59
+ "sonner": "^1.5.0",
60
+ "embla-carousel-react": "^8.3.0",
61
+ "class-variance-authority": "^0.7.0"
62
+ },
63
+ "peerDependencies": {
64
+ "react": "^19.0.0",
65
+ "react-dom": "^19.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "typescript": "~5.7.2",
69
+ "vite": "^6.0.0",
70
+ "vite-plugin-dts": "^4.5.0",
71
+ "@vitejs/plugin-react": "^4.3.4"
72
+ }
73
+ }
@@ -0,0 +1,99 @@
1
+ import React, { useState, useId } from 'react';
2
+ import { injectMrigBlockStyles } from './styles';
3
+ import { OpticalPanel } from '../components/panel/optical-panel';
4
+ import { OpticalInput } from '../components/input/optical-input';
5
+ import { OpticalButton } from '../components/button/optical-button';
6
+ import { Textarea as OpticalTextarea } from '../components/textarea/optical-textarea';
7
+
8
+ export interface ContactData {
9
+ name: string;
10
+ email: string;
11
+ message: string;
12
+ }
13
+
14
+ export interface ContactPanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'onSubmit'> {
15
+ title?: string;
16
+ description?: string;
17
+ isSubmitting?: boolean;
18
+ onSubmit: (data: ContactData) => void;
19
+ }
20
+
21
+ export function ContactPanel({
22
+ title = "Get in touch",
23
+ description = "We'd love to hear from you. Send us a message.",
24
+ isSubmitting = false,
25
+ onSubmit,
26
+ className = "",
27
+ ...props
28
+ }: ContactPanelProps) {
29
+ injectMrigBlockStyles();
30
+
31
+ const baseId = useId();
32
+
33
+ const [name, setName] = useState('');
34
+ const [email, setEmail] = useState('');
35
+ const [message, setMessage] = useState('');
36
+
37
+ const handleSubmit = (e: React.FormEvent) => {
38
+ e.preventDefault();
39
+ onSubmit({ name, email, message });
40
+ };
41
+
42
+ return (
43
+ <div className={`mrig-contact-container ${className}`} {...props}>
44
+ <OpticalPanel opticalStrength="default">
45
+ <div className="mrig-contact-header">
46
+ <h2 className="mrig-contact-title">{title}</h2>
47
+ <p className="mrig-contact-desc">{description}</p>
48
+ </div>
49
+
50
+ <form onSubmit={handleSubmit} className="mrig-contact-form">
51
+ <div className="mrig-contact-field">
52
+ <label htmlFor={`${baseId}-name`} className="mrig-contact-label">Name</label>
53
+ <OpticalInput
54
+ id={`${baseId}-name`}
55
+ type="text"
56
+ required
57
+ value={name}
58
+ onChange={e => setName(e.target.value)}
59
+ disabled={isSubmitting}
60
+ autoComplete="name"
61
+ />
62
+ </div>
63
+
64
+ <div className="mrig-contact-field">
65
+ <label htmlFor={`${baseId}-email`} className="mrig-contact-label">Email</label>
66
+ <OpticalInput
67
+ id={`${baseId}-email`}
68
+ type="email"
69
+ required
70
+ value={email}
71
+ onChange={e => setEmail(e.target.value)}
72
+ disabled={isSubmitting}
73
+ autoComplete="email"
74
+ />
75
+ </div>
76
+
77
+ <div className="mrig-contact-field">
78
+ <label htmlFor={`${baseId}-message`} className="mrig-contact-label">Message</label>
79
+ <OpticalTextarea
80
+ id={`${baseId}-message`}
81
+ required
82
+ value={message}
83
+ onChange={e => setMessage(e.target.value)}
84
+ disabled={isSubmitting}
85
+ />
86
+ </div>
87
+
88
+ <OpticalButton
89
+ type="submit"
90
+ disabled={isSubmitting}
91
+ style={{ marginTop: '1rem', width: '100%' }}
92
+ >
93
+ {isSubmitting ? 'Sending...' : 'Send Message'}
94
+ </OpticalButton>
95
+ </form>
96
+ </OpticalPanel>
97
+ </div>
98
+ );
99
+ }
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { injectMrigBlockStyles } from './styles';
3
+ import { OpticalPanel } from '../components/panel/optical-panel';
4
+
5
+ export interface FeatureItem {
6
+ id: string;
7
+ title: string;
8
+ description: string;
9
+ }
10
+
11
+ export interface FeatureGridProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ features: FeatureItem[];
13
+ }
14
+
15
+ export function FeatureGrid({
16
+ features,
17
+ className = "",
18
+ ...props
19
+ }: FeatureGridProps) {
20
+ injectMrigBlockStyles();
21
+
22
+ return (
23
+ <section className={`mrig-feature-grid-container ${className}`} {...props}>
24
+ <div className="mrig-feature-grid">
25
+ {features.map((feature) => (
26
+ <OpticalPanel
27
+ key={feature.id}
28
+ className="mrig-feature-item"
29
+ opticalStrength="soft"
30
+ >
31
+ <h3 className="mrig-feature-item-title">{feature.title}</h3>
32
+ <p className="mrig-feature-item-desc">{feature.description}</p>
33
+ </OpticalPanel>
34
+ ))}
35
+ </div>
36
+ </section>
37
+ );
38
+ }
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { injectMrigBlockStyles } from './styles';
3
+ import { OpticalPanel } from '../components/panel/optical-panel';
4
+
5
+ export interface OpticalCTAProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
6
+ title: React.ReactNode;
7
+ description?: React.ReactNode;
8
+ primaryAction: React.ReactNode;
9
+ secondaryAction?: React.ReactNode;
10
+ }
11
+
12
+ export function OpticalCTA({
13
+ title,
14
+ description,
15
+ primaryAction,
16
+ secondaryAction,
17
+ className = "",
18
+ ...props
19
+ }: OpticalCTAProps) {
20
+ injectMrigBlockStyles();
21
+
22
+ return (
23
+ <OpticalPanel className={`mrig-cta-panel ${className}`} opticalStrength="strong" {...props}>
24
+ <h2 className="mrig-cta-title">{title}</h2>
25
+ {description && <p className="mrig-cta-desc">{description}</p>}
26
+
27
+ <div className="mrig-cta-actions">
28
+ {primaryAction}
29
+ {secondaryAction}
30
+ </div>
31
+ </OpticalPanel>
32
+ );
33
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { injectMrigBlockStyles } from './styles';
3
+
4
+ export interface OpticalHeroProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> {
5
+ eyebrow?: string;
6
+ title: React.ReactNode;
7
+ headingLevel?: 1 | 2 | 3 | 4 | 5 | 6;
8
+ description?: React.ReactNode;
9
+ primaryAction?: React.ReactNode;
10
+ secondaryAction?: React.ReactNode;
11
+ visualArea?: React.ReactNode;
12
+ }
13
+
14
+ export function OpticalHero({
15
+ eyebrow,
16
+ title,
17
+ headingLevel = 1,
18
+ description,
19
+ primaryAction,
20
+ secondaryAction,
21
+ visualArea,
22
+ className = "",
23
+ ...props
24
+ }: OpticalHeroProps) {
25
+ injectMrigBlockStyles();
26
+
27
+ const HeadingTag = `h${headingLevel}` as React.ElementType;
28
+
29
+ return (
30
+ <section className={`mrig-hero ${className}`} {...props}>
31
+ <div className="mrig-hero-content">
32
+ {eyebrow && <p className="mrig-hero-eyebrow">{eyebrow}</p>}
33
+ <HeadingTag className="mrig-hero-title">{title}</HeadingTag>
34
+ {description && <p className="mrig-hero-description">{description}</p>}
35
+
36
+ {(primaryAction || secondaryAction) && (
37
+ <div className="mrig-hero-actions">
38
+ {primaryAction}
39
+ {secondaryAction}
40
+ </div>
41
+ )}
42
+ </div>
43
+
44
+ {visualArea && (
45
+ <div className="mrig-hero-visual">
46
+ {visualArea}
47
+ </div>
48
+ )}
49
+ </section>
50
+ );
51
+ }
@@ -0,0 +1,175 @@
1
+ const BLOCK_STYLES = `
2
+ /* OpticalHero */
3
+ .mrig-hero {
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 3rem;
7
+ padding: 4rem 1.5rem;
8
+ width: 100%;
9
+ max-width: 1200px;
10
+ margin: 0 auto;
11
+ }
12
+ @media (min-width: 768px) {
13
+ .mrig-hero {
14
+ flex-direction: row;
15
+ align-items: center;
16
+ padding: 6rem 2rem;
17
+ }
18
+ }
19
+ .mrig-hero-content {
20
+ flex: 1;
21
+ display: flex;
22
+ flex-direction: column;
23
+ gap: 1.5rem;
24
+ }
25
+ .mrig-hero-visual {
26
+ flex: 1;
27
+ display: flex;
28
+ justify-content: center;
29
+ align-items: center;
30
+ }
31
+ .mrig-hero-eyebrow {
32
+ font-size: 0.875rem;
33
+ text-transform: uppercase;
34
+ letter-spacing: 0.1em;
35
+ color: rgba(255, 255, 255, 0.6);
36
+ margin: 0;
37
+ }
38
+ .mrig-hero-title {
39
+ font-size: 3rem;
40
+ font-weight: 300;
41
+ line-height: 1.1;
42
+ margin: 0;
43
+ color: white;
44
+ }
45
+ @media (min-width: 768px) {
46
+ .mrig-hero-title {
47
+ font-size: 4rem;
48
+ }
49
+ }
50
+ .mrig-hero-description {
51
+ font-size: 1.125rem;
52
+ color: rgba(255, 255, 255, 0.7);
53
+ line-height: 1.6;
54
+ margin: 0;
55
+ max-width: 600px;
56
+ }
57
+ .mrig-hero-actions {
58
+ display: flex;
59
+ gap: 1rem;
60
+ flex-wrap: wrap;
61
+ margin-top: 1rem;
62
+ }
63
+
64
+ /* FeatureGrid */
65
+ .mrig-feature-grid-container {
66
+ padding: 4rem 1.5rem;
67
+ width: 100%;
68
+ max-width: 1200px;
69
+ margin: 0 auto;
70
+ }
71
+ .mrig-feature-grid {
72
+ display: grid;
73
+ grid-template-columns: 1fr;
74
+ gap: 2rem;
75
+ }
76
+ @media (min-width: 640px) {
77
+ .mrig-feature-grid {
78
+ grid-template-columns: repeat(2, 1fr);
79
+ }
80
+ }
81
+ @media (min-width: 1024px) {
82
+ .mrig-feature-grid {
83
+ grid-template-columns: repeat(3, 1fr);
84
+ }
85
+ }
86
+ .mrig-feature-item-title {
87
+ font-size: 1.25rem;
88
+ font-weight: 400;
89
+ color: white;
90
+ margin: 0 0 1rem 0;
91
+ }
92
+ .mrig-feature-item-desc {
93
+ font-size: 1rem;
94
+ color: rgba(255, 255, 255, 0.6);
95
+ line-height: 1.5;
96
+ margin: 0;
97
+ }
98
+
99
+ /* ContactPanel */
100
+ .mrig-contact-container {
101
+ width: 100%;
102
+ max-width: 600px;
103
+ margin: 0 auto;
104
+ }
105
+ .mrig-contact-header {
106
+ margin-bottom: 2rem;
107
+ text-align: center;
108
+ }
109
+ .mrig-contact-title {
110
+ font-size: 2rem;
111
+ font-weight: 300;
112
+ color: white;
113
+ margin: 0 0 0.5rem 0;
114
+ }
115
+ .mrig-contact-desc {
116
+ font-size: 1rem;
117
+ color: rgba(255, 255, 255, 0.6);
118
+ margin: 0;
119
+ }
120
+ .mrig-contact-form {
121
+ display: flex;
122
+ flex-direction: column;
123
+ gap: 1.5rem;
124
+ }
125
+ .mrig-contact-field {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: 0.5rem;
129
+ }
130
+ .mrig-contact-label {
131
+ font-size: 0.875rem;
132
+ color: rgba(255, 255, 255, 0.8);
133
+ }
134
+
135
+ /* OpticalCTA */
136
+ .mrig-cta-panel {
137
+ text-align: center;
138
+ padding: 4rem 2rem;
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ gap: 1.5rem;
143
+ max-width: 800px;
144
+ margin: 0 auto;
145
+ }
146
+ .mrig-cta-title {
147
+ font-size: 2.5rem;
148
+ font-weight: 300;
149
+ color: white;
150
+ margin: 0;
151
+ }
152
+ .mrig-cta-desc {
153
+ font-size: 1.125rem;
154
+ color: rgba(255, 255, 255, 0.7);
155
+ margin: 0;
156
+ max-width: 600px;
157
+ }
158
+ .mrig-cta-actions {
159
+ display: flex;
160
+ gap: 1rem;
161
+ justify-content: center;
162
+ flex-wrap: wrap;
163
+ margin-top: 1rem;
164
+ }
165
+ `;
166
+
167
+ let injected = false;
168
+ export function injectMrigBlockStyles() {
169
+ if (typeof window === 'undefined' || injected) return;
170
+ const style = document.createElement('style');
171
+ style.id = 'mrig-block-styles';
172
+ style.innerHTML = BLOCK_STYLES;
173
+ document.head.appendChild(style);
174
+ injected = true;
175
+ }
@@ -0,0 +1,199 @@
1
+ import React, { createContext, useContext, forwardRef, useState } from 'react';
2
+ import { RippleSurface } from '@mrigx/fx';
3
+ import { useMrigInteraction } from '@mrigx/react';
4
+
5
+ /* --- CONTEXT --- */
6
+ type AccordionType = 'single' | 'multiple';
7
+
8
+ interface AccordionContextValue {
9
+ type: AccordionType;
10
+ value: string | string[];
11
+ onItemOpen: (value: string) => void;
12
+ onItemClose: (value: string) => void;
13
+ collapsible?: boolean;
14
+ }
15
+
16
+ const AccordionContext = createContext<AccordionContextValue | undefined>(undefined);
17
+ const AccordionItemContext = createContext<{ value: string; disabled?: boolean } | undefined>(undefined);
18
+
19
+ /* --- ACCORDION ROOT --- */
20
+ export interface OpticalAccordionProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'defaultValue' | 'onChange'> {
21
+ type?: AccordionType;
22
+ value?: string | string[];
23
+ defaultValue?: string | string[];
24
+ onValueChange?: (value: any) => void;
25
+ collapsible?: boolean;
26
+ }
27
+
28
+ export const OpticalAccordion = forwardRef<HTMLDivElement, OpticalAccordionProps>(({
29
+ className = "",
30
+ type = 'single',
31
+ value: controlledValue,
32
+ defaultValue,
33
+ onValueChange,
34
+ collapsible = false,
35
+ ...props
36
+ }, ref) => {
37
+ const [uncontrolledValue, setUncontrolledValue] = useState<string | string[]>(
38
+ defaultValue !== undefined ? defaultValue : (type === 'multiple' ? [] : "")
39
+ );
40
+
41
+ const value = controlledValue !== undefined ? controlledValue : uncontrolledValue;
42
+
43
+ const handleItemOpen = (itemValue: string) => {
44
+ if (type === 'single') {
45
+ const newValue = itemValue;
46
+ if (controlledValue === undefined) setUncontrolledValue(newValue);
47
+ onValueChange?.(newValue);
48
+ } else {
49
+ const prev = (Array.isArray(value) ? value : []) as string[];
50
+ const newValue = [...prev, itemValue];
51
+ if (controlledValue === undefined) setUncontrolledValue(newValue);
52
+ onValueChange?.(newValue);
53
+ }
54
+ };
55
+
56
+ const handleItemClose = (itemValue: string) => {
57
+ if (type === 'single') {
58
+ if (collapsible) {
59
+ if (controlledValue === undefined) setUncontrolledValue("");
60
+ onValueChange?.("");
61
+ }
62
+ } else {
63
+ const prev = (Array.isArray(value) ? value : []) as string[];
64
+ const newValue = prev.filter(v => v !== itemValue);
65
+ if (controlledValue === undefined) setUncontrolledValue(newValue);
66
+ onValueChange?.(newValue);
67
+ }
68
+ };
69
+
70
+ return (
71
+ <AccordionContext.Provider value={{ type, value, onItemOpen: handleItemOpen, onItemClose: handleItemClose, collapsible }}>
72
+ <div ref={ref} className={`${className}`} {...props} />
73
+ </AccordionContext.Provider>
74
+ );
75
+ });
76
+ OpticalAccordion.displayName = "OpticalAccordion";
77
+
78
+ /* --- ACCORDION ITEM --- */
79
+ export interface OpticalAccordionItemProps extends React.HTMLAttributes<HTMLDivElement> {
80
+ value: string;
81
+ disabled?: boolean;
82
+ material?: string;
83
+ materialStrength?: number;
84
+ }
85
+
86
+ export const OpticalAccordionItem = forwardRef<HTMLDivElement, OpticalAccordionItemProps>(({
87
+ className = "",
88
+ value,
89
+ disabled = false,
90
+ ...props
91
+ }, ref) => {
92
+ return (
93
+ <AccordionItemContext.Provider value={{ value, disabled }}>
94
+ <div
95
+ ref={ref}
96
+ className={`border-b border-white/10 ${disabled ? 'opacity-50 pointer-events-none' : ''} ${className}`}
97
+ {...props}
98
+ />
99
+ </AccordionItemContext.Provider>
100
+ );
101
+ });
102
+ OpticalAccordionItem.displayName = "OpticalAccordionItem";
103
+
104
+ /* --- ACCORDION TRIGGER --- */
105
+ export const OpticalAccordionTrigger = forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(({
106
+ className = "",
107
+ children,
108
+ onClick,
109
+ ...props
110
+ }, ref) => {
111
+ const ctx = useContext(AccordionContext);
112
+ const itemCtx = useContext(AccordionItemContext);
113
+
114
+ if (!ctx || !itemCtx) {
115
+ throw new Error("OpticalAccordionTrigger must be used within an OpticalAccordionItem");
116
+ }
117
+
118
+ const isOpen = ctx.type === 'single'
119
+ ? ctx.value === itemCtx.value
120
+ : (Array.isArray(ctx.value) && ctx.value.includes(itemCtx.value));
121
+
122
+ const { pulseCenter } = useMrigInteraction();
123
+
124
+ const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
125
+ if (itemCtx.disabled) return;
126
+
127
+ pulseCenter(e.currentTarget, 0.4);
128
+
129
+ if (isOpen) {
130
+ ctx.onItemClose(itemCtx.value);
131
+ } else {
132
+ ctx.onItemOpen(itemCtx.value);
133
+ }
134
+ onClick?.(e);
135
+ };
136
+
137
+ return (
138
+ <div className="flex">
139
+ <RippleSurface
140
+ as="button"
141
+ ref={ref as any}
142
+ material="liquid"
143
+ materialStrength={0.5}
144
+ borderRadius={8}
145
+ type="button"
146
+ aria-expanded={isOpen}
147
+ disabled={itemCtx.disabled}
148
+ onClick={handleClick}
149
+ className={`flex flex-1 items-center justify-between py-4 text-sm font-medium tracking-wide text-white/90 transition-all hover:text-white ${className}`}
150
+ {...props}
151
+ >
152
+ {children}
153
+ <svg
154
+ className={`h-4 w-4 shrink-0 text-white/50 transition-transform duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${isOpen ? 'rotate-180 text-white/90' : ''}`}
155
+ fill="none"
156
+ viewBox="0 0 24 24"
157
+ stroke="currentColor"
158
+ >
159
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
160
+ </svg>
161
+ </RippleSurface>
162
+ </div>
163
+ );
164
+ });
165
+ OpticalAccordionTrigger.displayName = "OpticalAccordionTrigger";
166
+
167
+ /* --- ACCORDION CONTENT --- */
168
+ export const OpticalAccordionContent = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(({
169
+ className = "",
170
+ children,
171
+ ...props
172
+ }, ref) => {
173
+ const ctx = useContext(AccordionContext);
174
+ const itemCtx = useContext(AccordionItemContext);
175
+
176
+ if (!ctx || !itemCtx) {
177
+ throw new Error("OpticalAccordionContent must be used within an OpticalAccordionItem");
178
+ }
179
+
180
+ const isOpen = ctx.type === 'single'
181
+ ? ctx.value === itemCtx.value
182
+ : (Array.isArray(ctx.value) && ctx.value.includes(itemCtx.value));
183
+
184
+ return (
185
+ <div
186
+ className={`grid transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.16,1,0.3,1)] ${
187
+ isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'
188
+ }`}
189
+ >
190
+ <div className="overflow-hidden">
191
+ <div ref={ref} className={`pb-4 pt-0 text-sm leading-relaxed text-white/60 ${className}`} {...props}>
192
+ {children}
193
+ </div>
194
+ </div>
195
+ </div>
196
+ );
197
+ });
198
+ OpticalAccordionContent.displayName = "OpticalAccordionContent";
199
+