MCPcopy Create free account
hub / github.com/dailydotdev/apps / ImageModal

Function ImageModal

packages/shared/src/components/modals/ImageModal.tsx:46–203  ·  view source on GitHub ↗
({
  onRequestClose,
  src,
  alt = 'Image',
  originRect,
}: ImageModalProps)

Source from the content-addressed store, hash-verified

44 * trapped under that portal at the same z-index.
45 */
46export default function ImageModal({
47 onRequestClose,
48 src,
49 alt = 'Image',
50 originRect,
51}: ImageModalProps): ReactElement | null {
52 const close = () => onRequestClose?.(undefined as never);
53 const imgRef = useRef<HTMLImageElement>(null);
54 const hasPlayedRef = useRef(false);
55
56 // FLIP: render the image at its final centered size, but start it transformed
57 // to sit over the clicked thumbnail, then animate the transform away so it
58 // visually grows from the thumbnail into the full view.
59 const playFlip = () => {
60 const img = imgRef.current;
61 if (!originRect || !img || hasPlayedRef.current) {
62 return;
63 }
64 const final = img.getBoundingClientRect();
65 if (!final.width || !final.height) {
66 // Not laid out yet (image still loading) — onLoad will retry.
67 return;
68 }
69 hasPlayedRef.current = true;
70
71 // A single uniform scale (not independent sx/sy) so the image keeps its
72 // aspect ratio and never warps mid-flight. `min` contains the start frame
73 // within the thumbnail's footprint, so it reads as lifting off from there.
74 const scale = Math.min(
75 originRect.width / final.width,
76 originRect.height / final.height,
77 );
78 // Translate centers together (transform-origin is the center too), so the
79 // image grows out of where the thumbnail sat rather than from a corner.
80 const dx =
81 originRect.left + originRect.width / 2 - (final.left + final.width / 2);
82 const dy =
83 originRect.top + originRect.height / 2 - (final.top + final.height / 2);
84 // Transform scales the corner radius along with everything else, so a fixed
85 // radius would render near-square at the small start scale and then "snap"
86 // round. Pre-divide by the scale so the rendered radius stays a constant
87 // ~16px (rounded-16) for the whole animation; read the resting value off the
88 // element so it tracks the class instead of a hardcoded number.
89 const restRadius =
90 parseFloat(getComputedStyle(img).borderTopLeftRadius) || 16;
91
92 img.style.transformOrigin = 'center center';
93 img.style.transition = 'none';
94 img.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
95 img.style.borderRadius = `${restRadius / scale}px`;
96 img.style.opacity = '1';
97 // Two frames: let the browser paint the start state first, then transition
98 // to identity — doing both in one frame can coalesce and snap.
99 requestAnimationFrame(() => {
100 requestAnimationFrame(() => {
101 img.style.transition =
102 'transform 300ms cubic-bezier(0.16, 1, 0.3, 1), border-radius 300ms cubic-bezier(0.16, 1, 0.3, 1)';
103 img.style.transform = 'none';

Callers

nothing calls this directly

Calls 3

useEventListenerFunction · 0.90
playFlipFunction · 0.85
closeFunction · 0.70

Tested by

no test coverage detected