react-prism-carousel 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/FOR-AI.md ADDED
@@ -0,0 +1,240 @@
1
+ # react-prism-carousel: reference for AI coding assistants
2
+
3
+ This file is for AI assistants writing code that **uses** this component in another project. Read all of it before writing or changing a `<Carousel>`. The human-oriented docs are in `README.md`.
4
+
5
+ ## What it is
6
+
7
+ `Carousel` is a React 19 component that renders its children as the faces of a regular 3D prism, using pure CSS (`preserve-3d`, `sin()`/`cos()`, container query units, `sibling-index()`).
8
+
9
+ **It draws; it does not control.** It has no internal state, no autoplay, no buttons, no keyboard or drag handling. The consuming code owns `currentFace` in its own state and changes it in response to input.
10
+
11
+ ## Install and import
12
+
13
+ `npm install react-prism-carousel`. The package is ESM-only, has a peer dependency on `react@^19`, and ships its own types.
14
+
15
+ ```ts
16
+ import Carousel from "react-prism-carousel"; // or: import { Carousel } from …
17
+ import type { CarouselProps, FaceProps, PrismProps } from "react-prism-carousel";
18
+ import "react-prism-carousel/style.css"; // REQUIRED, once per app (e.g. in the root component or entry file)
19
+ ```
20
+
21
+ ## Minimal correct usage
22
+
23
+ ```tsx
24
+ const [face, setFace] = useState(0);
25
+
26
+ <div style={{ height: 320 }}> {/* REQUIRED: a parent with a definite height */}
27
+ <Carousel
28
+ orientation="horizontal" // required
29
+ noChildrenElem={<p>No items</p>} // required (null is allowed)
30
+ aria-label="Featured products" // always pass a label
31
+ currentFace={face}
32
+ transition="transform 0.6s ease" // optional; the default is no animation
33
+ >
34
+ {items.map((item) => <Slide key={item.id} {...item} />)}
35
+ </Carousel>
36
+ </div>
37
+ <button onClick={() => setFace((f) => f + 1)}>Next</button>
38
+ ```
39
+
40
+ ## Props
41
+
42
+ | Prop | Type | Default | Exact semantics |
43
+ |---|---|---|---|
44
+ | `orientation` | `"horizontal" \| "vertical"` | required | Horizontal spins around the Y axis (faces side by side, each 100% of the root's height). Vertical spins around the X axis (faces stacked, each 100% of the root's width). |
45
+ | `noChildrenElem` | `ReactNode` | required | Rendered, flat and full size, when there are 0 children. |
46
+ | `children` | `ReactNode` | | Processed with `Children.toArray`: arrays are flattened, `null`/`undefined`/booleans are dropped, a Fragment is ONE face. Each child becomes one face. Keys are preserved. |
47
+ | `currentFace` | `number` | `0` | Rotation in face units. Unbounded: with 6 faces, `6` shows face 0 and `-1` shows face 5. Fractions rotate partway. If `numEmptyFaces > 0`, clamped to `[0, childCount − 1]`. |
48
+ | `numEmptyFaces` | `number` | `0` | Extra invisible sides (rounded down, minimum 0). Total sides `n = childCount + numEmptyFaces`. Used to make an arc or scroller. |
49
+ | `faceSize` | `string` | | A CSS length: face width (horizontal) or height (vertical). The radius becomes `faceSize / (2·sin(180°/n))`. Use `px`, `rem`, `cqw` or `cqh`. **Never `%`**: it breaks the depth and triggers a dev warning. |
50
+ | `concave` | `boolean` | `false` | Faces point inward. The on-screen rotation direction is mirrored compared with convex. |
51
+ | `cameraOffset` | `string` | | A CSS length. Translates the prism along Z: **positive moves the camera closer**, negative moves it away. |
52
+ | `transition` | `string` | none | A CSS `transition` value applied to the prism. Rotation animates `transform`; `cameraOffset` animates `translate`. Forced to `none` under `prefers-reduced-motion: reduce`. |
53
+ | `centerpieceElem` | `ReactNode` | | Rendered on the prism's axis and counter-rotated so it always faces the viewer. Its wrapper fills the root's box: centre the content yourself. |
54
+ | `faceProps` | `FaceProps \| (index: number, isFront: boolean) => FaceProps` | | Spread onto every face wrapper `<span>`, AFTER the default `role`/`aria-*`, so it can override them. `isFront` = this face is the one nearest the front (`currentFace` rounded and wrapped). |
55
+ | `prismProps` | `PrismProps` | | Spread onto the prism `<div>`. `className` is merged. `style` is merged, but the component's CSS variables win. The `data-*` attributes can't be overridden. |
56
+ | other props | `HTMLAttributes<HTMLDivElement>` | | Spread onto the root `<div>`. `className` is merged; `style` passes through as is. Put `tabIndex`, `onKeyDown`, pointer handlers, `aria-label`, `perspective` and `perspectiveOrigin` here. |
57
+
58
+ ## DOM structure
59
+
60
+ ```
61
+ div.root role=region aria-roledescription=carousel {...rest props} container-type:size; perspective:1000px; contain:paint
62
+ div.prism {...prismProps} data-orientation … preserve-3d; rotated by currentFace
63
+ span role=group aria-roledescription=slide aria-label="i+1 of N" {...faceProps(i, isFront)}
64
+ child
65
+ …one span per child…
66
+ div.centerpiece
67
+ centerpieceElem
68
+ ```
69
+
70
+ ## Hard rules
71
+
72
+ 0. **Import `react-prism-carousel/style.css` once.** Without it, the faces render as plain stacked elements with no 3D.
73
+ 1. **The root needs a definite size.** It is `width: 100%; height: 100%` with `container-type: size`, so its content never sizes it. Put it in a parent with a height, or set `height`/`minHeight` on it with `style` or `className`. A carousel with no visible output is almost always 0px tall.
74
+ 2. **To advance, increment or decrement; never wrap `currentFace` yourself.** Use `f + 1`, not `(f + 1) % n`. Wrapping makes the prism spin backwards through every face when the transition is on. The component wraps internally for `isFront`. Exception: with `numEmptyFaces > 0` the value is clamped, so clamp it in your own state too (`Math.min(Math.max(f, 0), count - 1)`), or "next" will appear stuck at the ends.
75
+ 3. **Turn the transition off while dragging** (`transition={dragging ? undefined : "transform 0.4s ease-out"}`), feed a fractional `currentFace` during the drag, and `Math.round` it on release.
76
+ 4. **The camera lens is CSS, not a prop.** Set `style={{ perspective: "600px" }}` and/or `perspectiveOrigin` on the root. There is no `lens` or `cameraOrigin` prop.
77
+ 5. **Never `%` in `faceSize`.** Use `cqw` (horizontal) or `cqh` (vertical) for sizes relative to the carousel.
78
+ 6. **Don't override geometry.** On the prism or face wrappers, don't set `position`, `inset`, `margin`, `transform`, `rotate`, `translate`, `transform-origin` or `transform-style`. On face wrappers, also leave the width (horizontal) or height (vertical) alone. Safe to change: face `height` (horizontal) or `width` (vertical), `padding`, `cursor`, colours, classes.
79
+ 7. **Don't flatten 3D.** Don't put `overflow` (other than `visible`), `opacity < 1`, `filter`, `clip-path`, `mask`, `mix-blend-mode` or `isolation: isolate` on the prism; any of these collapses the carousel. For effects like dimming, style the slide inside the wrapper (`.dimmed > * { filter: … }`), not the wrapper.
80
+ 8. **The component's CSS is in `@layer react-prism-carousel.components`.** Any unlayered consumer CSS beats it, including CSS that breaks rules 6–7.
81
+ 9. **Always pass `aria-label`** (or `aria-labelledby`) on the root.
82
+ 10. **The `--prism-*` custom properties are internal.** Don't read or set them.
83
+
84
+ ## Geometry
85
+
86
+ - `n = childCount + numEmptyFaces`, `θ = 360° / n`.
87
+ - Default radius `r` = 50% of the root's width (horizontal) or height (vertical); with `faceSize`, `r = faceSize / (2·sin(θ/2))`.
88
+ - Face size = `2r·sin(θ/2)`. Defaults: 6 faces are each 50% of the width, 4 faces about 70.7%, 8 faces about 38.3%.
89
+ - Apothem (axis-to-face distance) `a = r·cos(θ/2)`.
90
+ - Camera on the axis: **convex** `cameraOffset = perspective + a`; **concave** `cameraOffset = perspective − a`.
91
+ - From outside, a convex prism shows its near half and a concave prism shows the inside of its far half. From inside, only the field of view in front of the camera is drawn (set by `perspective`; smaller = wider), and nothing behind the camera.
92
+
93
+ ## Behaviour to rely on
94
+
95
+ - **0 children:** `noChildrenElem`, flat, full size.
96
+ - **1 child and no empty faces:** flat, full size, no rotation.
97
+ - **2 children:** a two-sided card at full size, back to back; each step flips it 180°.
98
+ - **Clicks and painting** are limited to the root's box (`contain: paint`). Faces outside it never overlap or block neighbouring elements.
99
+ - **Faces are not made `inert`** when turned away. If only the front face should be interactive or reachable by screen readers: `faceProps={(i, isFront) => ({ inert: !isFront })}`.
100
+ - **Backfaces are visible (mirrored) by default.** Hide them with `backface-visibility: hidden` on the slide.
101
+ - Inside slides, `cqw`/`cqh` resolve against the carousel root.
102
+ - `prefers-reduced-motion` disables the `transition` prop. An inline `prismProps.style.transition` still overrides that.
103
+
104
+ ## Known issues (0.1.0)
105
+
106
+ Check the installed version in `node_modules/react-prism-carousel/package.json`. This section describes 0.1.0, and later versions of this file say which issues are fixed.
107
+
108
+ - **Clicks can reach the wrong element.** In a concave prism, a click on a visible face lands on the wrapper of the face opposite it, so the wrong `faceProps` handler runs. In a convex prism, clicks on faces other than the front one can land on the prism element instead. **Whenever faces have click handlers, or concave mode is used with interactive content, apply this workaround.** The CSS must be unlayered:
109
+
110
+ ```tsx
111
+ <Carousel prismProps={{ className: "prism-hit-fix" }} … />
112
+ ```
113
+
114
+ ```css
115
+ .prism-hit-fix { pointer-events: none; }
116
+ .prism-hit-fix > * { pointer-events: auto; backface-visibility: hidden; }
117
+ .prism-hit-fix > :last-child { pointer-events: none; } /* the centerpiece wrapper is always the last child */
118
+ .prism-hit-fix > :last-child > * { pointer-events: auto; }
119
+ @media (prefers-reduced-motion: reduce) { .prism-hit-fix > :last-child { transition: none; } } /* next issue */
120
+ ```
121
+
122
+ - **Under `prefers-reduced-motion`, the centerpiece still animates** its counter-rotation. The last rule above fixes it.
123
+ - **With exactly one face and no empty faces**, changing `currentFace` while a transition is set spins the centerpiece a full turn. Keep `currentFace` at 0 when there's only one face.
124
+
125
+ ## Recipes
126
+
127
+ ### Keyboard
128
+
129
+ ```tsx
130
+ <Carousel
131
+ tabIndex={0}
132
+ onKeyDown={(e) => {
133
+ if (e.key === "ArrowRight") setFace((f) => f + 1);
134
+ if (e.key === "ArrowLeft") setFace((f) => f - 1);
135
+ }}
136
+ …
137
+ />
138
+ ```
139
+
140
+ ### Drag
141
+
142
+ ```tsx
143
+ const [pos, setPos] = useState(0);
144
+ const [dragging, setDragging] = useState(false);
145
+ const start = useRef<{ x: number; pos: number } | null>(null);
146
+ const end = () => { start.current = null; setDragging(false); setPos((p) => Math.round(p)); };
147
+
148
+ <Carousel
149
+ currentFace={pos}
150
+ transition={dragging ? undefined : "transform 0.4s ease-out"}
151
+ style={{ touchAction: "pan-y" }}
152
+ onPointerDown={(e) => { e.currentTarget.setPointerCapture(e.pointerId); start.current = { x: e.clientX, pos }; setDragging(true); }}
153
+ onPointerMove={(e) => {
154
+ if (!start.current) return;
155
+ // n = total sides. With default sizing, face width = width·sin(180°/n)
156
+ const facePx = e.currentTarget.clientWidth * Math.sin(Math.PI / n);
157
+ setPos(start.current.pos - (e.clientX - start.current.x) / facePx);
158
+ }}
159
+ onPointerUp={end}
160
+ onPointerCancel={end}
161
+ …
162
+ />
163
+ ```
164
+
165
+ ### Arc or scroller with clickable faces
166
+
167
+ ```tsx
168
+ <Carousel
169
+ orientation="horizontal"
170
+ aria-label="Products"
171
+ noChildrenElem={null}
172
+ prismProps={{ className: "prism-hit-fix" }} // 0.1.0: see "Known issues"
173
+ currentFace={face} // keep 0 … items.length − 1
174
+ numEmptyFaces={9}
175
+ faceSize="30cqw" // keeps faces the same size whatever numEmptyFaces is
176
+ transition="transform 0.5s ease"
177
+ faceProps={(i, isFront) => ({
178
+ onClick: () => setFace(i),
179
+ className: isFront ? "front" : "dimmed",
180
+ style: { cursor: "pointer", paddingInline: 6 }, // padding = gap between faces
181
+ })}
182
+ >
183
+ {items.map((it) => <Card key={it.id} {...it} />)}
184
+ </Carousel>
185
+ ```
186
+
187
+ ### Per-face accessible names
188
+
189
+ ```tsx
190
+ faceProps={(i) => ({ "aria-label": items[i].title })}
191
+ ```
192
+
193
+ ### Card backs (background from both sides, content only from the front)
194
+
195
+ ```css
196
+ .card { transform-style: preserve-3d; }
197
+ .card-content { backface-visibility: hidden; }
198
+ ```
199
+
200
+ ```tsx
201
+ <div className="card"><span className="card-content">{title}</span></div>
202
+ ```
203
+
204
+ ### Logo in the middle
205
+
206
+ ```tsx
207
+ <Carousel
208
+ faceSize="22cqw"
209
+ style={{ perspectiveOrigin: "50% -40%" }} // view from slightly above
210
+ faceProps={{ style: { height: "40%" } }} // shorter faces reveal the centerpiece
211
+ centerpieceElem={
212
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }}>
213
+ <Logo />
214
+ </div>
215
+ }
216
+ …
217
+ />
218
+ ```
219
+
220
+ ### Camera inside the prism
221
+
222
+ ```tsx
223
+ // 8 faces on a 918px-wide root: r = 459, a = 459·cos(22.5°) ≈ 424
224
+ <Carousel concave style={{ perspective: "300px" }} cameraOffset="-124px" … /> // 300 − 424
225
+ ```
226
+
227
+ ## Common mistakes
228
+
229
+ | Symptom | Cause | Fix |
230
+ |---|---|---|
231
+ | Nothing renders | The root is 0px tall | Give the parent (or the root) a height. |
232
+ | Faces are piled up flat, with no 3D | The stylesheet isn't imported | `import "react-prism-carousel/style.css"` once. |
233
+ | A click runs the wrong face's handler, or side faces ignore clicks | The 0.1.0 hit-testing issue | Apply the workaround in "Known issues". |
234
+ | Prism collapses or looks flat | `%` in `faceSize`, or a 3D-flattening property on the prism | Use `cqw`/`cqh`/`px`; move effects onto the slides. |
235
+ | It spins backwards through every face at the end | `currentFace` wrapped with `%` | Keep incrementing. |
236
+ | "Next" seems stuck in an arc | Value clamped by `numEmptyFaces` while your state keeps growing | Clamp your state too. |
237
+ | Mirrored text on the back faces | Backfaces are visible by default | `backface-visibility: hidden` on the slide, or the card-back recipe. |
238
+ | Front face is huge | Large `faceSize` brings the front face near the camera | Negative `cameraOffset`, or a larger `perspective`. |
239
+ | Dragging feels laggy | Transition still on during the drag | `transition={dragging ? undefined : …}`. |
240
+ | Only one face is visible from inside | Narrow field of view | A smaller `perspective` (e.g. `300px`). |
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 BlueWebber
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,460 @@
1
- # Temporary Holding Version
1
+ # react-prism-carousel
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A 3D prism carousel for React. Every child becomes one face of a regular prism, and the geometry is done entirely in CSS: no measuring, no animation loop, no JavaScript per frame.
4
+
5
+ The component only **draws** the prism. Deciding which face is in front, and when and how it turns (buttons, keys, drag, autoplay), is up to you.
6
+
7
+ - [Requirements](#requirements)
8
+ - [Installation](#installation)
9
+ - [Quick start](#quick-start)
10
+ - [How it works](#how-it-works)
11
+ - [Props](#props)
12
+ - [Sizing](#sizing)
13
+ - [Rotation and animation](#rotation-and-animation)
14
+ - [Camera](#camera)
15
+ - [Styling faces](#styling-faces)
16
+ - [Centerpiece](#centerpiece)
17
+ - [Clicks, focus and accessibility](#clicks-focus-and-accessibility)
18
+ - [Edge cases](#edge-cases)
19
+ - [Overriding styles](#overriding-styles)
20
+ - [TypeScript](#typescript)
21
+ - [Known issues](#known-issues)
22
+ - [Developing](#developing)
23
+
24
+ ## Requirements
25
+
26
+ - React 19.
27
+ - A bundler that supports CSS Modules (Vite, Next.js, webpack with `css-loader`, …).
28
+ - A browser that supports `sibling-index()`, `sin()`/`cos()`, container queries (`container-type`, `cqw`/`cqh`), CSS nesting, `@layer`, and the individual `translate`/`rotate` properties. `sibling-index()` is the newest of these, so check its support first.
29
+
30
+ ## Installation
31
+
32
+ ```sh
33
+ npm install react-prism-carousel
34
+ ```
35
+
36
+ Import the component, and import the stylesheet once anywhere in your app (next to your root component, for example):
37
+
38
+ ```tsx
39
+ import Carousel from "react-prism-carousel";
40
+ import "react-prism-carousel/style.css";
41
+ ```
42
+
43
+ The package is ESM-only.
44
+
45
+ ### Using an AI coding assistant
46
+
47
+ The package includes a reference written for AI assistants, at `node_modules/react-prism-carousel/FOR-AI.md`, which always matches the version you installed. Assistants don't look inside `node_modules` on their own, so add this line to your project's agent instructions (`AGENTS.md`, `CLAUDE.md`, Cursor rules, …):
48
+
49
+ ```md
50
+ Before writing code that uses react-prism-carousel, read node_modules/react-prism-carousel/FOR-AI.md.
51
+ ```
52
+
53
+ It's also online at <https://unpkg.com/react-prism-carousel/FOR-AI.md>.
54
+
55
+ ## Quick start
56
+
57
+ ```tsx
58
+ import { useState } from "react";
59
+ import Carousel from "react-prism-carousel";
60
+ import "react-prism-carousel/style.css";
61
+
62
+ function Featured({ slides }) {
63
+ const [face, setFace] = useState(0);
64
+
65
+ return (
66
+ <>
67
+ {/* The carousel fills its parent, so the parent needs a height. */}
68
+ <div style={{ height: 320 }}>
69
+ <Carousel
70
+ orientation="horizontal"
71
+ aria-label="Featured products"
72
+ noChildrenElem={<p>Nothing to show yet.</p>}
73
+ currentFace={face}
74
+ transition="transform 0.6s ease"
75
+ >
76
+ {slides.map((slide) => (
77
+ <Slide key={slide.id} {...slide} />
78
+ ))}
79
+ </Carousel>
80
+ </div>
81
+
82
+ <button onClick={() => setFace((f) => f - 1)}>Previous</button>
83
+ <button onClick={() => setFace((f) => f + 1)}>Next</button>
84
+ </>
85
+ );
86
+ }
87
+ ```
88
+
89
+ There are two things to remember:
90
+
91
+ 1. **Give it a height.** The carousel is `100%` of its parent's width and height and doesn't grow with its content.
92
+ 2. **You own the rotation.** Change `currentFace` however you like. The component turns the prism to match.
93
+
94
+ ## How it works
95
+
96
+ With `n` sides (your children plus any `numEmptyFaces`), each child is wrapped in a `<span>` and placed on one side of a regular `n`-sided prism:
97
+
98
+ - **`orientation="horizontal"`** spins around the vertical axis. Faces sit side by side, each as tall as the carousel.
99
+ - **`orientation="vertical"`** spins around the horizontal axis. Faces are stacked, each as wide as the carousel.
100
+
101
+ By default the prism's corners touch the carousel's edges: the radius is half the carousel's width (or height, when vertical), so each face is `width × sin(180° / n)` wide. With 6 faces, each face is half the carousel's width; with 4, about 71%.
102
+
103
+ `currentFace` turns the whole prism so that face is at the front.
104
+
105
+ The rendered DOM:
106
+
107
+ ```
108
+ div role="region" aria-roledescription="carousel" ← the root: your other props go here
109
+ div ← the prism: prismProps go here
110
+ span role="group" aria-roledescription="slide" ← face wrapper: faceProps go here
111
+ {your child}
112
+ span …
113
+ div ← centerpiece wrapper
114
+ {centerpieceElem}
115
+ ```
116
+
117
+ ## Props
118
+
119
+ | Prop | Type | Default | |
120
+ |---|---|---|---|
121
+ | `orientation` | `"horizontal" \| "vertical"` | **required** | Which axis the prism spins around. |
122
+ | `noChildrenElem` | `ReactNode` | **required** | Shown, full size, when there are no children. |
123
+ | `children` | `ReactNode` | | One face per child. |
124
+ | `currentFace` | `number` | `0` | The face to bring to the front. Fractions and out-of-range values are allowed (see [Rotation](#rotation-and-animation)). |
125
+ | `numEmptyFaces` | `number` | `0` | Adds empty sides, turning the closed prism into an arc. Rounded down; negative values count as 0. |
126
+ | `faceSize` | `string` (CSS length) | | Each face's width (horizontal) or height (vertical). Sets the prism's radius instead of the default. |
127
+ | `concave` | `boolean` | `false` | Faces point inward, as if you were inside the prism. |
128
+ | `cameraOffset` | `string` (CSS length) | | Moves the camera: positive values bring it closer, negative values move it away. |
129
+ | `transition` | `string` | none | A CSS `transition` value for the prism. |
130
+ | `centerpieceElem` | `ReactNode` | | Content placed on the prism's axis, which the faces circle around. |
131
+ | `faceProps` | `FaceProps` or `(index, isFront) => FaceProps` | | HTML attributes for every face wrapper. |
132
+ | `prismProps` | `PrismProps` | | HTML attributes for the prism element. |
133
+ | any other prop | `HTMLAttributes<HTMLDivElement>` | | Passed to the root element. |
134
+
135
+ **Children.** Arrays are flattened. `null`, `undefined` and booleans are skipped, so `{cond && <Slide/>}` doesn't leave an empty face. A Fragment counts as one face. Children's `key`s are kept.
136
+
137
+ **Root props.** `className` is added to the component's own class. `style`, event handlers, `tabIndex`, `aria-*` and the rest are passed through unchanged. This is where CSS `perspective` and `perspective-origin` go (see [Camera](#camera)).
138
+
139
+ **`prismProps`.** `className` is merged. `style` is merged too, but the component's own CSS variables are applied after yours.
140
+
141
+ **`faceProps`.** Applied after the component's default `role`, `aria-roledescription` and `aria-label`, so you can override them. As a function, it receives the face's index and `isFront`: whether that face is the one at the front right now.
142
+
143
+ ## Sizing
144
+
145
+ The carousel is `100%` of its parent's width and height and uses `container-type: size`, which means **its size never depends on its content**. If its parent has no height, the carousel is 0px tall.
146
+
147
+ To make it a specific size, set `width`, `height`, `min-width` or `min-height` on the root with `style` or `className`. It can be bigger than its parent.
148
+
149
+ **`faceSize`** fixes the size of each face instead. The radius is then calculated from it: `radius = faceSize / (2 × sin(180° / n))`. Use it when faces must stay the same size however many sides there are (for example in an [arc](#arcs-and-scrollers)).
150
+
151
+ - Use any length: `px`, `rem`, or `cqw`/`cqh` to size relative to the carousel (`30cqw` is 30% of the carousel's width).
152
+ - **Don't use `%`.** The prism's depth is set with `transform-origin`, which doesn't accept percentages for Z, so the prism collapses. In development, the component warns about it in the console.
153
+ - A big `faceSize` makes a big prism, whose front face can come very close to the camera. Pull the camera back with a negative `cameraOffset`.
154
+
155
+ Anything outside the carousel's box is clipped (`contain: paint`). To show more of a large prism, make the carousel bigger.
156
+
157
+ **Gaps between faces:** add padding through `faceProps`. The wrappers use `box-sizing: border-box`, so padding shrinks your content, not the geometry.
158
+
159
+ ```tsx
160
+ faceProps={{ style: { paddingInline: 8 } }}
161
+ ```
162
+
163
+ ## Rotation and animation
164
+
165
+ ### `currentFace`
166
+
167
+ - **It isn't wrapped.** With 6 faces, `currentFace={6}` shows face 0 again and `-1` shows face 5. To loop smoothly, keep counting up (`f + 1`) instead of resetting to 0. Going from 5 back to 0 would spin the prism backwards through every face.
168
+ - **Fractions work.** `2.5` shows the prism halfway between faces 2 and 3, which is what you want for dragging (see below).
169
+ - **With `numEmptyFaces`, it's clamped** to `0 … children − 1`, so an empty side can never come to the front.
170
+ - `isFront` in `faceProps` uses the nearest whole face, wrapped into range.
171
+
172
+ ### `transition`
173
+
174
+ A plain CSS `transition` value, applied to the prism. The rotation is its `transform` and `cameraOffset` is its `translate`:
175
+
176
+ ```tsx
177
+ transition="transform 0.6s ease"
178
+ transition="transform 0.6s ease, translate 0.3s ease"
179
+ ```
180
+
181
+ The default is no transition: changes are instant.
182
+
183
+ For users with `prefers-reduced-motion: reduce`, the transition is switched off. A `transition` you set yourself in `prismProps.style` still applies; that's treated as your explicit choice.
184
+
185
+ ### Dragging
186
+
187
+ Drive `currentFace` with a fraction while dragging, turn the transition off during the drag, and round on release:
188
+
189
+ ```tsx
190
+ const [position, setPosition] = useState(0);
191
+ const [dragging, setDragging] = useState(false);
192
+ const start = useRef(null);
193
+
194
+ <Carousel
195
+ orientation="horizontal"
196
+ aria-label="Draggable carousel"
197
+ noChildrenElem={null}
198
+ currentFace={position}
199
+ transition={dragging ? undefined : "transform 0.4s ease-out"}
200
+ style={{ touchAction: "pan-y" }}
201
+ onPointerDown={(e) => {
202
+ e.currentTarget.setPointerCapture(e.pointerId);
203
+ start.current = { x: e.clientX, position };
204
+ setDragging(true);
205
+ }}
206
+ onPointerMove={(e) => {
207
+ if (!start.current) return;
208
+ const faceWidth = e.currentTarget.clientWidth / 2; // 6 faces: half the width
209
+ setPosition(start.current.position - (e.clientX - start.current.x) / faceWidth);
210
+ }}
211
+ onPointerUp={() => {
212
+ start.current = null;
213
+ setDragging(false);
214
+ setPosition((p) => Math.round(p));
215
+ }}
216
+ >
217
+ {slides}
218
+ </Carousel>
219
+ ```
220
+
221
+ ### Arcs and scrollers
222
+
223
+ `numEmptyFaces` adds invisible sides. A few real faces on a prism with many sides form a gentle arc, which works as a scroller with several faces visible at once. Set `faceSize` too, so the faces don't shrink as you add sides:
224
+
225
+ ```tsx
226
+ <Carousel
227
+ orientation="horizontal"
228
+ aria-label="Products"
229
+ noChildrenElem={null}
230
+ currentFace={face}
231
+ numEmptyFaces={9}
232
+ faceSize="30cqw"
233
+ transition="transform 0.5s ease"
234
+ faceProps={(i) => ({ onClick: () => setFace(i) })}
235
+ >
236
+ {products}
237
+ </Carousel>
238
+ ```
239
+
240
+ ## Camera
241
+
242
+ Three settings control what the viewer sees. Two of them are plain CSS on the root:
243
+
244
+ | | Set with | |
245
+ |---|---|---|
246
+ | Lens (field of view) | CSS `perspective` on the root | Default `1000px`. Smaller values give a wider view and stronger distortion. |
247
+ | Camera position, sideways and up/down | CSS `perspective-origin` on the root | Default `50% 50%`. `"50% -40%"` looks down on the prism from above. |
248
+ | Camera distance | the `cameraOffset` prop | Positive values bring the camera closer, negative values move it away. |
249
+
250
+ ```tsx
251
+ <Carousel
252
+ style={{ perspective: "600px", perspectiveOrigin: "50% -40%" }}
253
+ cameraOffset="-200px"
254
+ …
255
+ />
256
+ ```
257
+
258
+ `perspective-origin` moves the camera, but it keeps looking straight ahead, like a shifted lens. It doesn't turn to aim at the prism.
259
+
260
+ ### What the camera can see
261
+
262
+ - **From outside a convex prism**, you see the near half. The far half is turned away from you.
263
+ - **From outside a concave prism**, you see the inside of the far half. The near half is turned away from you.
264
+ - **From inside**, you only see what's in front of the camera. How much of the ring that is depends on `perspective`: at `1000px` on a carousel about 900px wide, the view is about 50° wide, roughly one face out of eight. Anything behind the camera isn't drawn.
265
+
266
+ Faces turned away from the camera are still drawn, mirrored, unless you hide their backs (see [Backfaces](#backfaces-and-card-backs)).
267
+
268
+ ### Putting the camera inside the prism
269
+
270
+ With `n` sides and radius `r` (half the carousel's width by default), the distance from the axis to each face is `a = r × cos(180° / n)`. To put the camera exactly on the axis:
271
+
272
+ - **convex:** `cameraOffset = perspective + a`
273
+ - **concave:** `cameraOffset = perspective − a`
274
+
275
+ For example, take 8 concave faces on a 918px-wide carousel: `r = 459px` and `a ≈ 424px`. With `perspective: 300px`, `cameraOffset="-124px"` puts the camera on the axis. A small `perspective` like that gives a wide enough view to see several faces around you.
276
+
277
+ ### Concave mode
278
+
279
+ `concave` turns every face inward. Because you're now looking at the inside of the ring, increasing `currentFace` turns the prism the other way on screen than in convex mode.
280
+
281
+ ## Styling faces
282
+
283
+ `faceProps` sets attributes on every face wrapper. Use an object for all faces, or a function for per-face values:
284
+
285
+ ```tsx
286
+ faceProps={(i, isFront) => ({
287
+ className: isFront ? "front" : "behind",
288
+ "aria-label": slides[i].title,
289
+ onClick: () => setFace(i),
290
+ })}
291
+ ```
292
+
293
+ **Dimming faces that aren't at the front:**
294
+
295
+ ```css
296
+ .behind > * {
297
+ filter: brightness(0.45);
298
+ }
299
+ ```
300
+
301
+ This styles your slide inside the wrapper rather than the wrapper itself. That matters for slides that use 3D themselves (card backs): `filter`, `opacity` and similar properties flatten 3D on the element they're applied to.
302
+
303
+ **Face height** (horizontal) or **width** (vertical) isn't part of the geometry, so you can change it. Faces stay centred:
304
+
305
+ ```tsx
306
+ faceProps={{ style: { height: "40%" } }}
307
+ ```
308
+
309
+ ### Backfaces and card backs
310
+
311
+ The browser draws faces that are turned away as their mirror image. You have three options:
312
+
313
+ - **Hide them completely:** set `backface-visibility: hidden` on your slide.
314
+ - **Show them mirrored:** do nothing.
315
+ - **Show a "card back"**, where the slide's background shows from both sides but its content only from the front. Set `transform-style: preserve-3d` on the slide and `backface-visibility: hidden` on its content:
316
+
317
+ ```css
318
+ .card {
319
+ transform-style: preserve-3d;
320
+ }
321
+ .card-content {
322
+ backface-visibility: hidden;
323
+ }
324
+ ```
325
+
326
+ ```tsx
327
+ <div className="card">
328
+ <span className="card-content">{title}</span>
329
+ </div>
330
+ ```
331
+
332
+ ## Centerpiece
333
+
334
+ `centerpieceElem` is placed on the prism's axis, with the faces circling around it. It turns against the prism's rotation, so it always faces the viewer. Faces in front of it cover it, and it covers faces behind it.
335
+
336
+ Its wrapper spans the carousel's whole box, so centre your content yourself:
337
+
338
+ ```tsx
339
+ <Carousel
340
+ orientation="horizontal"
341
+ aria-label="Products around a logo"
342
+ noChildrenElem={null}
343
+ currentFace={face}
344
+ faceSize="22cqw"
345
+ style={{ perspectiveOrigin: "50% -40%" }}
346
+ faceProps={{ style: { height: "40%" } }}
347
+ centerpieceElem={
348
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", height: "100%" }}>
349
+ <Logo />
350
+ </div>
351
+ }
352
+ >
353
+ {products}
354
+ </Carousel>
355
+ ```
356
+
357
+ Shorter faces and a raised camera, as in this example, let the centerpiece show above and below the ring.
358
+
359
+ ## Clicks, focus and accessibility
360
+
361
+ ### Clicks
362
+
363
+ Only what's inside the carousel's box is drawn and clickable (`contain: paint`). Faces that stick out of the box never cover or block neighbouring elements on the page.
364
+
365
+ Faces turned away from the viewer aren't made inert: in a scroller, several faces can be visible and clickable at once. (In 0.1.0, see [Known issues](#known-issues) for clicks that reach the wrong face.) If only the front face should be interactive and focusable:
366
+
367
+ ```tsx
368
+ faceProps={(i, isFront) => ({ inert: !isFront })}
369
+ ```
370
+
371
+ ### Keyboard
372
+
373
+ There's no built-in keyboard handling. Add it on the root:
374
+
375
+ ```tsx
376
+ <Carousel
377
+ tabIndex={0}
378
+ onKeyDown={(e) => {
379
+ if (e.key === "ArrowRight") setFace((f) => f + 1);
380
+ if (e.key === "ArrowLeft") setFace((f) => f - 1);
381
+ }}
382
+ …
383
+ />
384
+ ```
385
+
386
+ ### ARIA
387
+
388
+ The component follows the WAI-ARIA carousel pattern:
389
+
390
+ - The root has `role="region"` and `aria-roledescription="carousel"`. **Pass an `aria-label`** (or `aria-labelledby`) naming the carousel.
391
+ - Each face wrapper has `role="group"`, `aria-roledescription="slide"` and `aria-label="1 of 6"`. Override any of them through `faceProps`, for example to use the slide's title as its label.
392
+
393
+ All faces stay in the accessibility tree. Use the `inert` example above to hide all but the front face from screen readers too.
394
+
395
+ ## Edge cases
396
+
397
+ | Children | `numEmptyFaces` | Result |
398
+ |---|---|---|
399
+ | 0 | any | `noChildrenElem`, flat and full size. |
400
+ | 1 | 0 | The single face, flat and full size, with no rotation. |
401
+ | 1 | > 0 | One face on an arc. |
402
+ | 2 | 0 | A two-sided card: both faces are full size, back to back, and each turn flips it 180°. Hide backfaces so you don't see the other face mirrored. |
403
+
404
+ ## Overriding styles
405
+
406
+ The component's CSS lives in the cascade layer `react-prism-carousel.components`, so any CSS of yours that isn't in a layer overrides it. That includes CSS that breaks the prism. Avoid setting these:
407
+
408
+ - **On the prism and the face wrappers:** `position`, `inset`, `margin`, `transform`, `rotate`, `translate`, `transform-origin`, `transform-style`. On the face wrappers, also the width (horizontal) or height (vertical).
409
+ - **Properties that flatten 3D**, applied to the prism or the face wrappers: `overflow` other than `visible`, `opacity` below 1, `filter`, `clip-path`, `mask`, `mix-blend-mode`, `isolation: isolate`. On the prism, they flatten the whole carousel. On a face wrapper, they only affect slides that use 3D themselves. Put these effects on your slides instead.
410
+
411
+ Inside your slides, `cqw` and `cqh` refer to the carousel's root, since it's the nearest size container.
412
+
413
+ The `--prism-*` custom properties are internal and may change.
414
+
415
+ ## TypeScript
416
+
417
+ Types ship with the package:
418
+
419
+ ```ts
420
+ import Carousel, { type CarouselProps, type FaceProps, type PrismProps } from "react-prism-carousel";
421
+ // or the named export: import { Carousel } from "react-prism-carousel";
422
+ ```
423
+
424
+ - `CarouselProps`: every prop above, plus the root's HTML attributes.
425
+ - `FaceProps`: `React.HTMLAttributes<HTMLSpanElement>`.
426
+ - `PrismProps`: `React.HTMLAttributes<HTMLDivElement>`.
427
+
428
+ Every prop has a doc comment, so your editor shows its description on hover.
429
+
430
+ ## Known issues
431
+
432
+ These are known in 0.1.0 and will be fixed in a later version.
433
+
434
+ - **Clicks can reach the wrong face.** In a concave prism, a click on a visible face lands on the wrapper of the face opposite it, so that face's `faceProps` handlers run instead. In a convex prism, clicks on faces angled away from the viewer (every face but the front one) can land on the prism element instead. Clicks on the front face of a convex prism work. As a workaround, give the prism a class and add this unlayered CSS:
435
+
436
+ ```tsx
437
+ <Carousel prismProps={{ className: "prism-hit-fix" }} … />
438
+ ```
439
+
440
+ ```css
441
+ .prism-hit-fix { pointer-events: none; }
442
+ .prism-hit-fix > * { pointer-events: auto; backface-visibility: hidden; }
443
+ /* the centerpiece wrapper is always the prism's last child */
444
+ .prism-hit-fix > :last-child { pointer-events: none; }
445
+ .prism-hit-fix > :last-child > * { pointer-events: auto; }
446
+ ```
447
+
448
+ `backface-visibility` here applies only to the face wrappers, not to your slides, so it doesn't change what you see.
449
+ - **With `prefers-reduced-motion`, the centerpiece still animates** its turn while the prism changes instantly. Workaround, with the class above: `@media (prefers-reduced-motion: reduce) { .prism-hit-fix > :last-child { transition: none; } }`.
450
+ - **With exactly one face** (and no `numEmptyFaces`), changing `currentFace` with a transition spins the centerpiece a full turn. Keep `currentFace` at 0 when there's only one face.
451
+
452
+ ## Developing
453
+
454
+ ```sh
455
+ npm install --legacy-peer-deps # typescript-eslint doesn't accept TypeScript 7 as a peer yet
456
+ npm run dev # the demo page
457
+ npm run build # the package, into dist/
458
+ ```
459
+
460
+ The demo page in `demos/` has one section per feature: horizontal and vertical prisms, an arc scroller, dragging, `faceProps`, concave mode and the camera, card backs, the centerpiece, and the 0-, 1- and 2-face edge cases.
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ /** HTML attributes for each face's wrapper `<span>`. */
3
+ export type FaceProps = React.HTMLAttributes<HTMLSpanElement>;
4
+ /** HTML attributes for the prism `<div>`. */
5
+ export type PrismProps = React.HTMLAttributes<HTMLDivElement>;
6
+ /**
7
+ * Props for {@link Carousel}. Any other HTML attribute is passed to the root `<div>`
8
+ * (`className` is merged, `style` is passed as is): put `aria-label`, `tabIndex`,
9
+ * event handlers, and the CSS `perspective` / `perspectiveOrigin` there.
10
+ */
11
+ export interface CarouselProps extends React.HTMLAttributes<HTMLDivElement> {
12
+ /**
13
+ * One face per child. Arrays are flattened, `null` / `undefined` / booleans are
14
+ * skipped, and a Fragment counts as one face.
15
+ */
16
+ children?: React.ReactNode;
17
+ /**
18
+ * `"horizontal"` spins around the vertical axis (faces side by side, each as tall
19
+ * as the carousel); `"vertical"` spins around the horizontal axis.
20
+ */
21
+ orientation: "horizontal" | "vertical";
22
+ /** Shown, flat and full size, when there are no children. `null` is allowed. */
23
+ noChildrenElem: React.ReactNode;
24
+ /**
25
+ * Content on the prism's axis, turned so it always faces the viewer. Its wrapper
26
+ * fills the carousel's box, so centre the content yourself.
27
+ */
28
+ centerpieceElem?: React.ReactNode;
29
+ /**
30
+ * The face to bring to the front. Default 0. Not wrapped: with 6 faces, 6 shows
31
+ * face 0 again, so keep incrementing to loop smoothly. Fractions rotate partway
32
+ * (for dragging). Clamped to the real faces when `numEmptyFaces` > 0.
33
+ */
34
+ currentFace?: number;
35
+ /**
36
+ * Extra empty sides, which turn the closed prism into an arc. Default 0.
37
+ * Rounded down; negative values count as 0.
38
+ */
39
+ numEmptyFaces?: number;
40
+ /**
41
+ * Attributes for every face's wrapper `<span>`: one object for all faces, or a
42
+ * function for per-face values. Applied after the default ARIA attributes, so it
43
+ * can override them. `isFront`: this face is the one nearest the front.
44
+ */
45
+ faceProps?: FaceProps | ((index: number, isFront: boolean) => FaceProps);
46
+ /** Attributes for the prism `<div>`. `className` and `style` are merged. */
47
+ prismProps?: PrismProps;
48
+ /**
49
+ * Width (horizontal) or height (vertical) of each face, as a CSS length. The
50
+ * radius is calculated from it. Use px, rem, cqw or cqh, never % (the depth
51
+ * can't use %, so the prism collapses).
52
+ */
53
+ faceSize?: string;
54
+ /** Faces point inward, as if seen from inside the prism. Default false. */
55
+ concave?: boolean;
56
+ /**
57
+ * Moves the camera, as a CSS length: positive values bring it closer (eventually
58
+ * inside the prism), negative values move it away.
59
+ */
60
+ cameraOffset?: string;
61
+ /**
62
+ * CSS `transition` for the prism: rotation is `transform`, `cameraOffset` is
63
+ * `translate`. Default none. Switched off under `prefers-reduced-motion`.
64
+ */
65
+ transition?: string;
66
+ }
67
+ /**
68
+ * A 3D prism carousel: every child is one face. The component only draws the
69
+ * prism; keep `currentFace` in your own state and change it to turn it.
70
+ *
71
+ * The parent must have a height: the carousel fills it and never grows with its
72
+ * content. Import `react-prism-carousel/style.css` once.
73
+ *
74
+ * @see https://unpkg.com/react-prism-carousel/FOR-AI.md for AI coding assistants,
75
+ * also at node_modules/react-prism-carousel/FOR-AI.md
76
+ * @see https://github.com/BlueWebber/react-prism-carousel#readme
77
+ */
78
+ declare function Carousel({ children, orientation, noChildrenElem, centerpieceElem, faceProps, prismProps, currentFace, numEmptyFaces, faceSize, concave, cameraOffset, transition, ...props }: CarouselProps): React.JSX.Element;
79
+ export default Carousel;
@@ -0,0 +1,2 @@
1
+ export { default, default as Carousel } from "./Carousel.js";
2
+ export type { CarouselProps, FaceProps, PrismProps } from "./Carousel.js";
package/dist/index.js ADDED
@@ -0,0 +1,61 @@
1
+ import e from "react";
2
+ import { jsx as t, jsxs as n } from "react/jsx-runtime";
3
+ var r = {
4
+ carouselContainer: "_carouselContainer_1ax8l_4",
5
+ carousel: "_carousel_1ax8l_4",
6
+ centerpiece: "_centerpiece_1ax8l_36"
7
+ };
8
+ // src/Carousel.tsx
9
+ function i() {
10
+ try {
11
+ return process.env.NODE_ENV !== "production";
12
+ } catch {
13
+ return !1;
14
+ }
15
+ }
16
+ function a({ children: a, orientation: o, noChildrenElem: s, centerpieceElem: c, faceProps: l, prismProps: u, currentFace: d = 0, numEmptyFaces: f = 0, faceSize: p, concave: m = !1, cameraOffset: h, transition: g, ..._ }) {
17
+ e.useEffect(() => {
18
+ i() && p?.trim().endsWith("%") && console.warn(`Carousel: faceSize "${p}" uses %, which the 3D depth (transform-origin Z) doesn't accept. Use cqw (horizontal) or cqh (vertical) for sizes relative to the carousel.`);
19
+ }, [p]);
20
+ let v = e.Children.toArray(a), y = v.length, b = Number.isFinite(f) ? Math.max(0, Math.floor(f)) : 0, x = y === 0 || y + b === 1, S = d;
21
+ b && y > 0 && (S = Math.min(Math.max(d, 0), y - 1));
22
+ let C = y + b, w = -1;
23
+ if (y > 0 && Number.isFinite(S)) {
24
+ let e = (Math.round(S) % C + C) % C;
25
+ w = e < y ? e : -1;
26
+ }
27
+ return /* @__PURE__ */ t("div", {
28
+ role: "region",
29
+ "aria-roledescription": "carousel",
30
+ ..._,
31
+ className: [r.carouselContainer, _.className].filter(Boolean).join(" "),
32
+ children: /* @__PURE__ */ n("div", {
33
+ ...u,
34
+ "data-orientation": o,
35
+ "data-flat": x || void 0,
36
+ "data-facesize": p ? "manual" : void 0,
37
+ "data-concave": m || void 0,
38
+ style: {
39
+ ...u?.style,
40
+ "--prism-current-face": S,
41
+ "--prism-face-count": y,
42
+ "--prism-num-empty-faces": b,
43
+ "--prism-face-size": p,
44
+ "--prism-camera-offset": h,
45
+ "--prism-transition": g
46
+ },
47
+ className: [r.carousel, u?.className].filter(Boolean).join(" "),
48
+ children: [y === 0 ? s : v.map((n, r) => /* @__PURE__ */ t("span", {
49
+ role: "group",
50
+ "aria-roledescription": "slide",
51
+ "aria-label": `${r + 1} of ${y}`,
52
+ ...typeof l == "function" ? l(r, r === w) : l,
53
+ children: n
54
+ }, e.isValidElement(n) && n.key != null ? n.key : r)), /* @__PURE__ */ t("div", {
55
+ className: r.centerpiece,
56
+ children: c
57
+ })]
58
+ })
59
+ });
60
+ }
61
+ export { a as Carousel, a as default };
package/dist/style.css ADDED
@@ -0,0 +1,2 @@
1
+ @layer react-prism-carousel.components{._carouselContainer_1ax8l_4,._carousel_1ax8l_4,._carousel_1ax8l_4>*{box-sizing:border-box}._carouselContainer_1ax8l_4{perspective:1000px;contain:paint;width:100%;height:100%;display:flex;position:relative;container-type:size}._carousel_1ax8l_4[data-orientation=vertical]{--prism-radius:50cqh;--prism-rotate-deg:calc(360deg / (var(--prism-face-count) + var(--prism-num-empty-faces)));--prism-face-height:calc(sin(var(--prism-rotate-deg) / 2) * var(--prism-radius) * 2);--prism-origin-depth:calc(cos(var(--prism-rotate-deg) / 2) * var(--prism-radius) * var(--prism-depth-sign));transform:rotateX(calc(var(--prism-current-face) * var(--prism-rotate-deg) * -1))}._carousel_1ax8l_4[data-orientation=vertical]>:not(._centerpiece_1ax8l_36){height:var(--prism-face-height);width:100%;rotate:x calc((var(--prism-rotate-deg) * (sibling-index() - 1)))}._carousel_1ax8l_4[data-orientation=vertical]>._centerpiece_1ax8l_36{transform:rotateX(calc(var(--prism-current-face) * var(--prism-rotate-deg)))}._carousel_1ax8l_4[data-orientation=horizontal]{--prism-radius:50cqw;--prism-rotate-deg:calc(360deg / (var(--prism-face-count) + var(--prism-num-empty-faces)));--prism-face-width:calc(sin(var(--prism-rotate-deg) / 2) * var(--prism-radius) * 2);--prism-origin-depth:calc(cos(var(--prism-rotate-deg) / 2) * var(--prism-radius) * var(--prism-depth-sign));transform:rotateY(calc(var(--prism-current-face) * var(--prism-rotate-deg) * -1))}._carousel_1ax8l_4[data-orientation=horizontal]>:not(._centerpiece_1ax8l_36){width:var(--prism-face-width);height:100%;rotate:y calc((var(--prism-rotate-deg) * (sibling-index() - 1)))}._carousel_1ax8l_4[data-orientation=horizontal]>._centerpiece_1ax8l_36{transform:rotateY(calc(var(--prism-current-face) * var(--prism-rotate-deg)))}._carousel_1ax8l_4[data-facesize=manual]{--prism-radius:calc(var(--prism-face-size) / (2 * sin(var(--prism-rotate-deg) / 2)))}._carousel_1ax8l_4[data-concave]{--prism-depth-sign:1}._carousel_1ax8l_4[data-flat]{transform:none}._carousel_1ax8l_4[data-flat]>:not(._centerpiece_1ax8l_36){transform-origin:50%;width:100%;height:100%;rotate:none}._carousel_1ax8l_4{--prism-depth-sign:-1;width:100%;height:100%;transform-style:preserve-3d;transform-origin:50% 50% var(--prism-origin-depth);translate:0px 0px calc(var(--prism-camera-offset));transition:var(--prism-transition,none);display:flex;position:relative}._carousel_1ax8l_4>:not(._centerpiece_1ax8l_36){transform-origin:50% 50% var(--prism-origin-depth);transform-style:preserve-3d;margin:auto;position:absolute;inset:0}._centerpiece_1ax8l_36{transform-style:preserve-3d;translate:0px 0px var(--prism-origin-depth);transition:var(--prism-transition);margin:auto;position:absolute;inset:0}@media (prefers-reduced-motion:reduce){._carousel_1ax8l_4{transition:none}}}
2
+ /*$vite$:1*/
package/package.json CHANGED
@@ -1,6 +1,70 @@
1
1
  {
2
2
  "name": "react-prism-carousel",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A 3D prism carousel for React, drawn entirely in CSS. You control the rotation.",
5
+ "keywords": [
6
+ "react",
7
+ "carousel",
8
+ "3d",
9
+ "prism",
10
+ "css",
11
+ "slider",
12
+ "component"
13
+ ],
14
+ "license": "MIT",
15
+ "author": "BlueWebber",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/BlueWebber/react-prism-carousel.git"
19
+ },
20
+ "homepage": "https://github.com/BlueWebber/react-prism-carousel#readme",
21
+ "bugs": {
22
+ "url": "https://github.com/BlueWebber/react-prism-carousel/issues"
23
+ },
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./style.css": "./dist/style.css",
31
+ "./FOR-AI.md": "./FOR-AI.md",
32
+ "./package.json": "./package.json"
33
+ },
34
+ "main": "./dist/index.js",
35
+ "types": "./dist/index.d.ts",
36
+ "files": [
37
+ "dist",
38
+ "FOR-AI.md"
39
+ ],
40
+ "sideEffects": [
41
+ "**/*.css"
42
+ ],
43
+ "llms": "./FOR-AI.md",
44
+ "scripts": {
45
+ "dev": "vite",
46
+ "build": "tsc -b && vite build && tsc -p tsconfig.lib.json",
47
+ "lint": "eslint .",
48
+ "preview": "vite preview",
49
+ "prepublishOnly": "npm run build"
50
+ },
51
+ "peerDependencies": {
52
+ "react": "^19.0.0"
53
+ },
54
+ "devDependencies": {
55
+ "@eslint/js": "^10.0.1",
56
+ "@types/node": "^24.19.2",
57
+ "@types/react": "^19.3.0",
58
+ "@types/react-dom": "^19.3.0",
59
+ "@vitejs/plugin-react": "^6.1.2",
60
+ "eslint": "^10.13.0",
61
+ "eslint-plugin-react-hooks": "^7.1.1",
62
+ "eslint-plugin-react-refresh": "^0.5.7",
63
+ "globals": "^17.13.0",
64
+ "react": "^19.3.1",
65
+ "react-dom": "^19.3.1",
66
+ "typescript": "~7.0.2",
67
+ "typescript-eslint": "^8.72.0",
68
+ "vite": "^8.3.5"
69
+ }
70
+ }