@reactberry/system 2.0.0-beta

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 (165) hide show
  1. package/README.md +48 -0
  2. package/package.json +74 -0
  3. package/src/blocks/Accordion/index.tsx +158 -0
  4. package/src/blocks/AnimatedCarousel/index.tsx +188 -0
  5. package/src/blocks/AppleGlow/index.tsx +144 -0
  6. package/src/blocks/Avatar/index.tsx +167 -0
  7. package/src/blocks/Await/index.tsx +45 -0
  8. package/src/blocks/Cards/AnimatedCard/index.tsx +175 -0
  9. package/src/blocks/Cards/FluorescentCard/index.tsx +180 -0
  10. package/src/blocks/Cards/InfoCard/index.tsx +206 -0
  11. package/src/blocks/Cards/TickerCard/index.tsx +125 -0
  12. package/src/blocks/Carousel/index.tsx +216 -0
  13. package/src/blocks/Checkbox/index.tsx +101 -0
  14. package/src/blocks/Collection/index.tsx +59 -0
  15. package/src/blocks/Container/index.tsx +55 -0
  16. package/src/blocks/Controls/Control.tsx +67 -0
  17. package/src/blocks/Controls/index.tsx +11 -0
  18. package/src/blocks/CyclingNumber/index.tsx +78 -0
  19. package/src/blocks/DisplaySet/index.tsx +42 -0
  20. package/src/blocks/Divider/index.tsx +14 -0
  21. package/src/blocks/Draggable/index.tsx +266 -0
  22. package/src/blocks/Drawer/index.tsx +136 -0
  23. package/src/blocks/DynamicIsland/DynamicIsland.tsx +89 -0
  24. package/src/blocks/DynamicIsland/index.tsx +2 -0
  25. package/src/blocks/Fader/index.tsx +145 -0
  26. package/src/blocks/FamilyDrawer/README.md +116 -0
  27. package/src/blocks/FamilyDrawer/example.tsx +108 -0
  28. package/src/blocks/FamilyDrawer/index.tsx +119 -0
  29. package/src/blocks/FamilyDrawer/views/DefaultView.tsx +93 -0
  30. package/src/blocks/FamilyDrawer/views/KeyView.tsx +129 -0
  31. package/src/blocks/FamilyDrawer/views/PhraseView.tsx +129 -0
  32. package/src/blocks/FamilyDrawer/views/RemoveView.tsx +81 -0
  33. package/src/blocks/FieldSet/index.tsx +173 -0
  34. package/src/blocks/Filesystem/index.tsx +198 -0
  35. package/src/blocks/Gallery/Carousel/index.tsx +257 -0
  36. package/src/blocks/Gallery/Modal/index.tsx +83 -0
  37. package/src/blocks/Gallery/index.tsx +57 -0
  38. package/src/blocks/Gallery/utils/animationVariants.ts +18 -0
  39. package/src/blocks/Gallery/utils/aspectRatio.ts +14 -0
  40. package/src/blocks/Gallery/utils/downloadPhoto.ts +24 -0
  41. package/src/blocks/Gallery/utils/range.ts +11 -0
  42. package/src/blocks/GradientMesh/index.tsx +106 -0
  43. package/src/blocks/Group/index.tsx +152 -0
  44. package/src/blocks/Heading/index.tsx +111 -0
  45. package/src/blocks/HorizontalScroller/index.tsx +135 -0
  46. package/src/blocks/Icon/index.tsx +45 -0
  47. package/src/blocks/Indicator/index.tsx +27 -0
  48. package/src/blocks/InlineEditor/index.tsx +216 -0
  49. package/src/blocks/List/index.tsx +657 -0
  50. package/src/blocks/Main/index.tsx +17 -0
  51. package/src/blocks/Marquee/index.tsx +116 -0
  52. package/src/blocks/MaskedField/index.tsx +199 -0
  53. package/src/blocks/Menu/MenuContent.tsx +246 -0
  54. package/src/blocks/Menu/MenuContext.tsx +34 -0
  55. package/src/blocks/Menu/MenuItem.tsx +104 -0
  56. package/src/blocks/Menu/index.tsx +60 -0
  57. package/src/blocks/Modal/index.tsx +268 -0
  58. package/src/blocks/MorphingPopover/index.tsx +294 -0
  59. package/src/blocks/Overlay/Backdrop.tsx +48 -0
  60. package/src/blocks/Overlay/OverscrollGuard.tsx +36 -0
  61. package/src/blocks/Overlay/index.ts +2 -0
  62. package/src/blocks/Parallax/index.tsx +117 -0
  63. package/src/blocks/ParallaxSection/index.tsx +61 -0
  64. package/src/blocks/Placeholder/index.tsx +48 -0
  65. package/src/blocks/Popover/index.tsx +402 -0
  66. package/src/blocks/Progress/getProgressColor.ts +61 -0
  67. package/src/blocks/Progress/index.tsx +179 -0
  68. package/src/blocks/ProgressiveBlur/index.tsx +75 -0
  69. package/src/blocks/README.md +15 -0
  70. package/src/blocks/RenderAsset/index.tsx +18 -0
  71. package/src/blocks/ScrollContainer/index.tsx +93 -0
  72. package/src/blocks/ShinyText/index.tsx +72 -0
  73. package/src/blocks/Skeleton/index.tsx +71 -0
  74. package/src/blocks/Slider/SliderControls.tsx +119 -0
  75. package/src/blocks/Slider/index.tsx +140 -0
  76. package/src/blocks/Slider/useSlider.ts +126 -0
  77. package/src/blocks/Slideshow/index.tsx +177 -0
  78. package/src/blocks/Spotlight/index.tsx +144 -0
  79. package/src/blocks/Steps/StepIndicator.tsx +149 -0
  80. package/src/blocks/Steps/StepProgress.tsx +164 -0
  81. package/src/blocks/Steps/Steps.tsx +197 -0
  82. package/src/blocks/Steps/StepsNav.tsx +30 -0
  83. package/src/blocks/Steps/StepsTracker.tsx +80 -0
  84. package/src/blocks/Steps/hooks.ts +71 -0
  85. package/src/blocks/Steps/index.tsx +16 -0
  86. package/src/blocks/Steps/types.ts +71 -0
  87. package/src/blocks/StickySectionStack/index.tsx +136 -0
  88. package/src/blocks/Switch/index.tsx +85 -0
  89. package/src/blocks/SystemNotice/index.tsx +81 -0
  90. package/src/blocks/Table/README.md +251 -0
  91. package/src/blocks/Table/Table.tsx +207 -0
  92. package/src/blocks/Table/TablePagination.tsx +189 -0
  93. package/src/blocks/Table/index.ts +33 -0
  94. package/src/blocks/Table/useTableControls.ts +331 -0
  95. package/src/blocks/Tag/index.tsx +27 -0
  96. package/src/blocks/TextBreak/index.tsx +96 -0
  97. package/src/blocks/TextReveal/index.tsx +104 -0
  98. package/src/blocks/Thumbnail/index.tsx +26 -0
  99. package/src/blocks/Ticker/index.tsx +112 -0
  100. package/src/blocks/Toast/index.tsx +77 -0
  101. package/src/blocks/Tooltip/index.tsx +174 -0
  102. package/src/blocks/Underlay/index.tsx +104 -0
  103. package/src/blocks/Upload/Dropzone.tsx +92 -0
  104. package/src/blocks/Upload/UploadBtn.tsx +38 -0
  105. package/src/blocks/Upload/index.tsx +61 -0
  106. package/src/blocks/Upload/types.ts +37 -0
  107. package/src/blocks/VideoMarquee/index.tsx +511 -0
  108. package/src/blocks/index.ts +119 -0
  109. package/src/blocks/pagination/Pagination.tsx +148 -0
  110. package/src/blocks/pagination/PaginationList.tsx +41 -0
  111. package/src/blocks/pagination/index.ts +2 -0
  112. package/src/charts/BarChart.tsx +63 -0
  113. package/src/charts/PieChart.tsx +39 -0
  114. package/src/charts/index.ts +3 -0
  115. package/src/charts/utils.ts +103 -0
  116. package/src/docs/README.md +373 -0
  117. package/src/docs/reference/README.md +299 -0
  118. package/src/elements/box.ts +163 -0
  119. package/src/elements/button.ts +49 -0
  120. package/src/elements/field.ts +129 -0
  121. package/src/elements/index.ts +8 -0
  122. package/src/elements/text.ts +47 -0
  123. package/src/elements/utils.js +97 -0
  124. package/src/hooks/use-copy-to-clipboard.tsx +33 -0
  125. package/src/hooks/use-enter-submit.tsx +23 -0
  126. package/src/hooks/use-local-storage.ts +42 -0
  127. package/src/hooks/use-sidebar.tsx +109 -0
  128. package/src/hooks/useAnimatedText.ts +32 -0
  129. package/src/hooks/useAutosizeTextArea.ts +45 -0
  130. package/src/hooks/useBreakpoint.tsx +123 -0
  131. package/src/hooks/useClickOutside.tsx +38 -0
  132. package/src/hooks/useHover.tsx +33 -0
  133. package/src/hooks/useHoverList.tsx +17 -0
  134. package/src/hooks/useKeyboardShortcuts.ts +91 -0
  135. package/src/hooks/useKeypress.ts +27 -0
  136. package/src/hooks/useOverlay.ts +32 -0
  137. package/src/hooks/useReducedMotion.ts +25 -0
  138. package/src/hooks/useStandaloneMode.ts +35 -0
  139. package/src/hooks/useTouchDevice.ts +34 -0
  140. package/src/icons/index.tsx +129 -0
  141. package/src/index.ts +12 -0
  142. package/src/providers/DesignSystemProvider.tsx +35 -0
  143. package/src/providers/StyledComponentsRegistry.tsx +30 -0
  144. package/src/providers/index.ts +2 -0
  145. package/src/themes/README.md +30 -0
  146. package/src/themes/default/assets/badge-avatar.tsx +45 -0
  147. package/src/themes/default/assets/logo.tsx +42 -0
  148. package/src/themes/default/global.ts +138 -0
  149. package/src/themes/default/modes/dark/config.js +49 -0
  150. package/src/themes/default/modes/dark/skins.js +631 -0
  151. package/src/themes/default/modes/dark/theme.js +87 -0
  152. package/src/themes/default/modes/light/config.js +48 -0
  153. package/src/themes/default/modes/light/skins.js +1026 -0
  154. package/src/themes/default/modes/light/theme.js +74 -0
  155. package/src/themes/default/tokens/controls.js +53 -0
  156. package/src/themes/default/tokens/shadows.js +63 -0
  157. package/src/themes/default/tokens/shapes.js +37 -0
  158. package/src/themes/default/tokens/space.js +143 -0
  159. package/src/themes/default/tokens/spectre.js +16 -0
  160. package/src/themes/default/utils.js +523 -0
  161. package/src/themes/index.ts +11 -0
  162. package/src/types.ts +394 -0
  163. package/src/utils/overlayTheme.ts +61 -0
  164. package/src/utils/pickColor.ts +15 -0
  165. package/tsconfig.json +24 -0
