@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
@@ -0,0 +1,140 @@
1
+ "use client";
2
+ import { motion, type PanInfo } from "motion/react";
3
+ import { ReactNode, type WheelEvent } from "react";
4
+ import useSlider, { ResponsiveConfig, SliderConfig } from "./useSlider";
5
+ import { Box } from "@/design-system/elements";
6
+ import SliderControls from "./SliderControls";
7
+
8
+ interface Props {
9
+ config?: SliderConfig;
10
+ responsiveConfig?: ResponsiveConfig[];
11
+ children: ReactNode[];
12
+ }
13
+
14
+ const Slider = ({ config, responsiveConfig = [], children }: Props) => {
15
+ const {
16
+ currentSlide,
17
+ currentPage,
18
+ canGoNext,
19
+ canGoPrev,
20
+ scrollTo,
21
+ settings,
22
+ setCurrentPage,
23
+ nextSlide,
24
+ prevSlide,
25
+ setScrollTo,
26
+ slideWidth,
27
+ totalPages,
28
+ } = useSlider(children, config, responsiveConfig);
29
+
30
+ const showControls = totalPages > 1;
31
+
32
+ return (
33
+ <Box
34
+ width="100%"
35
+ position={"relative"}
36
+ overflow={"hidden"}
37
+ display={"flex"}
38
+ flexDirection={"column"}
39
+ px={`${settings.paddingX ?? 0}px`}
40
+ py={`${settings.paddingY ?? 0}px`}
41
+ >
42
+ <Box
43
+ as={motion.div}
44
+ initial={{ x: 0 }}
45
+ animate={{
46
+ x: `-${(100 / children.length) * currentSlide}%`,
47
+ width: `${slideWidth < 100 ? 100 : slideWidth}%`,
48
+ }}
49
+ transition={{
50
+ type: "spring",
51
+ visualDuration: 0.3,
52
+ bounce: 0.2,
53
+ }}
54
+ >
55
+ <Box
56
+ as={motion.div}
57
+ display={"flex"}
58
+ justifyContent={
59
+ children.length < settings.slidesToShow ? "flex-start" : "initial"
60
+ }
61
+ mr={`-${settings.spacing}px`}
62
+ ml={`-${settings.spacing}px`}
63
+ onWheel={(e: WheelEvent<HTMLDivElement>) => {
64
+ if (e.deltaX < -1) {
65
+ if (scrollTo) return;
66
+ prevSlide();
67
+ setScrollTo(true);
68
+ } else if (e.deltaX > 1) {
69
+ if (scrollTo) return;
70
+ nextSlide();
71
+ setScrollTo(true);
72
+ } else {
73
+ setScrollTo(false);
74
+ }
75
+ }}
76
+ drag="x"
77
+ dragConstraints={{ left: 0, right: 0 }}
78
+ onDragEnd={(
79
+ _event: MouseEvent | TouchEvent | PointerEvent,
80
+ { offset, velocity }: PanInfo,
81
+ ) => {
82
+ const swipe = Math.abs(offset.x) > 50 && Math.abs(velocity.x) > 500;
83
+ if (swipe && offset.x > 0) prevSlide();
84
+ else if (swipe) nextSlide();
85
+ }}
86
+ >
87
+ {children.map((child, index) => {
88
+ // Calculate item width - use slidesToShow if we have enough items, otherwise use actual count
89
+ const itemsToShow = Math.min(
90
+ children.length,
91
+ settings.slidesToShow,
92
+ );
93
+ const calculatedWidth = 100 / itemsToShow;
94
+
95
+ // If maxItemWidth is set and we have fewer items than slidesToShow,
96
+ // respect the maxItemWidth to prevent stretching
97
+ const shouldRespectMaxWidth =
98
+ children.length < settings.slidesToShow && settings.maxItemWidth;
99
+
100
+ return (
101
+ <Box
102
+ key={index}
103
+ pl={`${settings.spacing}px`}
104
+ pr={`${settings.spacing}px`}
105
+ width={
106
+ shouldRespectMaxWidth ? undefined : `${calculatedWidth}%`
107
+ }
108
+ maxWidth={
109
+ shouldRespectMaxWidth
110
+ ? `${settings.maxItemWidth}px`
111
+ : undefined
112
+ }
113
+ flex={shouldRespectMaxWidth ? "0 0 auto" : undefined}
114
+ >
115
+ {child}
116
+ </Box>
117
+ );
118
+ })}
119
+ </Box>
120
+ </Box>
121
+
122
+ {showControls && (
123
+ <SliderControls
124
+ canGoPrev={canGoPrev}
125
+ canGoNext={canGoNext}
126
+ onPrev={prevSlide}
127
+ onNext={nextSlide}
128
+ slides={totalPages}
129
+ onChange={setCurrentPage}
130
+ activeSlide={currentPage}
131
+ showDots={settings.dots}
132
+ showArrows={settings.arrows}
133
+ paddingX={settings.paddingX}
134
+ />
135
+ )}
136
+ </Box>
137
+ );
138
+ };
139
+
140
+ export default Slider;
@@ -0,0 +1,126 @@
1
+ import { useState, useEffect } from "react";
2
+
3
+ export interface SliderConfig {
4
+ slidesToShow?: number;
5
+ slidesToScroll?: number;
6
+ infinite?: boolean;
7
+ dots?: boolean;
8
+ arrows?: boolean;
9
+ spacing?: number;
10
+ paddingX?: number;
11
+ paddingY?: number;
12
+ maxItemWidth?: number;
13
+ }
14
+
15
+ export interface ResponsiveConfig {
16
+ breakpoint: number;
17
+ settings: SliderConfig;
18
+ }
19
+
20
+ const useSlider = (
21
+ data: any[],
22
+ config: SliderConfig = {},
23
+ responsiveConfig: ResponsiveConfig[] = [],
24
+ ) => {
25
+ const [currentPage, setCurrentPage] = useState(0);
26
+ const [canGoNext, setCanGoNext] = useState(false);
27
+ const [canGoPrev, setCanGoPrev] = useState(false);
28
+ const [scrollTo, setScrollTo] = useState<boolean>(false);
29
+ const [resized, setResized] = useState<boolean>(false);
30
+
31
+ const [settings, setSettings] = useState({
32
+ slidesToShow: 1,
33
+ slidesToScroll: 1,
34
+ infinite: false,
35
+ dots: true,
36
+ arrows: true,
37
+ spacing: 0,
38
+ paddingX: 0,
39
+ paddingY: 0,
40
+ maxItemWidth: 300,
41
+ responsive: [],
42
+ ...config,
43
+ });
44
+
45
+ const totalPages = Math.max(
46
+ 1,
47
+ Math.ceil(data.length / settings.slidesToShow),
48
+ );
49
+
50
+ const nextSlide = () => {
51
+ if (!canGoNext) return;
52
+ setCurrentPage((prev) => Math.min(prev + 1, totalPages - 1));
53
+ };
54
+
55
+ const prevSlide = () => {
56
+ if (!canGoPrev) return;
57
+ setCurrentPage((prev) => Math.max(prev - 1, 0));
58
+ };
59
+
60
+ useEffect(() => {
61
+ if (settings.infinite) {
62
+ setCanGoNext(true);
63
+ setCanGoPrev(true);
64
+ } else {
65
+ setCanGoNext(currentPage < totalPages - 1);
66
+ setCanGoPrev(currentPage > 0);
67
+ }
68
+ }, [
69
+ settings.infinite,
70
+ settings.slidesToShow,
71
+ currentPage,
72
+ data.length,
73
+ totalPages,
74
+ ]);
75
+
76
+ const currentSlide = currentPage * settings.slidesToShow;
77
+ const slideWidth = (100 * data.length) / settings.slidesToShow;
78
+
79
+ const handleResize = () => {
80
+ const width = window.innerWidth;
81
+ if (responsiveConfig.length === 0) return;
82
+
83
+ const responsiveSettings = responsiveConfig.find(
84
+ (item) => item.breakpoint < width,
85
+ );
86
+
87
+ if (responsiveSettings) {
88
+ setSettings({
89
+ ...settings,
90
+ ...responsiveSettings.settings,
91
+ });
92
+ }
93
+ };
94
+
95
+ useEffect(() => {
96
+ if (!resized) {
97
+ handleResize();
98
+ setResized(true);
99
+ return;
100
+ }
101
+ }, [resized]);
102
+
103
+ useEffect(() => {
104
+ window.addEventListener("resize", handleResize);
105
+ return () => {
106
+ window.removeEventListener("resize", handleResize);
107
+ };
108
+ });
109
+
110
+ return {
111
+ currentSlide,
112
+ currentPage,
113
+ canGoNext,
114
+ canGoPrev,
115
+ scrollTo,
116
+ settings,
117
+ slideWidth,
118
+ totalPages,
119
+ setScrollTo,
120
+ setCurrentPage,
121
+ nextSlide,
122
+ prevSlide,
123
+ };
124
+ };
125
+
126
+ export default useSlider;
@@ -0,0 +1,177 @@
1
+ "use client";
2
+ import { Box } from "@/design-system/elements";
3
+ import type { ImageType } from "@/design-system/types";
4
+ import { motion, AnimatePresence } from "motion/react";
5
+ import { useState, useEffect } from "react";
6
+
7
+ export const wrap = (min: number, max: number, v: number) => {
8
+ const rangeSize = max - min;
9
+ return ((((v - min) % rangeSize) + rangeSize) % rangeSize) + min;
10
+ };
11
+
12
+ type SlideshowProps = {
13
+ photos: ImageType[];
14
+ showProgress?: boolean;
15
+ duration?: number;
16
+ fade?: boolean;
17
+ };
18
+
19
+ const imageVariants = {
20
+ enter: { opacity: 0, scale: 1.04, y: 10, transition: { duration: 0.6 } },
21
+ center: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.6 } },
22
+ exit: { opacity: 0, scale: 1.04, y: -10, transition: { duration: 0.6 } },
23
+ };
24
+
25
+ const controlVariants = {
26
+ enter: { opacity: 0.2, width: ".5rem" },
27
+ center: { opacity: 0.7, width: "2rem" },
28
+ exit: { opacity: 0.1, width: ".5rem" },
29
+ hover: { opacity: 1, scale: 1.1 },
30
+ };
31
+
32
+ export function Slideshow({
33
+ photos,
34
+ showProgress,
35
+ duration = 3,
36
+ fade = true,
37
+ }: SlideshowProps) {
38
+ const [page, setPage] = useState(0);
39
+ const [autoplayKey, setAutoplayKey] = useState(0);
40
+ const [mounted, setMounted] = useState(false);
41
+ const [instanceId] = useState(() => Math.random().toString(36).substr(2, 9)); // Prevents SSR mismatches
42
+
43
+ useEffect(() => setMounted(true), []);
44
+
45
+ useEffect(() => {
46
+ if (photos.length > 1) {
47
+ const timer = setTimeout(
48
+ () => setPage((prev) => (prev + 1) % photos.length),
49
+ duration * 1000
50
+ );
51
+ return () => clearTimeout(timer);
52
+ }
53
+ }, [page, photos, duration, autoplayKey]);
54
+
55
+ if (!photos || photos.length === 0) {
56
+ return (
57
+ <Box
58
+ width="100%"
59
+ height="100%"
60
+ bg="base"
61
+ shape="rounded"
62
+ display="flex"
63
+ alignItems="center"
64
+ justifyContent="center"
65
+ >
66
+ No images available
67
+ </Box>
68
+ );
69
+ }
70
+
71
+ if (photos.length === 1) {
72
+ return (
73
+ <Box
74
+ as={motion.div}
75
+ variants={imageVariants}
76
+ initial="enter"
77
+ animate="center"
78
+ exit="exit"
79
+ whileHover="initial"
80
+ width="100%"
81
+ height="100%"
82
+ position="absolute"
83
+ maxWidth="100%"
84
+ top="0"
85
+ left="0"
86
+ shape="rounded"
87
+ zIndex={1}
88
+ background={`url(${photos[0].url}) no-repeat center center / cover`}
89
+ />
90
+ );
91
+ }
92
+
93
+ const results = photos;
94
+ const dataIndex = wrap(0, results.length, page);
95
+
96
+ return (
97
+ <Box
98
+ as={motion.div}
99
+ overflow="hidden"
100
+ position="relative"
101
+ width="100%"
102
+ height="100%"
103
+ bg="base"
104
+ shape="rounded"
105
+ >
106
+ {fade && (
107
+ <Box
108
+ as={motion.div}
109
+ position="absolute"
110
+ width="100%"
111
+ height="100%"
112
+ top="0"
113
+ zIndex={2}
114
+ backgroundImage="linear-gradient(to bottom, transparent, currentColor 100%)"
115
+ />
116
+ )}
117
+
118
+ <AnimatePresence>
119
+ {mounted && showProgress && (
120
+ <Box
121
+ position="absolute"
122
+ width="100%"
123
+ bottom="0"
124
+ left="0"
125
+ zIndex={2}
126
+ display="flex"
127
+ alignItems="center"
128
+ justifyContent="center"
129
+ gap="xsmall"
130
+ p="large"
131
+ backgroundImage="linear-gradient(to bottom, rgba(0,0,0,0), rgba(0,0,0,0.6))"
132
+ >
133
+ {results.map((_, i) => (
134
+ <Box
135
+ as={motion.div}
136
+ key={i}
137
+ variants={controlVariants}
138
+ initial="enter"
139
+ animate={i === dataIndex ? "center" : "none"}
140
+ whileHover="hover"
141
+ exit="exit"
142
+ flex="none"
143
+ size="0.5rem"
144
+ bg="white"
145
+ shape="rounded"
146
+ cursor="pointer"
147
+ onClick={() => {
148
+ setPage(i);
149
+ setAutoplayKey((prev) => prev + 1);
150
+ }}
151
+ />
152
+ ))}
153
+ </Box>
154
+ )}
155
+ <Box
156
+ as={motion.div}
157
+ key={`slideshow-${page}-${instanceId}`}
158
+ variants={imageVariants}
159
+ initial="enter"
160
+ animate="center"
161
+ exit="exit"
162
+ whileHover="initial"
163
+ width="100%"
164
+ height="100%"
165
+ position="absolute"
166
+ maxWidth="100%"
167
+ top="0"
168
+ left="0"
169
+ zIndex={1}
170
+ background={`url(${
171
+ results[dataIndex].url || results[0].url
172
+ }) no-repeat center center / cover`}
173
+ />
174
+ </AnimatePresence>
175
+ </Box>
176
+ );
177
+ }
@@ -0,0 +1,144 @@
1
+ "use client";
2
+ import { useRef, useState } from "react";
3
+ import {
4
+ motion,
5
+ useMotionTemplate,
6
+ useMotionValue,
7
+ useSpring,
8
+ } from "motion/react";
9
+ import Box, { BoxProps } from "@/design-system/elements/box";
10
+
11
+ interface SpotlightProps extends Omit<BoxProps, "children"> {
12
+ children: React.ReactNode;
13
+ aspect?: number | number[] | string[] | {};
14
+ spotlightColor?: string;
15
+ }
16
+
17
+ export default function Spotlight({
18
+ children,
19
+ aspect = "4/5",
20
+ spotlightColor = "transparent.brand.1",
21
+ ...props
22
+ }: SpotlightProps) {
23
+ const ref = useRef<HTMLDivElement>(null);
24
+ const [isHovered, setIsHovered] = useState(false);
25
+
26
+ // Spotlight values with springs for smoother animation
27
+ const spotlightX = useSpring(useMotionValue(0));
28
+ const spotlightY = useSpring(useMotionValue(0));
29
+
30
+ // Tilt values with adjusted spring configuration
31
+ const tiltX = useMotionValue(0);
32
+ const tiltY = useMotionValue(0);
33
+ const tiltXSpring = useSpring(tiltX, {
34
+ stiffness: 300,
35
+ damping: 30,
36
+ });
37
+ const tiltYSpring = useSpring(tiltY, {
38
+ stiffness: 300,
39
+ damping: 30,
40
+ });
41
+
42
+ const transform = useMotionTemplate`perspective(1000px) rotateX(${tiltXSpring}deg) rotateY(${tiltYSpring}deg)`;
43
+
44
+ function handleMouseMove(e: React.MouseEvent<HTMLDivElement>) {
45
+ if (!ref.current) return;
46
+
47
+ const rect = ref.current.getBoundingClientRect();
48
+
49
+ // Calculate mouse position relative to card center
50
+ const centerX = rect.left + rect.width / 2;
51
+ const centerY = rect.top + rect.height / 2;
52
+ const mouseX = e.clientX - centerX;
53
+ const mouseY = e.clientY - centerY;
54
+
55
+ // Update spotlight position
56
+ spotlightX.set(e.clientX - rect.left);
57
+ spotlightY.set(e.clientY - rect.top);
58
+
59
+ // Calculate rotation (reduced range for subtler effect)
60
+ const rotateX = (mouseY / (rect.height / 2)) * -5;
61
+ const rotateY = (mouseX / (rect.width / 2)) * 5;
62
+
63
+ tiltX.set(rotateX);
64
+ tiltY.set(rotateY);
65
+ }
66
+
67
+ function handleMouseEnter() {
68
+ setIsHovered(true);
69
+ }
70
+
71
+ function handleMouseLeave() {
72
+ setIsHovered(false);
73
+ // Reset tilt with animation
74
+ tiltX.set(0);
75
+ tiltY.set(0);
76
+ }
77
+
78
+ return (
79
+ <Box
80
+ as={motion.div}
81
+ ref={ref}
82
+ onMouseMove={handleMouseMove}
83
+ onMouseEnter={handleMouseEnter}
84
+ onMouseLeave={handleMouseLeave}
85
+ position={"relative"}
86
+ style={{
87
+ transformStyle: "preserve-3d",
88
+ transform,
89
+ transition: "all 0.2s ease-out",
90
+ transformOrigin: "center center",
91
+ aspectRatio: aspect,
92
+ }}
93
+ border="1px solid"
94
+ skin="card"
95
+ shape="rounded"
96
+ borderColor="transparent.light.2"
97
+ p="medium"
98
+ $shadow="medium"
99
+ overflow="hidden"
100
+ {...props}
101
+ >
102
+ {/* Spotlight effect */}
103
+ <Box
104
+ as={motion.div}
105
+ position={"absolute"}
106
+ width="100%"
107
+ height="100%"
108
+ top="0"
109
+ left="0"
110
+ opacity={isHovered ? 1 : 0}
111
+ zIndex={0}
112
+ color={spotlightColor}
113
+ style={{
114
+ pointerEvents: "none",
115
+ transition: "opacity 0.2s ease-out",
116
+ background: useMotionTemplate`
117
+ radial-gradient(
118
+ 300px circle at ${spotlightX}px ${spotlightY}px,
119
+ currentColor,
120
+ transparent 80%
121
+ )
122
+ `,
123
+ }}
124
+ />
125
+
126
+ {/* Content */}
127
+ <Box
128
+ as={motion.div}
129
+ position={"relative"}
130
+ height={"100%"}
131
+ width={"100%"}
132
+ zIndex={1}
133
+ style={{
134
+ transform: isHovered ? "translateZ(48px)" : "translateZ(0px)",
135
+ scale: isHovered ? 1.04 : 1,
136
+ transition: "transform 0.2s ease-out, scale 0.2s ease-out",
137
+ transformStyle: "preserve-3d",
138
+ }}
139
+ >
140
+ {children}
141
+ </Box>
142
+ </Box>
143
+ );
144
+ }