@okinoxis/hero-scene 0.2.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.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @okinoxis/hero-scene
2
+
3
+ Compound component for cinematic hero backgrounds in Next.js — image rotation, parallax, vignettes, blur, and patterns.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @okinoxis/hero-scene
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```tsx
14
+ import { HeroScene } from '@okinoxis/hero-scene'
15
+
16
+ const images = [
17
+ { src: '/karate.jpg', color: '70, 150, 220' },
18
+ { src: '/surf.jpg', color: '220, 160, 60' },
19
+ { src: '/tennis.jpg', color: '200, 110, 50' },
20
+ ]
21
+
22
+ <HeroScene images={images} interval={30000} className="min-h-svh">
23
+ <HeroScene.Parallax />
24
+ <HeroScene.Vignette />
25
+ <HeroScene.Blur />
26
+ <HeroScene.Pattern />
27
+ <HeroScene.DarkOverlay />
28
+ <HeroScene.Content>
29
+ <h1>Your headline</h1>
30
+ </HeroScene.Content>
31
+ </HeroScene>
32
+ ```
33
+
34
+ Only include the sub-components you need. All are optional.
35
+
36
+ ## Layers
37
+
38
+ ```
39
+ Background z:0 — images (crossfade) + fallback color, moved by Parallax
40
+ DarkOverlay z:10 — dark mode only overlay
41
+ Blur z:15 — backdrop blur with radial mask (clear center)
42
+ Vignette z:20 — color-matched radial gradient per image
43
+ Pattern z:20 — repeating dot grid
44
+ Content z:30 — your content
45
+ ```
46
+
47
+ ## API
48
+
49
+ ### `<HeroScene>`
50
+
51
+ | Prop | Type | Default |
52
+ |------|------|---------|
53
+ | `images` * | `{ src: string, color: string }[]` | — |
54
+ | `initialIndex` | `number` | `0` |
55
+ | `interval` | `number` | `30000` |
56
+ | `transitionDuration` | `number` | `700` |
57
+ | `className` | `string` | — |
58
+ | `onIndexChange` | `(index: number) => void` | — |
59
+
60
+ `color` is `"r, g, b"` (e.g. `"210, 100, 60"`) for CSS `color-mix()`.
61
+
62
+ ### `<HeroScene.Parallax>`
63
+
64
+ | Prop | Type | Default |
65
+ |------|------|---------|
66
+ | `speed` | `number` | `0.4` |
67
+ | `mouseShiftX` | `number` | `25` |
68
+ | `mouseShiftY` | `number` | `15` |
69
+ | `mouseLerp` | `number` | `0.04` |
70
+
71
+ ### `<HeroScene.Vignette>`
72
+
73
+ | Prop | Type | Default |
74
+ |------|------|---------|
75
+ | `centerX` | `number` | `50` |
76
+ | `centerY` | `number` | `100` |
77
+ | `shape` | `'circle' \| 'ellipse'` | `'circle'` |
78
+ | `stops` | `[number, number][]` | `[[0,0]...[100,0.6]]` |
79
+ | `transitionDuration` | `number` | `1000` |
80
+
81
+ ### `<HeroScene.Blur>`
82
+
83
+ | Prop | Type | Default |
84
+ |------|------|---------|
85
+ | `amount` | `number` | `24` |
86
+ | `centerX` | `number` | `50` |
87
+ | `centerY` | `number` | `65` |
88
+ | `innerRadius` | `number` | `15` |
89
+ | `outerRadius` | `number` | `55` |
90
+
91
+ ### `<HeroScene.Pattern>`
92
+
93
+ | Prop | Type | Default |
94
+ |------|------|---------|
95
+ | `dotSize` | `number` | `1` |
96
+ | `spacing` | `number` | `20` |
97
+ | `lightColor` | `string` | `'rgba(0 0 0 / 0.15)'` |
98
+ | `darkColor` | `string` | `'rgba(255 255 255 / 0.1)'` |
99
+
100
+ ### `<HeroScene.DarkOverlay>`
101
+
102
+ | Prop | Type | Default |
103
+ |------|------|---------|
104
+ | `opacity` | `number` | `0.4` |
105
+
106
+ ### `<HeroScene.Content>`
107
+
108
+ | Prop | Type | Default |
109
+ |------|------|---------|
110
+ | `className` | `string` | — |
111
+
112
+ ## Accessibility
113
+
114
+ - Respects `prefers-reduced-motion` — disables all animations
115
+ - Pauses off-screen via Intersection Observer
116
+ - All decorative layers are `aria-hidden`
117
+
118
+ ## Requirements
119
+
120
+ React 18+, Next.js 14+, Tailwind CSS (for `dark:` variant)
121
+
122
+ ## License
123
+
124
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,424 @@
1
+ "use client";
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __export = (target, all) => {
10
+ for (var name in all)
11
+ __defProp(target, name, { get: all[name], enumerable: true });
12
+ };
13
+ var __copyProps = (to, from, except, desc) => {
14
+ if (from && typeof from === "object" || typeof from === "function") {
15
+ for (let key of __getOwnPropNames(from))
16
+ if (!__hasOwnProp.call(to, key) && key !== except)
17
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
18
+ }
19
+ return to;
20
+ };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
29
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
+
31
+ // src/index.ts
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
+ HeroScene: () => HeroScene,
35
+ buildBlurMask: () => buildBlurMask,
36
+ buildVignetteGradient: () => buildVignetteGradient,
37
+ useReducedMotion: () => useReducedMotion
38
+ });
39
+ module.exports = __toCommonJS(index_exports);
40
+
41
+ // src/hero-scene.tsx
42
+ var import_image = __toESM(require("next/image"), 1);
43
+ var import_react4 = require("react");
44
+
45
+ // src/hero-scene-context.ts
46
+ var import_react = require("react");
47
+ var HeroSceneContext = (0, import_react.createContext)(null);
48
+ function useHeroScene() {
49
+ const ctx = (0, import_react.useContext)(HeroSceneContext);
50
+ if (!ctx) {
51
+ throw new Error(
52
+ "HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) must be rendered inside a <HeroScene> parent."
53
+ );
54
+ }
55
+ return ctx;
56
+ }
57
+
58
+ // src/utils.ts
59
+ var DEFAULT_STOPS = [
60
+ [0, 0],
61
+ [15, 0.08],
62
+ [30, 0.15],
63
+ [45, 0.25],
64
+ [60, 0.35],
65
+ [75, 0.45],
66
+ [88, 0.55],
67
+ [100, 0.6]
68
+ ];
69
+ function buildVignetteGradient(color, config) {
70
+ const shape = config.shape ?? "circle";
71
+ const cx = config.centerX ?? 50;
72
+ const cy = config.centerY ?? 100;
73
+ const stops = config.stops ?? DEFAULT_STOPS;
74
+ const gradientStops = stops.map(
75
+ ([pos, opacity]) => opacity === 0 ? `transparent ${pos}%` : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`
76
+ ).join(", ");
77
+ return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`;
78
+ }
79
+ function buildBlurMask(config) {
80
+ const cx = config.centerX ?? 50;
81
+ const cy = config.centerY ?? 65;
82
+ const inner = config.innerRadius ?? 15;
83
+ const outer = config.outerRadius ?? 55;
84
+ return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`;
85
+ }
86
+
87
+ // src/use-in-viewport.ts
88
+ var import_react2 = require("react");
89
+ function useInViewport(ref) {
90
+ const [inViewport, setInViewport] = (0, import_react2.useState)(true);
91
+ (0, import_react2.useEffect)(() => {
92
+ const el = ref.current;
93
+ if (!el) return;
94
+ const observer = new IntersectionObserver(
95
+ ([entry]) => {
96
+ setInViewport(entry.isIntersecting);
97
+ },
98
+ { threshold: 0 }
99
+ );
100
+ observer.observe(el);
101
+ return () => observer.disconnect();
102
+ }, [ref]);
103
+ return inViewport;
104
+ }
105
+
106
+ // src/use-reduced-motion.ts
107
+ var import_react3 = require("react");
108
+ function useReducedMotion() {
109
+ const [reduced, setReduced] = (0, import_react3.useState)(false);
110
+ (0, import_react3.useEffect)(() => {
111
+ const mql = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
112
+ setReduced(mql.matches);
113
+ function onChange(e) {
114
+ setReduced(e.matches);
115
+ }
116
+ mql.addEventListener("change", onChange);
117
+ return () => mql.removeEventListener("change", onChange);
118
+ }, []);
119
+ return reduced;
120
+ }
121
+
122
+ // src/hero-scene.tsx
123
+ var import_jsx_runtime = require("react/jsx-runtime");
124
+ function HeroSceneRoot({
125
+ images,
126
+ initialIndex = 0,
127
+ interval = 3e4,
128
+ transitionDuration = 700,
129
+ className,
130
+ onIndexChange,
131
+ children
132
+ }) {
133
+ const [index, setIndex] = (0, import_react4.useState)(initialIndex);
134
+ const rootRef = (0, import_react4.useRef)(null);
135
+ const reducedMotion = useReducedMotion();
136
+ const isInViewport = useInViewport(rootRef);
137
+ (0, import_react4.useEffect)(() => {
138
+ if (interval <= 0 || images.length <= 1) return;
139
+ let current = initialIndex;
140
+ const id = setInterval(() => {
141
+ const next = (current + 1) % images.length;
142
+ current = next;
143
+ setIndex(next);
144
+ setTimeout(() => {
145
+ onIndexChange?.(next);
146
+ }, 0);
147
+ }, interval);
148
+ return () => clearInterval(id);
149
+ }, [initialIndex, interval, images.length, onIndexChange]);
150
+ const activeColor = images[index]?.color ?? "128, 128, 128";
151
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
152
+ HeroSceneContext.Provider,
153
+ {
154
+ value: {
155
+ images,
156
+ index,
157
+ transitionDuration,
158
+ reducedMotion,
159
+ isInViewport,
160
+ containerRef: rootRef
161
+ },
162
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
163
+ "div",
164
+ {
165
+ ref: rootRef,
166
+ className,
167
+ style: { position: "relative", overflow: "hidden" },
168
+ children: [
169
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
170
+ "div",
171
+ {
172
+ "data-hero-images": "",
173
+ "aria-hidden": "true",
174
+ style: {
175
+ position: "absolute",
176
+ inset: 0,
177
+ backgroundColor: `rgb(${activeColor})`,
178
+ transition: reducedMotion ? "none" : "background-color 1s ease"
179
+ },
180
+ children: images.map((img, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
181
+ import_image.default,
182
+ {
183
+ src: img.src,
184
+ alt: "",
185
+ width: 1920,
186
+ height: 1080,
187
+ priority: i === initialIndex,
188
+ sizes: "100vw",
189
+ style: {
190
+ position: "absolute",
191
+ inset: 0,
192
+ width: "100%",
193
+ height: "100%",
194
+ objectFit: "cover",
195
+ opacity: i === index ? 1 : 0,
196
+ transition: reducedMotion ? "none" : `opacity ${transitionDuration}ms ease`
197
+ }
198
+ },
199
+ img.src
200
+ ))
201
+ }
202
+ ),
203
+ children
204
+ ]
205
+ }
206
+ )
207
+ }
208
+ );
209
+ }
210
+ function Parallax({
211
+ speed = 0.4,
212
+ mouseShiftX = 25,
213
+ mouseShiftY = 15,
214
+ mouseLerp = 0.04
215
+ }) {
216
+ const { reducedMotion, isInViewport, containerRef } = useHeroScene();
217
+ const scrollY = (0, import_react4.useRef)(0);
218
+ const targetMouseX = (0, import_react4.useRef)(0);
219
+ const targetMouseY = (0, import_react4.useRef)(0);
220
+ const currentMouseX = (0, import_react4.useRef)(0);
221
+ const currentMouseY = (0, import_react4.useRef)(0);
222
+ (0, import_react4.useEffect)(() => {
223
+ if (reducedMotion) return;
224
+ const root = containerRef.current;
225
+ if (!root) return;
226
+ const imagesEl = root.querySelector("[data-hero-images]");
227
+ if (!imagesEl) return;
228
+ imagesEl.style.inset = "-15%";
229
+ imagesEl.style.willChange = "transform";
230
+ return () => {
231
+ imagesEl.style.inset = "0";
232
+ imagesEl.style.willChange = "";
233
+ imagesEl.style.transform = "";
234
+ };
235
+ }, [reducedMotion, containerRef]);
236
+ (0, import_react4.useEffect)(() => {
237
+ if (reducedMotion) return;
238
+ const root = containerRef.current;
239
+ if (!root) return;
240
+ const imagesEl = root.querySelector("[data-hero-images]");
241
+ if (!imagesEl) return;
242
+ let rafId;
243
+ let running = true;
244
+ function loop() {
245
+ if (!running) return;
246
+ currentMouseX.current += (targetMouseX.current - currentMouseX.current) * mouseLerp;
247
+ currentMouseY.current += (targetMouseY.current - currentMouseY.current) * mouseLerp;
248
+ const y = scrollY.current * speed;
249
+ const mx = currentMouseX.current * mouseShiftX;
250
+ const my = currentMouseY.current * mouseShiftY;
251
+ imagesEl.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`;
252
+ rafId = requestAnimationFrame(loop);
253
+ }
254
+ function onScroll() {
255
+ if (!isInViewport) return;
256
+ scrollY.current = globalThis.scrollY;
257
+ }
258
+ function onMouseMove(e) {
259
+ if (!isInViewport) return;
260
+ targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2;
261
+ targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2;
262
+ }
263
+ globalThis.addEventListener("scroll", onScroll, { passive: true });
264
+ globalThis.addEventListener("mousemove", onMouseMove, { passive: true });
265
+ rafId = requestAnimationFrame(loop);
266
+ return () => {
267
+ running = false;
268
+ globalThis.removeEventListener("scroll", onScroll);
269
+ globalThis.removeEventListener("mousemove", onMouseMove);
270
+ cancelAnimationFrame(rafId);
271
+ };
272
+ }, [
273
+ reducedMotion,
274
+ isInViewport,
275
+ containerRef,
276
+ speed,
277
+ mouseShiftX,
278
+ mouseShiftY,
279
+ mouseLerp
280
+ ]);
281
+ return null;
282
+ }
283
+ function Vignette({
284
+ centerX = 50,
285
+ centerY = 100,
286
+ shape = "circle",
287
+ stops,
288
+ transitionDuration = 1e3
289
+ }) {
290
+ const { images, index, reducedMotion } = useHeroScene();
291
+ const transitionMs = reducedMotion ? "0ms" : `${transitionDuration}ms`;
292
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: images.map((img, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
293
+ "div",
294
+ {
295
+ "aria-hidden": "true",
296
+ style: {
297
+ position: "absolute",
298
+ inset: 0,
299
+ zIndex: 20,
300
+ pointerEvents: "none",
301
+ opacity: i === index ? 1 : 0,
302
+ transition: `opacity ${transitionMs} ease`,
303
+ background: buildVignetteGradient(img.color, {
304
+ centerX,
305
+ centerY,
306
+ shape,
307
+ stops
308
+ })
309
+ }
310
+ },
311
+ `vignette-${img.color}`
312
+ )) });
313
+ }
314
+ function Blur({
315
+ amount = 24,
316
+ centerX = 50,
317
+ centerY = 65,
318
+ innerRadius = 15,
319
+ outerRadius = 55
320
+ }) {
321
+ useHeroScene();
322
+ const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius });
323
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
324
+ "div",
325
+ {
326
+ "aria-hidden": "true",
327
+ style: {
328
+ position: "absolute",
329
+ inset: 0,
330
+ zIndex: 15,
331
+ backdropFilter: `blur(${amount}px)`,
332
+ WebkitBackdropFilter: `blur(${amount}px)`,
333
+ maskImage: mask,
334
+ WebkitMaskImage: mask,
335
+ pointerEvents: "none"
336
+ }
337
+ }
338
+ );
339
+ }
340
+ function Pattern({
341
+ dotSize = 1,
342
+ spacing = 20,
343
+ lightColor = "rgba(0 0 0 / 0.15)",
344
+ darkColor = "rgba(255 255 255 / 0.1)"
345
+ }) {
346
+ useHeroScene();
347
+ const bgImage = (color) => `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`;
348
+ const bgSize = `${spacing}px ${spacing}px`;
349
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
350
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
351
+ "div",
352
+ {
353
+ "aria-hidden": "true",
354
+ className: "dark:hidden",
355
+ style: {
356
+ position: "absolute",
357
+ inset: 0,
358
+ zIndex: 20,
359
+ pointerEvents: "none",
360
+ backgroundImage: bgImage(lightColor),
361
+ backgroundSize: bgSize
362
+ }
363
+ }
364
+ ),
365
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
366
+ "div",
367
+ {
368
+ "aria-hidden": "true",
369
+ className: "hidden dark:block",
370
+ style: {
371
+ position: "absolute",
372
+ inset: 0,
373
+ zIndex: 20,
374
+ pointerEvents: "none",
375
+ backgroundImage: bgImage(darkColor),
376
+ backgroundSize: bgSize
377
+ }
378
+ }
379
+ )
380
+ ] });
381
+ }
382
+ function DarkOverlay({ opacity = 0.4 }) {
383
+ useHeroScene();
384
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
385
+ "div",
386
+ {
387
+ "aria-hidden": "true",
388
+ className: "pointer-events-none hidden dark:block",
389
+ style: {
390
+ position: "absolute",
391
+ inset: 0,
392
+ zIndex: 10,
393
+ backgroundColor: `rgba(0 0 0 / ${opacity})`
394
+ }
395
+ }
396
+ );
397
+ }
398
+ function Content({ className, children }) {
399
+ useHeroScene();
400
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
401
+ "div",
402
+ {
403
+ className,
404
+ style: { position: "relative", zIndex: 30 },
405
+ children
406
+ }
407
+ );
408
+ }
409
+ var HeroScene = Object.assign(HeroSceneRoot, {
410
+ Parallax,
411
+ Vignette,
412
+ Blur,
413
+ Pattern,
414
+ DarkOverlay,
415
+ Content
416
+ });
417
+ // Annotate the CommonJS export names for ESM import in node:
418
+ 0 && (module.exports = {
419
+ HeroScene,
420
+ buildBlurMask,
421
+ buildVignetteGradient,
422
+ useReducedMotion
423
+ });
424
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/hero-scene.tsx","../src/hero-scene-context.ts","../src/utils.ts","../src/use-in-viewport.ts","../src/use-reduced-motion.ts"],"sourcesContent":["export { HeroScene } from './hero-scene'\nexport { buildVignetteGradient, buildBlurMask } from './utils'\nexport { useReducedMotion } from './use-reduced-motion'\n\nexport type {\n HeroSceneProps,\n HeroImage,\n ParallaxProps,\n VignetteProps,\n BlurProps,\n PatternProps,\n DarkOverlayProps,\n ContentProps,\n} from './types'\n","'use client'\n\nimport Image from 'next/image'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { HeroSceneContext, useHeroScene } from './hero-scene-context'\nimport type {\n BlurProps,\n ContentProps,\n DarkOverlayProps,\n HeroSceneProps,\n ParallaxProps,\n PatternProps,\n VignetteProps,\n} from './types'\nimport { buildBlurMask, buildVignetteGradient } from './utils'\nimport { useInViewport } from './use-in-viewport'\nimport { useReducedMotion } from './use-reduced-motion'\n\n// ─── Root Component ──────────────────────────────────────────\n\nfunction HeroSceneRoot({\n images,\n initialIndex = 0,\n interval = 30_000,\n transitionDuration = 700,\n className,\n onIndexChange,\n children,\n}: HeroSceneProps) {\n const [index, setIndex] = useState(initialIndex)\n const rootRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const isInViewport = useInViewport(rootRef)\n\n // ── Image rotation ──\n useEffect(() => {\n if (interval <= 0 || images.length <= 1) return\n\n let current = initialIndex\n const id = setInterval(() => {\n const next = (current + 1) % images.length\n current = next\n setIndex(next)\n setTimeout(() => {\n onIndexChange?.(next)\n }, 0)\n }, interval)\n return () => clearInterval(id)\n }, [initialIndex, interval, images.length, onIndexChange])\n\n const activeColor = images[index]?.color ?? '128, 128, 128'\n\n return (\n <HeroSceneContext.Provider\n value={{\n images,\n index,\n transitionDuration,\n reducedMotion,\n isInViewport,\n containerRef: rootRef,\n }}\n >\n <div\n ref={rootRef}\n className={className}\n style={{ position: 'relative', overflow: 'hidden' }}\n >\n {/* ── Background images (no parallax — Parallax child wraps these) ── */}\n <div\n data-hero-images=\"\"\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgb(${activeColor})`,\n transition: reducedMotion ? 'none' : 'background-color 1s ease',\n }}\n >\n {images.map((img, i) => (\n <Image\n key={img.src}\n src={img.src}\n alt=\"\"\n width={1920}\n height={1080}\n priority={i === initialIndex}\n sizes=\"100vw\"\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n opacity: i === index ? 1 : 0,\n transition: reducedMotion\n ? 'none'\n : `opacity ${transitionDuration}ms ease`,\n }}\n />\n ))}\n </div>\n\n {children}\n </div>\n </HeroSceneContext.Provider>\n )\n}\n\n// ─── Parallax ────────────────────────────────────────────────\n\nfunction Parallax({\n speed = 0.4,\n mouseShiftX = 25,\n mouseShiftY = 15,\n mouseLerp = 0.04,\n}: ParallaxProps) {\n const { reducedMotion, isInViewport, containerRef } = useHeroScene()\n\n const scrollY = useRef(0)\n const targetMouseX = useRef(0)\n const targetMouseY = useRef(0)\n const currentMouseX = useRef(0)\n const currentMouseY = useRef(0)\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n // Expand the images container to allow parallax overflow\n imagesEl.style.inset = '-15%'\n imagesEl.style.willChange = 'transform'\n\n return () => {\n imagesEl.style.inset = '0'\n imagesEl.style.willChange = ''\n imagesEl.style.transform = ''\n }\n }, [reducedMotion, containerRef])\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n let rafId: number\n let running = true\n\n function loop() {\n if (!running) return\n\n currentMouseX.current +=\n (targetMouseX.current - currentMouseX.current) * mouseLerp\n currentMouseY.current +=\n (targetMouseY.current - currentMouseY.current) * mouseLerp\n\n const y = scrollY.current * speed\n const mx = currentMouseX.current * mouseShiftX\n const my = currentMouseY.current * mouseShiftY\n imagesEl!.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`\n\n rafId = requestAnimationFrame(loop)\n }\n\n function onScroll() {\n if (!isInViewport) return\n scrollY.current = globalThis.scrollY\n }\n\n function onMouseMove(e: MouseEvent) {\n if (!isInViewport) return\n targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2\n targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2\n }\n\n globalThis.addEventListener('scroll', onScroll, { passive: true })\n globalThis.addEventListener('mousemove', onMouseMove, { passive: true })\n rafId = requestAnimationFrame(loop)\n\n return () => {\n running = false\n globalThis.removeEventListener('scroll', onScroll)\n globalThis.removeEventListener('mousemove', onMouseMove)\n cancelAnimationFrame(rafId)\n }\n }, [\n reducedMotion,\n isInViewport,\n containerRef,\n speed,\n mouseShiftX,\n mouseShiftY,\n mouseLerp,\n ])\n\n // Parallax is a behavior-only component — renders nothing\n return null\n}\n\n// ─── Vignette ────────────────────────────────────────────────\n\nfunction Vignette({\n centerX = 50,\n centerY = 100,\n shape = 'circle',\n stops,\n transitionDuration = 1000,\n}: VignetteProps) {\n const { images, index, reducedMotion } = useHeroScene()\n const transitionMs = reducedMotion ? '0ms' : `${transitionDuration}ms`\n\n return (\n <>\n {images.map((img, i) => (\n <div\n key={`vignette-${img.color}`}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n opacity: i === index ? 1 : 0,\n transition: `opacity ${transitionMs} ease`,\n background: buildVignetteGradient(img.color, {\n centerX,\n centerY,\n shape,\n stops,\n }),\n }}\n />\n ))}\n </>\n )\n}\n\n// ─── Blur ────────────────────────────────────────────────────\n\nfunction Blur({\n amount = 24,\n centerX = 50,\n centerY = 65,\n innerRadius = 15,\n outerRadius = 55,\n}: BlurProps) {\n useHeroScene() // validate context\n\n const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius })\n\n return (\n <div\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 15,\n backdropFilter: `blur(${amount}px)`,\n WebkitBackdropFilter: `blur(${amount}px)`,\n maskImage: mask,\n WebkitMaskImage: mask,\n pointerEvents: 'none',\n }}\n />\n )\n}\n\n// ─── Pattern ─────────────────────────────────────────────────\n\nfunction Pattern({\n dotSize = 1,\n spacing = 20,\n lightColor = 'rgba(0 0 0 / 0.15)',\n darkColor = 'rgba(255 255 255 / 0.1)',\n}: PatternProps) {\n useHeroScene() // validate context\n\n const bgImage = (color: string) =>\n `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`\n const bgSize = `${spacing}px ${spacing}px`\n\n return (\n <>\n <div\n aria-hidden=\"true\"\n className=\"dark:hidden\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(lightColor),\n backgroundSize: bgSize,\n }}\n />\n <div\n aria-hidden=\"true\"\n className=\"hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(darkColor),\n backgroundSize: bgSize,\n }}\n />\n </>\n )\n}\n\n// ─── DarkOverlay ─────────────────────────────────────────────\n\nfunction DarkOverlay({ opacity = 0.4 }: DarkOverlayProps) {\n useHeroScene() // validate context\n\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 10,\n backgroundColor: `rgba(0 0 0 / ${opacity})`,\n }}\n />\n )\n}\n\n// ─── Content ─────────────────────────────────────────────────\n\nfunction Content({ className, children }: ContentProps) {\n useHeroScene() // validate context\n\n return (\n <div\n className={className}\n style={{ position: 'relative', zIndex: 30 }}\n >\n {children}\n </div>\n )\n}\n\n// ─── Compound export ─────────────────────────────────────────\n\nexport const HeroScene = Object.assign(HeroSceneRoot, {\n Parallax,\n Vignette,\n Blur,\n Pattern,\n DarkOverlay,\n Content,\n})\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { HeroSceneContextValue } from './types'\n\nexport const HeroSceneContext = createContext<HeroSceneContextValue | null>(null)\n\nexport function useHeroScene(): HeroSceneContextValue {\n const ctx = useContext(HeroSceneContext)\n if (!ctx) {\n throw new Error(\n 'HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) ' +\n 'must be rendered inside a <HeroScene> parent.',\n )\n }\n return ctx\n}\n","import type { VignetteProps, BlurProps } from './types'\n\nconst DEFAULT_STOPS: [number, number][] = [\n [0, 0],\n [15, 0.08],\n [30, 0.15],\n [45, 0.25],\n [60, 0.35],\n [75, 0.45],\n [88, 0.55],\n [100, 0.6],\n]\n\nexport function buildVignetteGradient(\n color: string,\n config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>,\n): string {\n const shape = config.shape ?? 'circle'\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 100\n const stops = config.stops ?? DEFAULT_STOPS\n\n const gradientStops = stops\n .map(([pos, opacity]) =>\n opacity === 0\n ? `transparent ${pos}%`\n : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`,\n )\n .join(', ')\n\n return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`\n}\n\nexport function buildBlurMask(\n config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>,\n): string {\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 65\n const inner = config.innerRadius ?? 15\n const outer = config.outerRadius ?? 55\n\n return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\n\n/**\n * Returns true when the referenced element is at least partially visible\n * in the viewport, using IntersectionObserver. SSR-safe — defaults to true\n * so effects run immediately on first paint before the observer fires.\n */\nexport function useInViewport(ref: RefObject<HTMLElement | null>): boolean {\n const [inViewport, setInViewport] = useState(true)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n setInViewport(entry.isIntersecting)\n },\n { threshold: 0 },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [ref])\n\n return inViewport\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns true when the user has enabled \"prefers-reduced-motion: reduce\"\n * in their OS or browser settings. SSR-safe — defaults to false.\n */\nexport function useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false)\n\n useEffect(() => {\n const mql = globalThis.matchMedia('(prefers-reduced-motion: reduce)')\n setReduced(mql.matches)\n\n function onChange(e: MediaQueryListEvent) {\n setReduced(e.matches)\n }\n\n mql.addEventListener('change', onChange)\n return () => mql.removeEventListener('change', onChange)\n }, [])\n\n return reduced\n}\n"],"mappings":"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","names":["import_react","import_react","import_react","Image"]}
@@ -0,0 +1,106 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type HeroImage = {
4
+ /** Image URL */
5
+ src: string;
6
+ /** Dominant color as "r, g, b" string — used for vignette and fallback bg */
7
+ color: string;
8
+ };
9
+ type HeroSceneProps = {
10
+ /** Array of images to rotate through */
11
+ images: HeroImage[];
12
+ /** Index of the initial image to show. Default: 0 */
13
+ initialIndex?: number;
14
+ /** Rotation interval in ms. 0 disables rotation. Default: 30000 */
15
+ interval?: number;
16
+ /** Image transition duration in ms. Default: 700 */
17
+ transitionDuration?: number;
18
+ /** Extra className for the container */
19
+ className?: string;
20
+ /** Callback when the active image index changes */
21
+ onIndexChange?: (index: number) => void;
22
+ /** Compound children — HeroScene.Parallax, HeroScene.Vignette, etc. */
23
+ children?: React.ReactNode;
24
+ };
25
+ type ParallaxProps = {
26
+ /** Vertical parallax speed (0 = fixed, 1 = normal scroll). Default: 0.4 */
27
+ speed?: number;
28
+ /** Max horizontal shift from mouse in px. Default: 25 */
29
+ mouseShiftX?: number;
30
+ /** Max vertical shift from mouse in px. Default: 15 */
31
+ mouseShiftY?: number;
32
+ /** Mouse follow lerp factor (0-1, lower = smoother). Default: 0.04 */
33
+ mouseLerp?: number;
34
+ };
35
+ type VignetteProps = {
36
+ /** Vignette shape center X position (%). Default: 50 */
37
+ centerX?: number;
38
+ /** Vignette shape center Y position (%). Default: 100 */
39
+ centerY?: number;
40
+ /** Vignette shape type. Default: 'circle' */
41
+ shape?: 'circle' | 'ellipse';
42
+ /** Opacity stops — array of [percentage, opacity] pairs from center to edge.
43
+ * Default: [[0, 0], [15, 0.08], [30, 0.15], [45, 0.25], [60, 0.35], [75, 0.45], [88, 0.55], [100, 0.6]] */
44
+ stops?: [number, number][];
45
+ /** Transition duration in ms when color changes. Default: 1000 */
46
+ transitionDuration?: number;
47
+ };
48
+ type BlurProps = {
49
+ /** Blur amount in px. Default: 24 (backdrop-blur-xl) */
50
+ amount?: number;
51
+ /** Blur mask center X (%). Default: 50 */
52
+ centerX?: number;
53
+ /** Blur mask center Y (%). Default: 65 */
54
+ centerY?: number;
55
+ /** Inner radius where blur starts (%). Default: 15 */
56
+ innerRadius?: number;
57
+ /** Outer radius where blur is full (%). Default: 55 */
58
+ outerRadius?: number;
59
+ };
60
+ type PatternProps = {
61
+ /** Dot size in px. Default: 1 */
62
+ dotSize?: number;
63
+ /** Grid spacing in px. Default: 20 */
64
+ spacing?: number;
65
+ /** Dot color for light mode. Default: 'rgba(0 0 0 / 0.15)' */
66
+ lightColor?: string;
67
+ /** Dot color for dark mode. Default: 'rgba(255 255 255 / 0.1)' */
68
+ darkColor?: string;
69
+ };
70
+ type DarkOverlayProps = {
71
+ /** Overlay opacity. Default: 0.4 */
72
+ opacity?: number;
73
+ };
74
+ type ContentProps = {
75
+ /** Extra className for the content wrapper */
76
+ className?: string;
77
+ /** Content to render above all layers */
78
+ children?: React.ReactNode;
79
+ };
80
+
81
+ declare function HeroSceneRoot({ images, initialIndex, interval, transitionDuration, className, onIndexChange, children, }: HeroSceneProps): react_jsx_runtime.JSX.Element;
82
+ declare function Parallax({ speed, mouseShiftX, mouseShiftY, mouseLerp, }: ParallaxProps): null;
83
+ declare function Vignette({ centerX, centerY, shape, stops, transitionDuration, }: VignetteProps): react_jsx_runtime.JSX.Element;
84
+ declare function Blur({ amount, centerX, centerY, innerRadius, outerRadius, }: BlurProps): react_jsx_runtime.JSX.Element;
85
+ declare function Pattern({ dotSize, spacing, lightColor, darkColor, }: PatternProps): react_jsx_runtime.JSX.Element;
86
+ declare function DarkOverlay({ opacity }: DarkOverlayProps): react_jsx_runtime.JSX.Element;
87
+ declare function Content({ className, children }: ContentProps): react_jsx_runtime.JSX.Element;
88
+ declare const HeroScene: typeof HeroSceneRoot & {
89
+ Parallax: typeof Parallax;
90
+ Vignette: typeof Vignette;
91
+ Blur: typeof Blur;
92
+ Pattern: typeof Pattern;
93
+ DarkOverlay: typeof DarkOverlay;
94
+ Content: typeof Content;
95
+ };
96
+
97
+ declare function buildVignetteGradient(color: string, config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>): string;
98
+ declare function buildBlurMask(config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>): string;
99
+
100
+ /**
101
+ * Returns true when the user has enabled "prefers-reduced-motion: reduce"
102
+ * in their OS or browser settings. SSR-safe — defaults to false.
103
+ */
104
+ declare function useReducedMotion(): boolean;
105
+
106
+ export { type BlurProps, type ContentProps, type DarkOverlayProps, type HeroImage, HeroScene, type HeroSceneProps, type ParallaxProps, type PatternProps, type VignetteProps, buildBlurMask, buildVignetteGradient, useReducedMotion };
@@ -0,0 +1,106 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+
3
+ type HeroImage = {
4
+ /** Image URL */
5
+ src: string;
6
+ /** Dominant color as "r, g, b" string — used for vignette and fallback bg */
7
+ color: string;
8
+ };
9
+ type HeroSceneProps = {
10
+ /** Array of images to rotate through */
11
+ images: HeroImage[];
12
+ /** Index of the initial image to show. Default: 0 */
13
+ initialIndex?: number;
14
+ /** Rotation interval in ms. 0 disables rotation. Default: 30000 */
15
+ interval?: number;
16
+ /** Image transition duration in ms. Default: 700 */
17
+ transitionDuration?: number;
18
+ /** Extra className for the container */
19
+ className?: string;
20
+ /** Callback when the active image index changes */
21
+ onIndexChange?: (index: number) => void;
22
+ /** Compound children — HeroScene.Parallax, HeroScene.Vignette, etc. */
23
+ children?: React.ReactNode;
24
+ };
25
+ type ParallaxProps = {
26
+ /** Vertical parallax speed (0 = fixed, 1 = normal scroll). Default: 0.4 */
27
+ speed?: number;
28
+ /** Max horizontal shift from mouse in px. Default: 25 */
29
+ mouseShiftX?: number;
30
+ /** Max vertical shift from mouse in px. Default: 15 */
31
+ mouseShiftY?: number;
32
+ /** Mouse follow lerp factor (0-1, lower = smoother). Default: 0.04 */
33
+ mouseLerp?: number;
34
+ };
35
+ type VignetteProps = {
36
+ /** Vignette shape center X position (%). Default: 50 */
37
+ centerX?: number;
38
+ /** Vignette shape center Y position (%). Default: 100 */
39
+ centerY?: number;
40
+ /** Vignette shape type. Default: 'circle' */
41
+ shape?: 'circle' | 'ellipse';
42
+ /** Opacity stops — array of [percentage, opacity] pairs from center to edge.
43
+ * Default: [[0, 0], [15, 0.08], [30, 0.15], [45, 0.25], [60, 0.35], [75, 0.45], [88, 0.55], [100, 0.6]] */
44
+ stops?: [number, number][];
45
+ /** Transition duration in ms when color changes. Default: 1000 */
46
+ transitionDuration?: number;
47
+ };
48
+ type BlurProps = {
49
+ /** Blur amount in px. Default: 24 (backdrop-blur-xl) */
50
+ amount?: number;
51
+ /** Blur mask center X (%). Default: 50 */
52
+ centerX?: number;
53
+ /** Blur mask center Y (%). Default: 65 */
54
+ centerY?: number;
55
+ /** Inner radius where blur starts (%). Default: 15 */
56
+ innerRadius?: number;
57
+ /** Outer radius where blur is full (%). Default: 55 */
58
+ outerRadius?: number;
59
+ };
60
+ type PatternProps = {
61
+ /** Dot size in px. Default: 1 */
62
+ dotSize?: number;
63
+ /** Grid spacing in px. Default: 20 */
64
+ spacing?: number;
65
+ /** Dot color for light mode. Default: 'rgba(0 0 0 / 0.15)' */
66
+ lightColor?: string;
67
+ /** Dot color for dark mode. Default: 'rgba(255 255 255 / 0.1)' */
68
+ darkColor?: string;
69
+ };
70
+ type DarkOverlayProps = {
71
+ /** Overlay opacity. Default: 0.4 */
72
+ opacity?: number;
73
+ };
74
+ type ContentProps = {
75
+ /** Extra className for the content wrapper */
76
+ className?: string;
77
+ /** Content to render above all layers */
78
+ children?: React.ReactNode;
79
+ };
80
+
81
+ declare function HeroSceneRoot({ images, initialIndex, interval, transitionDuration, className, onIndexChange, children, }: HeroSceneProps): react_jsx_runtime.JSX.Element;
82
+ declare function Parallax({ speed, mouseShiftX, mouseShiftY, mouseLerp, }: ParallaxProps): null;
83
+ declare function Vignette({ centerX, centerY, shape, stops, transitionDuration, }: VignetteProps): react_jsx_runtime.JSX.Element;
84
+ declare function Blur({ amount, centerX, centerY, innerRadius, outerRadius, }: BlurProps): react_jsx_runtime.JSX.Element;
85
+ declare function Pattern({ dotSize, spacing, lightColor, darkColor, }: PatternProps): react_jsx_runtime.JSX.Element;
86
+ declare function DarkOverlay({ opacity }: DarkOverlayProps): react_jsx_runtime.JSX.Element;
87
+ declare function Content({ className, children }: ContentProps): react_jsx_runtime.JSX.Element;
88
+ declare const HeroScene: typeof HeroSceneRoot & {
89
+ Parallax: typeof Parallax;
90
+ Vignette: typeof Vignette;
91
+ Blur: typeof Blur;
92
+ Pattern: typeof Pattern;
93
+ DarkOverlay: typeof DarkOverlay;
94
+ Content: typeof Content;
95
+ };
96
+
97
+ declare function buildVignetteGradient(color: string, config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>): string;
98
+ declare function buildBlurMask(config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>): string;
99
+
100
+ /**
101
+ * Returns true when the user has enabled "prefers-reduced-motion: reduce"
102
+ * in their OS or browser settings. SSR-safe — defaults to false.
103
+ */
104
+ declare function useReducedMotion(): boolean;
105
+
106
+ export { type BlurProps, type ContentProps, type DarkOverlayProps, type HeroImage, HeroScene, type HeroSceneProps, type ParallaxProps, type PatternProps, type VignetteProps, buildBlurMask, buildVignetteGradient, useReducedMotion };
package/dist/index.js ADDED
@@ -0,0 +1,385 @@
1
+ "use client";
2
+
3
+ // src/hero-scene.tsx
4
+ import Image from "next/image";
5
+ import { useEffect as useEffect3, useRef, useState as useState3 } from "react";
6
+
7
+ // src/hero-scene-context.ts
8
+ import { createContext, useContext } from "react";
9
+ var HeroSceneContext = createContext(null);
10
+ function useHeroScene() {
11
+ const ctx = useContext(HeroSceneContext);
12
+ if (!ctx) {
13
+ throw new Error(
14
+ "HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) must be rendered inside a <HeroScene> parent."
15
+ );
16
+ }
17
+ return ctx;
18
+ }
19
+
20
+ // src/utils.ts
21
+ var DEFAULT_STOPS = [
22
+ [0, 0],
23
+ [15, 0.08],
24
+ [30, 0.15],
25
+ [45, 0.25],
26
+ [60, 0.35],
27
+ [75, 0.45],
28
+ [88, 0.55],
29
+ [100, 0.6]
30
+ ];
31
+ function buildVignetteGradient(color, config) {
32
+ const shape = config.shape ?? "circle";
33
+ const cx = config.centerX ?? 50;
34
+ const cy = config.centerY ?? 100;
35
+ const stops = config.stops ?? DEFAULT_STOPS;
36
+ const gradientStops = stops.map(
37
+ ([pos, opacity]) => opacity === 0 ? `transparent ${pos}%` : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`
38
+ ).join(", ");
39
+ return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`;
40
+ }
41
+ function buildBlurMask(config) {
42
+ const cx = config.centerX ?? 50;
43
+ const cy = config.centerY ?? 65;
44
+ const inner = config.innerRadius ?? 15;
45
+ const outer = config.outerRadius ?? 55;
46
+ return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`;
47
+ }
48
+
49
+ // src/use-in-viewport.ts
50
+ import { useEffect, useState } from "react";
51
+ function useInViewport(ref) {
52
+ const [inViewport, setInViewport] = useState(true);
53
+ useEffect(() => {
54
+ const el = ref.current;
55
+ if (!el) return;
56
+ const observer = new IntersectionObserver(
57
+ ([entry]) => {
58
+ setInViewport(entry.isIntersecting);
59
+ },
60
+ { threshold: 0 }
61
+ );
62
+ observer.observe(el);
63
+ return () => observer.disconnect();
64
+ }, [ref]);
65
+ return inViewport;
66
+ }
67
+
68
+ // src/use-reduced-motion.ts
69
+ import { useEffect as useEffect2, useState as useState2 } from "react";
70
+ function useReducedMotion() {
71
+ const [reduced, setReduced] = useState2(false);
72
+ useEffect2(() => {
73
+ const mql = globalThis.matchMedia("(prefers-reduced-motion: reduce)");
74
+ setReduced(mql.matches);
75
+ function onChange(e) {
76
+ setReduced(e.matches);
77
+ }
78
+ mql.addEventListener("change", onChange);
79
+ return () => mql.removeEventListener("change", onChange);
80
+ }, []);
81
+ return reduced;
82
+ }
83
+
84
+ // src/hero-scene.tsx
85
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
86
+ function HeroSceneRoot({
87
+ images,
88
+ initialIndex = 0,
89
+ interval = 3e4,
90
+ transitionDuration = 700,
91
+ className,
92
+ onIndexChange,
93
+ children
94
+ }) {
95
+ const [index, setIndex] = useState3(initialIndex);
96
+ const rootRef = useRef(null);
97
+ const reducedMotion = useReducedMotion();
98
+ const isInViewport = useInViewport(rootRef);
99
+ useEffect3(() => {
100
+ if (interval <= 0 || images.length <= 1) return;
101
+ let current = initialIndex;
102
+ const id = setInterval(() => {
103
+ const next = (current + 1) % images.length;
104
+ current = next;
105
+ setIndex(next);
106
+ setTimeout(() => {
107
+ onIndexChange?.(next);
108
+ }, 0);
109
+ }, interval);
110
+ return () => clearInterval(id);
111
+ }, [initialIndex, interval, images.length, onIndexChange]);
112
+ const activeColor = images[index]?.color ?? "128, 128, 128";
113
+ return /* @__PURE__ */ jsx(
114
+ HeroSceneContext.Provider,
115
+ {
116
+ value: {
117
+ images,
118
+ index,
119
+ transitionDuration,
120
+ reducedMotion,
121
+ isInViewport,
122
+ containerRef: rootRef
123
+ },
124
+ children: /* @__PURE__ */ jsxs(
125
+ "div",
126
+ {
127
+ ref: rootRef,
128
+ className,
129
+ style: { position: "relative", overflow: "hidden" },
130
+ children: [
131
+ /* @__PURE__ */ jsx(
132
+ "div",
133
+ {
134
+ "data-hero-images": "",
135
+ "aria-hidden": "true",
136
+ style: {
137
+ position: "absolute",
138
+ inset: 0,
139
+ backgroundColor: `rgb(${activeColor})`,
140
+ transition: reducedMotion ? "none" : "background-color 1s ease"
141
+ },
142
+ children: images.map((img, i) => /* @__PURE__ */ jsx(
143
+ Image,
144
+ {
145
+ src: img.src,
146
+ alt: "",
147
+ width: 1920,
148
+ height: 1080,
149
+ priority: i === initialIndex,
150
+ sizes: "100vw",
151
+ style: {
152
+ position: "absolute",
153
+ inset: 0,
154
+ width: "100%",
155
+ height: "100%",
156
+ objectFit: "cover",
157
+ opacity: i === index ? 1 : 0,
158
+ transition: reducedMotion ? "none" : `opacity ${transitionDuration}ms ease`
159
+ }
160
+ },
161
+ img.src
162
+ ))
163
+ }
164
+ ),
165
+ children
166
+ ]
167
+ }
168
+ )
169
+ }
170
+ );
171
+ }
172
+ function Parallax({
173
+ speed = 0.4,
174
+ mouseShiftX = 25,
175
+ mouseShiftY = 15,
176
+ mouseLerp = 0.04
177
+ }) {
178
+ const { reducedMotion, isInViewport, containerRef } = useHeroScene();
179
+ const scrollY = useRef(0);
180
+ const targetMouseX = useRef(0);
181
+ const targetMouseY = useRef(0);
182
+ const currentMouseX = useRef(0);
183
+ const currentMouseY = useRef(0);
184
+ useEffect3(() => {
185
+ if (reducedMotion) return;
186
+ const root = containerRef.current;
187
+ if (!root) return;
188
+ const imagesEl = root.querySelector("[data-hero-images]");
189
+ if (!imagesEl) return;
190
+ imagesEl.style.inset = "-15%";
191
+ imagesEl.style.willChange = "transform";
192
+ return () => {
193
+ imagesEl.style.inset = "0";
194
+ imagesEl.style.willChange = "";
195
+ imagesEl.style.transform = "";
196
+ };
197
+ }, [reducedMotion, containerRef]);
198
+ useEffect3(() => {
199
+ if (reducedMotion) return;
200
+ const root = containerRef.current;
201
+ if (!root) return;
202
+ const imagesEl = root.querySelector("[data-hero-images]");
203
+ if (!imagesEl) return;
204
+ let rafId;
205
+ let running = true;
206
+ function loop() {
207
+ if (!running) return;
208
+ currentMouseX.current += (targetMouseX.current - currentMouseX.current) * mouseLerp;
209
+ currentMouseY.current += (targetMouseY.current - currentMouseY.current) * mouseLerp;
210
+ const y = scrollY.current * speed;
211
+ const mx = currentMouseX.current * mouseShiftX;
212
+ const my = currentMouseY.current * mouseShiftY;
213
+ imagesEl.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`;
214
+ rafId = requestAnimationFrame(loop);
215
+ }
216
+ function onScroll() {
217
+ if (!isInViewport) return;
218
+ scrollY.current = globalThis.scrollY;
219
+ }
220
+ function onMouseMove(e) {
221
+ if (!isInViewport) return;
222
+ targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2;
223
+ targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2;
224
+ }
225
+ globalThis.addEventListener("scroll", onScroll, { passive: true });
226
+ globalThis.addEventListener("mousemove", onMouseMove, { passive: true });
227
+ rafId = requestAnimationFrame(loop);
228
+ return () => {
229
+ running = false;
230
+ globalThis.removeEventListener("scroll", onScroll);
231
+ globalThis.removeEventListener("mousemove", onMouseMove);
232
+ cancelAnimationFrame(rafId);
233
+ };
234
+ }, [
235
+ reducedMotion,
236
+ isInViewport,
237
+ containerRef,
238
+ speed,
239
+ mouseShiftX,
240
+ mouseShiftY,
241
+ mouseLerp
242
+ ]);
243
+ return null;
244
+ }
245
+ function Vignette({
246
+ centerX = 50,
247
+ centerY = 100,
248
+ shape = "circle",
249
+ stops,
250
+ transitionDuration = 1e3
251
+ }) {
252
+ const { images, index, reducedMotion } = useHeroScene();
253
+ const transitionMs = reducedMotion ? "0ms" : `${transitionDuration}ms`;
254
+ return /* @__PURE__ */ jsx(Fragment, { children: images.map((img, i) => /* @__PURE__ */ jsx(
255
+ "div",
256
+ {
257
+ "aria-hidden": "true",
258
+ style: {
259
+ position: "absolute",
260
+ inset: 0,
261
+ zIndex: 20,
262
+ pointerEvents: "none",
263
+ opacity: i === index ? 1 : 0,
264
+ transition: `opacity ${transitionMs} ease`,
265
+ background: buildVignetteGradient(img.color, {
266
+ centerX,
267
+ centerY,
268
+ shape,
269
+ stops
270
+ })
271
+ }
272
+ },
273
+ `vignette-${img.color}`
274
+ )) });
275
+ }
276
+ function Blur({
277
+ amount = 24,
278
+ centerX = 50,
279
+ centerY = 65,
280
+ innerRadius = 15,
281
+ outerRadius = 55
282
+ }) {
283
+ useHeroScene();
284
+ const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius });
285
+ return /* @__PURE__ */ jsx(
286
+ "div",
287
+ {
288
+ "aria-hidden": "true",
289
+ style: {
290
+ position: "absolute",
291
+ inset: 0,
292
+ zIndex: 15,
293
+ backdropFilter: `blur(${amount}px)`,
294
+ WebkitBackdropFilter: `blur(${amount}px)`,
295
+ maskImage: mask,
296
+ WebkitMaskImage: mask,
297
+ pointerEvents: "none"
298
+ }
299
+ }
300
+ );
301
+ }
302
+ function Pattern({
303
+ dotSize = 1,
304
+ spacing = 20,
305
+ lightColor = "rgba(0 0 0 / 0.15)",
306
+ darkColor = "rgba(255 255 255 / 0.1)"
307
+ }) {
308
+ useHeroScene();
309
+ const bgImage = (color) => `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`;
310
+ const bgSize = `${spacing}px ${spacing}px`;
311
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
312
+ /* @__PURE__ */ jsx(
313
+ "div",
314
+ {
315
+ "aria-hidden": "true",
316
+ className: "dark:hidden",
317
+ style: {
318
+ position: "absolute",
319
+ inset: 0,
320
+ zIndex: 20,
321
+ pointerEvents: "none",
322
+ backgroundImage: bgImage(lightColor),
323
+ backgroundSize: bgSize
324
+ }
325
+ }
326
+ ),
327
+ /* @__PURE__ */ jsx(
328
+ "div",
329
+ {
330
+ "aria-hidden": "true",
331
+ className: "hidden dark:block",
332
+ style: {
333
+ position: "absolute",
334
+ inset: 0,
335
+ zIndex: 20,
336
+ pointerEvents: "none",
337
+ backgroundImage: bgImage(darkColor),
338
+ backgroundSize: bgSize
339
+ }
340
+ }
341
+ )
342
+ ] });
343
+ }
344
+ function DarkOverlay({ opacity = 0.4 }) {
345
+ useHeroScene();
346
+ return /* @__PURE__ */ jsx(
347
+ "div",
348
+ {
349
+ "aria-hidden": "true",
350
+ className: "pointer-events-none hidden dark:block",
351
+ style: {
352
+ position: "absolute",
353
+ inset: 0,
354
+ zIndex: 10,
355
+ backgroundColor: `rgba(0 0 0 / ${opacity})`
356
+ }
357
+ }
358
+ );
359
+ }
360
+ function Content({ className, children }) {
361
+ useHeroScene();
362
+ return /* @__PURE__ */ jsx(
363
+ "div",
364
+ {
365
+ className,
366
+ style: { position: "relative", zIndex: 30 },
367
+ children
368
+ }
369
+ );
370
+ }
371
+ var HeroScene = Object.assign(HeroSceneRoot, {
372
+ Parallax,
373
+ Vignette,
374
+ Blur,
375
+ Pattern,
376
+ DarkOverlay,
377
+ Content
378
+ });
379
+ export {
380
+ HeroScene,
381
+ buildBlurMask,
382
+ buildVignetteGradient,
383
+ useReducedMotion
384
+ };
385
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hero-scene.tsx","../src/hero-scene-context.ts","../src/utils.ts","../src/use-in-viewport.ts","../src/use-reduced-motion.ts"],"sourcesContent":["'use client'\n\nimport Image from 'next/image'\nimport { useEffect, useRef, useState } from 'react'\n\nimport { HeroSceneContext, useHeroScene } from './hero-scene-context'\nimport type {\n BlurProps,\n ContentProps,\n DarkOverlayProps,\n HeroSceneProps,\n ParallaxProps,\n PatternProps,\n VignetteProps,\n} from './types'\nimport { buildBlurMask, buildVignetteGradient } from './utils'\nimport { useInViewport } from './use-in-viewport'\nimport { useReducedMotion } from './use-reduced-motion'\n\n// ─── Root Component ──────────────────────────────────────────\n\nfunction HeroSceneRoot({\n images,\n initialIndex = 0,\n interval = 30_000,\n transitionDuration = 700,\n className,\n onIndexChange,\n children,\n}: HeroSceneProps) {\n const [index, setIndex] = useState(initialIndex)\n const rootRef = useRef<HTMLDivElement>(null)\n const reducedMotion = useReducedMotion()\n const isInViewport = useInViewport(rootRef)\n\n // ── Image rotation ──\n useEffect(() => {\n if (interval <= 0 || images.length <= 1) return\n\n let current = initialIndex\n const id = setInterval(() => {\n const next = (current + 1) % images.length\n current = next\n setIndex(next)\n setTimeout(() => {\n onIndexChange?.(next)\n }, 0)\n }, interval)\n return () => clearInterval(id)\n }, [initialIndex, interval, images.length, onIndexChange])\n\n const activeColor = images[index]?.color ?? '128, 128, 128'\n\n return (\n <HeroSceneContext.Provider\n value={{\n images,\n index,\n transitionDuration,\n reducedMotion,\n isInViewport,\n containerRef: rootRef,\n }}\n >\n <div\n ref={rootRef}\n className={className}\n style={{ position: 'relative', overflow: 'hidden' }}\n >\n {/* ── Background images (no parallax — Parallax child wraps these) ── */}\n <div\n data-hero-images=\"\"\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n backgroundColor: `rgb(${activeColor})`,\n transition: reducedMotion ? 'none' : 'background-color 1s ease',\n }}\n >\n {images.map((img, i) => (\n <Image\n key={img.src}\n src={img.src}\n alt=\"\"\n width={1920}\n height={1080}\n priority={i === initialIndex}\n sizes=\"100vw\"\n style={{\n position: 'absolute',\n inset: 0,\n width: '100%',\n height: '100%',\n objectFit: 'cover',\n opacity: i === index ? 1 : 0,\n transition: reducedMotion\n ? 'none'\n : `opacity ${transitionDuration}ms ease`,\n }}\n />\n ))}\n </div>\n\n {children}\n </div>\n </HeroSceneContext.Provider>\n )\n}\n\n// ─── Parallax ────────────────────────────────────────────────\n\nfunction Parallax({\n speed = 0.4,\n mouseShiftX = 25,\n mouseShiftY = 15,\n mouseLerp = 0.04,\n}: ParallaxProps) {\n const { reducedMotion, isInViewport, containerRef } = useHeroScene()\n\n const scrollY = useRef(0)\n const targetMouseX = useRef(0)\n const targetMouseY = useRef(0)\n const currentMouseX = useRef(0)\n const currentMouseY = useRef(0)\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n // Expand the images container to allow parallax overflow\n imagesEl.style.inset = '-15%'\n imagesEl.style.willChange = 'transform'\n\n return () => {\n imagesEl.style.inset = '0'\n imagesEl.style.willChange = ''\n imagesEl.style.transform = ''\n }\n }, [reducedMotion, containerRef])\n\n useEffect(() => {\n if (reducedMotion) return\n\n const root = containerRef.current\n if (!root) return\n\n const imagesEl = root.querySelector<HTMLDivElement>('[data-hero-images]')\n if (!imagesEl) return\n\n let rafId: number\n let running = true\n\n function loop() {\n if (!running) return\n\n currentMouseX.current +=\n (targetMouseX.current - currentMouseX.current) * mouseLerp\n currentMouseY.current +=\n (targetMouseY.current - currentMouseY.current) * mouseLerp\n\n const y = scrollY.current * speed\n const mx = currentMouseX.current * mouseShiftX\n const my = currentMouseY.current * mouseShiftY\n imagesEl!.style.transform = `translate3d(${mx}px, ${y + my}px, 0)`\n\n rafId = requestAnimationFrame(loop)\n }\n\n function onScroll() {\n if (!isInViewport) return\n scrollY.current = globalThis.scrollY\n }\n\n function onMouseMove(e: MouseEvent) {\n if (!isInViewport) return\n targetMouseX.current = (e.clientX / globalThis.innerWidth - 0.5) * 2\n targetMouseY.current = (e.clientY / globalThis.innerHeight - 0.5) * 2\n }\n\n globalThis.addEventListener('scroll', onScroll, { passive: true })\n globalThis.addEventListener('mousemove', onMouseMove, { passive: true })\n rafId = requestAnimationFrame(loop)\n\n return () => {\n running = false\n globalThis.removeEventListener('scroll', onScroll)\n globalThis.removeEventListener('mousemove', onMouseMove)\n cancelAnimationFrame(rafId)\n }\n }, [\n reducedMotion,\n isInViewport,\n containerRef,\n speed,\n mouseShiftX,\n mouseShiftY,\n mouseLerp,\n ])\n\n // Parallax is a behavior-only component — renders nothing\n return null\n}\n\n// ─── Vignette ────────────────────────────────────────────────\n\nfunction Vignette({\n centerX = 50,\n centerY = 100,\n shape = 'circle',\n stops,\n transitionDuration = 1000,\n}: VignetteProps) {\n const { images, index, reducedMotion } = useHeroScene()\n const transitionMs = reducedMotion ? '0ms' : `${transitionDuration}ms`\n\n return (\n <>\n {images.map((img, i) => (\n <div\n key={`vignette-${img.color}`}\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n opacity: i === index ? 1 : 0,\n transition: `opacity ${transitionMs} ease`,\n background: buildVignetteGradient(img.color, {\n centerX,\n centerY,\n shape,\n stops,\n }),\n }}\n />\n ))}\n </>\n )\n}\n\n// ─── Blur ────────────────────────────────────────────────────\n\nfunction Blur({\n amount = 24,\n centerX = 50,\n centerY = 65,\n innerRadius = 15,\n outerRadius = 55,\n}: BlurProps) {\n useHeroScene() // validate context\n\n const mask = buildBlurMask({ centerX, centerY, innerRadius, outerRadius })\n\n return (\n <div\n aria-hidden=\"true\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 15,\n backdropFilter: `blur(${amount}px)`,\n WebkitBackdropFilter: `blur(${amount}px)`,\n maskImage: mask,\n WebkitMaskImage: mask,\n pointerEvents: 'none',\n }}\n />\n )\n}\n\n// ─── Pattern ─────────────────────────────────────────────────\n\nfunction Pattern({\n dotSize = 1,\n spacing = 20,\n lightColor = 'rgba(0 0 0 / 0.15)',\n darkColor = 'rgba(255 255 255 / 0.1)',\n}: PatternProps) {\n useHeroScene() // validate context\n\n const bgImage = (color: string) =>\n `radial-gradient(circle, ${color} ${dotSize}px, transparent ${dotSize}px)`\n const bgSize = `${spacing}px ${spacing}px`\n\n return (\n <>\n <div\n aria-hidden=\"true\"\n className=\"dark:hidden\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(lightColor),\n backgroundSize: bgSize,\n }}\n />\n <div\n aria-hidden=\"true\"\n className=\"hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 20,\n pointerEvents: 'none',\n backgroundImage: bgImage(darkColor),\n backgroundSize: bgSize,\n }}\n />\n </>\n )\n}\n\n// ─── DarkOverlay ─────────────────────────────────────────────\n\nfunction DarkOverlay({ opacity = 0.4 }: DarkOverlayProps) {\n useHeroScene() // validate context\n\n return (\n <div\n aria-hidden=\"true\"\n className=\"pointer-events-none hidden dark:block\"\n style={{\n position: 'absolute',\n inset: 0,\n zIndex: 10,\n backgroundColor: `rgba(0 0 0 / ${opacity})`,\n }}\n />\n )\n}\n\n// ─── Content ─────────────────────────────────────────────────\n\nfunction Content({ className, children }: ContentProps) {\n useHeroScene() // validate context\n\n return (\n <div\n className={className}\n style={{ position: 'relative', zIndex: 30 }}\n >\n {children}\n </div>\n )\n}\n\n// ─── Compound export ─────────────────────────────────────────\n\nexport const HeroScene = Object.assign(HeroSceneRoot, {\n Parallax,\n Vignette,\n Blur,\n Pattern,\n DarkOverlay,\n Content,\n})\n","'use client'\n\nimport { createContext, useContext } from 'react'\nimport type { HeroSceneContextValue } from './types'\n\nexport const HeroSceneContext = createContext<HeroSceneContextValue | null>(null)\n\nexport function useHeroScene(): HeroSceneContextValue {\n const ctx = useContext(HeroSceneContext)\n if (!ctx) {\n throw new Error(\n 'HeroScene compound components (Parallax, Vignette, Blur, Pattern, DarkOverlay, Content) ' +\n 'must be rendered inside a <HeroScene> parent.',\n )\n }\n return ctx\n}\n","import type { VignetteProps, BlurProps } from './types'\n\nconst DEFAULT_STOPS: [number, number][] = [\n [0, 0],\n [15, 0.08],\n [30, 0.15],\n [45, 0.25],\n [60, 0.35],\n [75, 0.45],\n [88, 0.55],\n [100, 0.6],\n]\n\nexport function buildVignetteGradient(\n color: string,\n config: Pick<VignetteProps, 'shape' | 'centerX' | 'centerY' | 'stops'>,\n): string {\n const shape = config.shape ?? 'circle'\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 100\n const stops = config.stops ?? DEFAULT_STOPS\n\n const gradientStops = stops\n .map(([pos, opacity]) =>\n opacity === 0\n ? `transparent ${pos}%`\n : `color-mix(in srgb, rgb(${color}) ${Math.round(opacity * 100)}%, transparent) ${pos}%`,\n )\n .join(', ')\n\n return `radial-gradient(${shape} at ${cx}% ${cy}%, ${gradientStops})`\n}\n\nexport function buildBlurMask(\n config: Pick<BlurProps, 'centerX' | 'centerY' | 'innerRadius' | 'outerRadius'>,\n): string {\n const cx = config.centerX ?? 50\n const cy = config.centerY ?? 65\n const inner = config.innerRadius ?? 15\n const outer = config.outerRadius ?? 55\n\n return `radial-gradient(circle at ${cx}% ${cy}%, transparent ${inner}%, black ${outer}%)`\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\nimport type { RefObject } from 'react'\n\n/**\n * Returns true when the referenced element is at least partially visible\n * in the viewport, using IntersectionObserver. SSR-safe — defaults to true\n * so effects run immediately on first paint before the observer fires.\n */\nexport function useInViewport(ref: RefObject<HTMLElement | null>): boolean {\n const [inViewport, setInViewport] = useState(true)\n\n useEffect(() => {\n const el = ref.current\n if (!el) return\n\n const observer = new IntersectionObserver(\n ([entry]) => {\n setInViewport(entry.isIntersecting)\n },\n { threshold: 0 },\n )\n\n observer.observe(el)\n return () => observer.disconnect()\n }, [ref])\n\n return inViewport\n}\n","'use client'\n\nimport { useEffect, useState } from 'react'\n\n/**\n * Returns true when the user has enabled \"prefers-reduced-motion: reduce\"\n * in their OS or browser settings. SSR-safe — defaults to false.\n */\nexport function useReducedMotion(): boolean {\n const [reduced, setReduced] = useState(false)\n\n useEffect(() => {\n const mql = globalThis.matchMedia('(prefers-reduced-motion: reduce)')\n setReduced(mql.matches)\n\n function onChange(e: MediaQueryListEvent) {\n setReduced(e.matches)\n }\n\n mql.addEventListener('change', onChange)\n return () => mql.removeEventListener('change', onChange)\n }, [])\n\n return reduced\n}\n"],"mappings":"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","names":["useEffect","useState","useEffect","useState","useState","useEffect"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@okinoxis/hero-scene",
3
+ "version": "0.2.0",
4
+ "description": "Cinematic hero sections for Next.js — compound component API with image rotation, parallax, vignette overlays, blur masks, and dot patterns.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "typecheck": "tsc --noEmit",
26
+ "publint": "publint",
27
+ "size": "size-limit",
28
+ "prepublishOnly": "npm run build && npm run publint && npm run size"
29
+ },
30
+ "size-limit": [
31
+ {
32
+ "path": "./dist/index.js",
33
+ "limit": "10 KB"
34
+ }
35
+ ],
36
+ "peerDependencies": {
37
+ "react": "^18.0.0 || ^19.0.0",
38
+ "react-dom": "^18.0.0 || ^19.0.0",
39
+ "next": ">=14.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@size-limit/preset-small-lib": "^11.2.0",
43
+ "@types/react": "^19.2.14",
44
+ "@types/react-dom": "^19.2.3",
45
+ "next": "^16.1.6",
46
+ "publint": "^0.3.12",
47
+ "react": "^19.2.4",
48
+ "react-dom": "^19.2.4",
49
+ "size-limit": "^11.2.0",
50
+ "tsup": "^8.5.1",
51
+ "typescript": "^5.9.3"
52
+ },
53
+ "keywords": [
54
+ "hero",
55
+ "parallax",
56
+ "vignette",
57
+ "nextjs",
58
+ "react",
59
+ "cinematic",
60
+ "compound-component"
61
+ ],
62
+ "license": "MIT"
63
+ }