package/README.md ADDED
@@ -0,0 +1,48 @@
1
+ # reactberry
2
+
3
+ `@reactberry/system` is the single-package Next.js UI library for Reactberry projects.
4
+
5
+ ## Package model
6
+
7
+ - This package intentionally depends on Next.js APIs like `next/image` and `next/navigation`.
8
+ - It ships source files from `src/` for Next.js apps to transpile.
9
+ - Consumer apps should add `@reactberry/system` to `transpilePackages`.
10
+
11
+ ## Installation
12
+
13
+ Install `@reactberry/system` together with its peer dependencies in your consuming app:
14
+
15
+ ```bash
16
+ npm install @reactberry/system@beta styled-components next react react-dom
17
+ ```
18
+
19
+ If you want the exact current prerelease, install `@reactberry/system@2.0.0-beta` instead.
20
+
21
+ - `next`
22
+ - `react`
23
+ - `react-dom`
24
+ - `styled-components`
25
+
26
+ ## Next.js setup
27
+
28
+ Add `@reactberry/system` to `transpilePackages` in your app's `next.config.js` or `next.config.ts`.
29
+
30
+ ## Example
31
+
32
+ Import components from `@reactberry/system` and wrap your app with `DesignSystemProvider`.
33
+
34
+ ## Exports
35
+
36
+ - `@reactberry/system`
37
+ - `@reactberry/system/providers`
38
+ - `@reactberry/system/themes`
39
+ - `@reactberry/system/blocks`
40
+ - `@reactberry/system/elements`
41
+ - `@reactberry/system/icons`
42
+ - `@reactberry/system/charts`
43
+
44
+ ## Notes
45
+
46
+ - `src/index.ts` is the root barrel export.
47
+ - `DesignSystemProvider` includes the styled-components registry for Next.js usage.
48
+ - The package keeps the extracted design-system source as the repo's source of truth.
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@reactberry/system",
3
+ "version": "2.0.0-beta",
4
+ "description": "Reusable Next.js UI library package for Reactberry projects.",
5
+ "type": "module",
6
+ "types": "./src/index.ts",
7
+ "files": [
8
+ "src/**/*.ts",
9
+ "src/**/*.tsx",
10
+ "src/**/*.js",
11
+ "README.md",
12
+ "tsconfig.json"
13
+ ],
14
+ "engines": {
15
+ "node": "20.x",
16
+ "npm": ">=9.0.0"
17
+ },
18
+ "scripts": {
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
20
+ "docs:dev": "npm --prefix docs run dev",
21
+ "docs:build": "npm --prefix docs run build",
22
+ "docs:typecheck": "npm --prefix docs run typecheck"
23
+ },
24
+ "peerDependencies": {
25
+ "next": "^16.0.0",
26
+ "react": "^19.0.0",
27
+ "react-dom": "^19.0.0",
28
+ "styled-components": "^6.1.0"
29
+ },
30
+ "dependencies": {
31
+ "@floating-ui/react": "^0.26.28",
32
+ "@headlessui/react": "^2.2.9",
33
+ "@nivo/bar": "^0.99.0",
34
+ "@nivo/pie": "^0.99.0",
35
+ "@number-flow/react": "^0.5.14",
36
+ "@paper-design/shaders-react": "^0.0.69",
37
+ "chroma-js": "^2.4.2",
38
+ "motion": "^12.35.2",
39
+ "react-dropzone": "^14.4.1",
40
+ "react-number-format": "^5.4.4",
41
+ "react-swipeable": "^7.0.2",
42
+ "react-textarea-autosize": "^8.5.9",
43
+ "react-use-measure": "^2.1.7",
44
+ "styled-system": "^5.1.5"
45
+ },
46
+ "devDependencies": {
47
+ "@types/node": "^20",
48
+ "@types/react": "^19.2.14",
49
+ "@types/react-dom": "^19.2.3",
50
+ "@types/styled-system": "^5.1.22",
51
+ "next": "^16.1.6",
52
+ "react": "^19.2.4",
53
+ "react-dom": "^19.2.4",
54
+ "styled-components": "^6.1.1",
55
+ "typescript": "^5"
56
+ },
57
+ "exports": {
58
+ ".": "./src/index.ts",
59
+ "./providers": "./src/providers/index.ts",
60
+ "./themes": "./src/themes/index.ts",
61
+ "./blocks": "./src/blocks/index.ts",
62
+ "./elements": "./src/elements/index.ts",
63
+ "./icons": "./src/icons/index.tsx",
64
+ "./charts": "./src/charts/index.ts",
65
+ "./package.json": "./package.json"
66
+ },
67
+ "keywords": [
68
+ "reactberry",
69
+ "design-system",
70
+ "nextjs",
71
+ "react",
72
+ "styled-components"
73
+ ]
74
+ }
@@ -0,0 +1,158 @@
1
+ "use client";
2
+ import { Box } from "@/design-system/elements";
3
+ import { AnimatePresence, motion } from "motion/react";
4
+ import React, { useState } from "react";
5
+ import { useLocalStorage } from "@/design-system/hooks/use-local-storage";
6
+
7
+ import { IconArrowSmDown } from "@/design-system/icons";
8
+ import Group from "../Group";
9
+
10
+ interface AccordionItem {
11
+ id: string;
12
+ trigger: React.ReactNode;
13
+ content: React.ReactNode;
14
+ disabled?: boolean;
15
+ }
16
+
17
+ interface AccordionProps {
18
+ items: AccordionItem[];
19
+ allowMultiple?: boolean;
20
+ defaultOpen?: string[];
21
+ onToggle?: (itemId: string, isOpen: boolean, openItems: string[]) => void;
22
+ fontSize?: string;
23
+ spacing?: string;
24
+ persistKey?: string;
25
+ iconPosition?: "start" | "end";
26
+ containerProps?: {
27
+ [key: string]: any;
28
+ };
29
+ itemProps?: {
30
+ [key: string]: any;
31
+ };
32
+ [key: string]: any;
33
+ }
34
+
35
+ const Accordion: React.FC<AccordionProps> = ({
36
+ items,
37
+ allowMultiple = true,
38
+ defaultOpen = [],
39
+ onToggle,
40
+ persistKey,
41
+ iconPosition = "end",
42
+ containerProps = {},
43
+ itemProps = {},
44
+ }) => {
45
+ const [persistedOpenItems, setPersistedOpenItems] = useLocalStorage<string[]>(
46
+ persistKey ? `accordion-${persistKey}` : "",
47
+ defaultOpen
48
+ );
49
+
50
+ const [openItems, setOpenItems] = useState<Set<string>>(() => {
51
+ return new Set(persistKey ? persistedOpenItems : defaultOpen);
52
+ });
53
+
54
+ const handleToggle = (itemId: string) => {
55
+ const item = items.find((item) => item.id === itemId);
56
+ if (item?.disabled) return;
57
+
58
+ setOpenItems((prevOpenItems) => {
59
+ const newOpenItems = new Set(prevOpenItems);
60
+ const isCurrentlyOpen = newOpenItems.has(itemId);
61
+
62
+ if (isCurrentlyOpen) {
63
+ newOpenItems.delete(itemId);
64
+ } else {
65
+ if (!allowMultiple) {
66
+ newOpenItems.clear();
67
+ }
68
+ newOpenItems.add(itemId);
69
+ }
70
+
71
+ const openItemsArray = Array.from(newOpenItems);
72
+ onToggle?.(itemId, !isCurrentlyOpen, openItemsArray);
73
+
74
+ // Persist to localStorage if persistKey is provided
75
+ if (persistKey) {
76
+ setPersistedOpenItems(openItemsArray);
77
+ }
78
+
79
+ return newOpenItems;
80
+ });
81
+ };
82
+
83
+ return (
84
+ <Box
85
+ width="100%"
86
+ display="flex"
87
+ flexDirection={"column"}
88
+ {...containerProps}
89
+ >
90
+ {items.map((item) => {
91
+ const isOpen = openItems.has(item.id);
92
+ const isDisabled = item.disabled;
93
+
94
+ return (
95
+ <Box
96
+ key={item.id}
97
+ as={motion.div}
98
+ opacity={isDisabled ? 0.5 : 1}
99
+ {...itemProps}
100
+ >
101
+ <Box
102
+ width="100%"
103
+ cursor={isDisabled ? "not-allowed" : "pointer"}
104
+ display="flex"
105
+ alignItems="center"
106
+ justifyContent={"space-between"}
107
+ gap="mini"
108
+ disabled={isDisabled}
109
+ onClick={() => handleToggle(item.id)}
110
+ >
111
+ <Group
112
+ as={motion.div}
113
+ justifyContent="center"
114
+ flex="none"
115
+ order={iconPosition === "start" ? 0 : 1}
116
+ animate={{
117
+ rotate: isOpen
118
+ ? iconPosition === "start"
119
+ ? 0
120
+ : 180
121
+ : iconPosition === "start"
122
+ ? -90
123
+ : 0,
124
+ opacity: isOpen ? 1 : 0.5,
125
+ }}
126
+ transition={{ duration: 0.1 }}
127
+ >
128
+ <Box as={IconArrowSmDown} size="1.75em" />
129
+ </Group>
130
+ <Box flex="1" order={iconPosition === "start" ? 1 : 0}>
131
+ {item.trigger}
132
+ </Box>
133
+ </Box>
134
+
135
+ <AnimatePresence mode="sync">
136
+ {isOpen && (
137
+ <Box
138
+ as={motion.div}
139
+ initial={{ height: 0, opacity: 0 }}
140
+ animate={{ height: "auto", opacity: 1 }}
141
+ exit={{ height: 0, opacity: 0 }}
142
+ transition={{
143
+ duration: 0.2,
144
+ ease: "easeInOut",
145
+ }}
146
+ >
147
+ {item.content}
148
+ </Box>
149
+ )}
150
+ </AnimatePresence>
151
+ </Box>
152
+ );
153
+ })}
154
+ </Box>
155
+ );
156
+ };
157
+
158
+ export default Accordion;
@@ -0,0 +1,188 @@
1
+ // carousel.tsx
2
+ "use client";
3
+
4
+ import { Box } from "@/design-system/elements";
5
+ import {
6
+ AnimatePresence,
7
+ MotionConfig,
8
+ motion,
9
+ useMotionTemplate,
10
+ useSpring,
11
+ } from "motion/react";
12
+ import { useEffect, useState } from "react";
13
+ import type { ImageType } from "@/design-system/types";
14
+ import { ControlLeft, ControlRight } from "../Controls/Control";
15
+
16
+ const COLLAPSED_ASPECT_RATIO = 0.5;
17
+ const FULL_ASPECT_RATIO = 3 / 2;
18
+ const MARGIN = 24;
19
+ const GAP = 2;
20
+
21
+ type CarouselProps = {
22
+ images: ImageType[];
23
+ };
24
+
25
+ type ThumbnailsProps = {
26
+ images: ImageType[];
27
+ index: number;
28
+ setIndex: (value: number) => void;
29
+ };
30
+
31
+ export default function AnimatedCarousel({ images }: CarouselProps) {
32
+ const [index, setIndex] = useState(0);
33
+
34
+ const x = index * 100;
35
+ const xSpring = useSpring(x, { bounce: 0 });
36
+ const xPercentage = useMotionTemplate`-${xSpring}%`;
37
+
38
+ useEffect(() => {
39
+ xSpring.set(x);
40
+ }, [x, xSpring]);
41
+
42
+ useEffect(() => {
43
+ function handleKeyPress(e: KeyboardEvent) {
44
+ if (e.key === "ArrowLeft") {
45
+ if (index > 0) {
46
+ setIndex(index - 1);
47
+ }
48
+ } else if (e.key === "ArrowRight") {
49
+ if (index < images.length - 1) {
50
+ setIndex(index + 1);
51
+ }
52
+ }
53
+ }
54
+
55
+ document.addEventListener("keydown", handleKeyPress);
56
+
57
+ return () => {
58
+ document.removeEventListener("keydown", handleKeyPress);
59
+ };
60
+ }, [index, images.length]);
61
+
62
+ return (
63
+ <MotionConfig transition={{ type: "spring", bounce: 0 }}>
64
+ <Box
65
+ display={"flex"}
66
+ flexDirection={"column"}
67
+ justifyContent={"space-between"}
68
+ height={"100%"}
69
+ >
70
+ <Box position="relative" overflow={"hidden"}>
71
+ <Box as={motion.div} style={{ x: xPercentage }} display={"flex"}>
72
+ {images.map((image, i) => (
73
+ <Box
74
+ as={motion.img}
75
+ key={image.id}
76
+ src={image.url}
77
+ animate={{ opacity: i === index ? 1 : 0.4 }}
78
+ aspect={"1/0.85"}
79
+ width="100%"
80
+ height="100vh"
81
+ maxHeight={"70vh"}
82
+ style={{ objectFit: "cover" }}
83
+ flex="none"
84
+ />
85
+ ))}
86
+ </Box>
87
+
88
+ <AnimatePresence initial={false}>
89
+ {index > 0 && (
90
+ <ControlLeft
91
+ initial={{ opacity: 0 }}
92
+ animate={{ opacity: 0.7 }}
93
+ exit={{ opacity: 0, pointerEvents: "none" }}
94
+ whileHover={{ opacity: 1, scale: 1.1 }}
95
+ ml="small"
96
+ onClick={() => setIndex(index - 1)}
97
+ />
98
+ )}
99
+ </AnimatePresence>
100
+
101
+ <AnimatePresence initial={false}>
102
+ {index + 1 < images.length && (
103
+ <ControlRight
104
+ initial={{ opacity: 0 }}
105
+ animate={{ opacity: 0.7 }}
106
+ exit={{ opacity: 0, pointerEvents: "none" }}
107
+ whileHover={{ opacity: 1, scale: 1.1 }}
108
+ mr="small"
109
+ left="auto"
110
+ right="0"
111
+ onClick={() => setIndex(index + 1)}
112
+ />
113
+ )}
114
+ </AnimatePresence>
115
+ </Box>
116
+
117
+ <Thumbnails images={images} index={index} setIndex={setIndex} />
118
+ </Box>
119
+ </MotionConfig>
120
+ );
121
+ }
122
+
123
+ function Thumbnails({ images, index, setIndex }: ThumbnailsProps) {
124
+ const x =
125
+ index * 100 * (COLLAPSED_ASPECT_RATIO / FULL_ASPECT_RATIO) +
126
+ MARGIN +
127
+ index * GAP;
128
+ const xSpring = useSpring(x, { bounce: 0 });
129
+ const xPercentage = useMotionTemplate`-${xSpring}%`;
130
+
131
+ useEffect(() => {
132
+ xSpring.set(x);
133
+ }, [x, xSpring]);
134
+
135
+ return (
136
+ <Box
137
+ display="flex"
138
+ justifyContent={"center"}
139
+ overflow={"hidden"}
140
+ height="3rem"
141
+ >
142
+ <Box
143
+ as={motion.div}
144
+ style={{
145
+ aspectRatio: FULL_ASPECT_RATIO,
146
+ gap: `${GAP}%`,
147
+ x: xPercentage,
148
+ }}
149
+ display="flex"
150
+ minWidth={"0"}
151
+ >
152
+ {images.map((image, i) => (
153
+ <Box
154
+ as={motion.div}
155
+ onClick={() => setIndex(i)}
156
+ initial={false}
157
+ animate={i === index ? "active" : "inactive"}
158
+ cursor="pointer"
159
+ variants={{
160
+ active: {
161
+ aspectRatio: FULL_ASPECT_RATIO,
162
+ marginLeft: `${MARGIN}%`,
163
+ marginRight: `${MARGIN}%`,
164
+ },
165
+ inactive: {
166
+ aspectRatio: COLLAPSED_ASPECT_RATIO,
167
+ marginLeft: 0,
168
+ marginRight: 0,
169
+ },
170
+ }}
171
+ height="100%"
172
+ flex="none"
173
+ key={image.id}
174
+ >
175
+ <Box
176
+ as="img"
177
+ alt=""
178
+ src={image.url}
179
+ width="100%"
180
+ height="100vh"
181
+ style={{ objectFit: "cover" }}
182
+ />
183
+ </Box>
184
+ ))}
185
+ </Box>
186
+ </Box>
187
+ );
188
+ }
@@ -0,0 +1,144 @@
1
+ "use client"
2
+
3
+ import { useEffect, useRef, useCallback } from "react"
4
+ import { motion, AnimatePresence } from "motion/react"
5
+ import Box, { BoxProps } from "@/design-system/elements/box"
6
+ import { useReducedMotion } from "@/design-system/hooks/useReducedMotion"
7
+
8
+ // Default Apple Intelligence colors
9
+ const DEFAULT_COLORS = [
10
+ "#3B82F6", // Blue
11
+ "#A855F7", // Purple
12
+ "#7A84FF", // Red
13
+ "#35B3DF", // Cyan
14
+ ]
15
+
16
+ export type AppleGlowProps = Omit<BoxProps, "children"> & {
17
+ colors?: string[]
18
+ borderRadius?: string | number
19
+ intensity?: "sm" | "md" | "lg" | "xl"
20
+ preview?: boolean // Controls visibility
21
+ blurAmount?: number // Custom blur amount in pixels
22
+ backgroundColor?: string // Background color for the mask
23
+ rotationSpeed?: number // ms per tick (default 50)
24
+ }
25
+
26
+ // Blur intensity mapping
27
+ const BLUR_INTENSITY = {
28
+ sm: 8,
29
+ md: 16,
30
+ lg: 24,
31
+ xl: 32,
32
+ }
33
+
34
+ // Generate linear gradient — unrolled for common color counts to avoid .join()
35
+ function createLinearGradient(colors: string[], angle: number): string {
36
+ switch (colors.length) {
37
+ case 2:
38
+ return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]})`
39
+ case 3:
40
+ return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]}, ${colors[2]})`
41
+ case 4:
42
+ return `linear-gradient(${angle}deg, ${colors[0]}, ${colors[1]}, ${colors[2]}, ${colors[3]})`
43
+ default:
44
+ return `linear-gradient(${angle}deg, ${colors.join(", ")})`
45
+ }
46
+ }
47
+
48
+ export default function AppleGlow({
49
+ colors = DEFAULT_COLORS,
50
+ borderRadius,
51
+ intensity = "xl",
52
+ preview = false,
53
+ blurAmount,
54
+ backgroundColor = "base",
55
+ rotationSpeed = 50,
56
+ ...boxProps
57
+ }: AppleGlowProps) {
58
+ const prefersReducedMotion = useReducedMotion()
59
+ const containerRef = useRef<HTMLDivElement>(null)
60
+ const angleRef = useRef(234.576)
61
+ const rafRef = useRef<number | null>(null)
62
+ const lastTimeRef = useRef(0)
63
+ const blur = blurAmount ?? BLUR_INTENSITY[intensity]
64
+
65
+ const applyFrame = useCallback(() => {
66
+ if (containerRef.current) {
67
+ containerRef.current.style.background = createLinearGradient(
68
+ colors,
69
+ angleRef.current,
70
+ )
71
+ }
72
+ }, [colors])
73
+
74
+ // Rotate gradient angle over time (rAF-driven, frame-synced)
75
+ useEffect(() => {
76
+ if (!preview || prefersReducedMotion) {
77
+ // Show a static gradient when reduced motion is preferred
78
+ if (preview) applyFrame()
79
+ return
80
+ }
81
+
82
+ const tick = (now: number) => {
83
+ const delta = lastTimeRef.current ? now - lastTimeRef.current : 0
84
+ lastTimeRef.current = now
85
+
86
+ if (delta > 0 && rotationSpeed > 0) {
87
+ angleRef.current =
88
+ (angleRef.current + delta / rotationSpeed) % 360
89
+ }
90
+ applyFrame()
91
+ rafRef.current = requestAnimationFrame(tick)
92
+ }
93
+
94
+ lastTimeRef.current = 0
95
+ rafRef.current = requestAnimationFrame(tick)
96
+
97
+ return () => {
98
+ if (rafRef.current != null) {
99
+ cancelAnimationFrame(rafRef.current)
100
+ rafRef.current = null
101
+ }
102
+ }
103
+ }, [preview, rotationSpeed, applyFrame, prefersReducedMotion])
104
+
105
+ return (
106
+ <AnimatePresence>
107
+ {preview && (
108
+ <Box
109
+ as={motion.div}
110
+ ref={containerRef}
111
+ position="absolute"
112
+ top={0}
113
+ left={0}
114
+ right={0}
115
+ bottom={0}
116
+ initial={{ opacity: 0 }}
117
+ animate={{ opacity: 1 }}
118
+ exit={{ opacity: 0 }}
119
+ transition={{ duration: 0.3 }}
120
+ style={{
121
+ pointerEvents: "none",
122
+ borderRadius,
123
+ willChange: "background",
124
+ }}
125
+ {...boxProps}
126
+ >
127
+ {/* Blurred inset mask - the blur on the edges reveals the gradient */}
128
+ <Box
129
+ position="absolute"
130
+ top="2px"
131
+ left="2px"
132
+ right="2px"
133
+ bottom="2px"
134
+ bg={backgroundColor}
135
+ style={{
136
+ filter: `blur(${blur}px)`,
137
+ borderRadius: borderRadius ? `inherit` : undefined,
138
+ }}
139
+ />
140
+ </Box>
141
+ )}
142
+ </AnimatePresence>
143
+ )
144
+ }