+ {cards.map((card) => {
+ const depth = depthById.get(card.id) ?? 0;
+ const isFront = depth === 0;
+ const isDismissing = dismissingId === card.id;
+ const isActive = !isFront && !isDismissing && activeId === card.id;
+
+ let tx = 0;
+ let ty = 0;
+ const baseOffset = depth * peekSize;
+ if (peek === 'bottom') ty = baseOffset;
+ else if (peek === 'top') ty = -baseOffset;
+ else if (peek === 'right') tx = baseOffset;
+ else if (peek === 'left') tx = -baseOffset;
+
+ // Hover/focus "peek-a-little-more" nudge along the peek axis.
+ if (isActive && hoverPeek > 0) {
+ if (peek === 'bottom') ty += hoverPeek;
+ else if (peek === 'top') ty -= hoverPeek;
+ else if (peek === 'right') tx += hoverPeek;
+ else if (peek === 'left') tx -= hoverPeek;
+ }
+
+ // Depth treatment: top/bottom peek shrinks back cards (receding), while
+ // left/right peek rotates them (fanning). Only applied to non-front,
+ // non-dismissing cards.
+ const onVerticalAxis = peek === 'top' || peek === 'bottom';
+ const scale = onVerticalAxis ? Math.max(0.3, 1 - depth * depthScale) : 1;
+ const rotateSign = peek === 'right' ? 1 : peek === 'left' ? -1 : 0;
+ const rotate = rotateSign * depth * fanAngle;
+
+ if (isFront && swipeToDismiss) {
+ if (isDismissing) {
+ const offScreen = 720;
+ tx += dismissDir.x * offScreen;
+ ty += dismissDir.y * offScreen;
+ } else if (dragProgress > 0) {
+ tx += dismissDir.x * dragProgress;
+ ty += dismissDir.y * dragProgress;
+ }
+ }
+
+ const transformParts = [`translate3d(${tx}px, ${ty}px, 0)`];
+ if (scale !== 1) transformParts.push(`scale(${scale})`);
+ if (rotate !== 0) transformParts.push(`rotate(${rotate}deg)`);
+ const transform = transformParts.join(' ');
+
+ // While the front card is actively being dragged, remove transition so
+ // it tracks the pointer 1:1. Everything else (flick to front, release,
+ // dismiss animation) keeps the transition on.
+ const tracking = isFront && dragging && !isDismissing;
+ const transition = tracking
+ ? 'none'
+ : `transform ${duration}ms ${easing}, opacity ${duration}ms ${easing}`;
+
+ const zIndex = cards.length - depth;
+ const depthOpacity = isFront ? 1 : Math.max(0.25, 1 - depth * depthFade);
+ const opacity = isDismissing ? 0 : isActive ? 1 : depthOpacity;
+
+ const interactive = !isFront;
+ const swipeable = isFront && swipeToDismiss;
+
+ return (
+
handleBackCardActivate(card.id) : undefined}
+ onKeyDown={interactive ? (e) => handleBackKeyDown(e, card.id) : undefined}
+ onMouseEnter={interactive ? () => activateBack(card.id) : undefined}
+ onMouseLeave={interactive ? () => deactivateBack(card.id) : undefined}
+ onFocus={interactive ? () => activateBack(card.id) : undefined}
+ onBlur={interactive ? () => deactivateBack(card.id) : undefined}
+ onPointerDown={swipeable ? handleFrontPointerDown : undefined}
+ onPointerMove={swipeable ? handleFrontPointerMove : undefined}
+ onPointerUp={swipeable ? handleFrontPointerUp : undefined}
+ onPointerCancel={swipeable ? handleFrontPointerUp : undefined}
+ style={{
+ gridArea: 'stack',
+ transform,
+ transformOrigin: TRANSFORM_ORIGIN[peek],
+ transition,
+ zIndex,
+ opacity,
+ willChange: 'transform',
+ cursor: interactive
+ ? 'pointer'
+ : swipeable
+ ? dragging
+ ? 'grabbing'
+ : 'grab'
+ : undefined,
+ touchAction: swipeable ? 'none' : undefined,
+ userSelect: swipeable ? 'none' : undefined,
+ ...cardStyle,
+ }}
+ >
+ {card.node}
+
+ );
+ })}
+
+ );
+}
diff --git a/src/index.ts b/src/index.ts
index 9848e06..30fcb7d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -10,3 +10,10 @@ export { InspectorBubble } from './InspectorBubble';
export type { InspectorBubbleProps, InspectorBubbleColors, ElementInfo } from './InspectorBubble';
export { ZoomLens } from './ZoomLens';
export type { ZoomLensProps, ZoomLensBehavior, ZoomLensEvents, ZoomLensTarget } from './ZoomLens';
+export { FlickDeck } from './FlickDeck';
+export type {
+ FlickDeckProps,
+ FlickDeckPeek,
+ FlickDeckEvents,
+ FlickDeckAnimation,
+} from './FlickDeck';