{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "DomeGallery-JS-TW",
	"title": "DomeGallery",
	"description": "Immersive 3D dome gallery projecting images on a hemispheric surface.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "DomeGallery/DomeGallery.jsx",
			"content": "import { useEffect, useMemo, useRef, useCallback } from 'react';\nimport { useGesture } from '@use-gesture/react';\n\nconst DEFAULT_IMAGES = [\n  {\n    src: 'https://images.unsplash.com/photo-1755331039789-7e5680e26e8f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Abstract art'\n  },\n  {\n    src: 'https://images.unsplash.com/photo-1755569309049-98410b94f66d?q=80&w=772&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Modern sculpture'\n  },\n  {\n    src: 'https://images.unsplash.com/photo-1755497595318-7e5e3523854f?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Digital artwork'\n  },\n  {\n    src: 'https://images.unsplash.com/photo-1755353985163-c2a0fe5ac3d8?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Contemporary art'\n  },\n  {\n    src: 'https://images.unsplash.com/photo-1745965976680-d00be7dc0377?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Geometric pattern'\n  },\n  {\n    src: 'https://images.unsplash.com/photo-1752588975228-21f44630bb3c?q=80&w=774&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',\n    alt: 'Textured surface'\n  },\n  {\n    src: 'https://pbs.twimg.com/media/Gyla7NnXMAAXSo_?format=jpg&name=large',\n    alt: 'Social media image'\n  }\n];\n\nconst DEFAULTS = {\n  maxVerticalRotationDeg: 5,\n  dragSensitivity: 20,\n  enlargeTransitionMs: 300,\n  segments: 35\n};\n\nconst clamp = (v, min, max) => Math.min(Math.max(v, min), max);\nconst normalizeAngle = d => ((d % 360) + 360) % 360;\nconst wrapAngleSigned = deg => {\n  const a = (((deg + 180) % 360) + 360) % 360;\n  return a - 180;\n};\nconst getDataNumber = (el, name, fallback) => {\n  const attr = el.dataset[name] ?? el.getAttribute(`data-${name}`);\n  const n = attr == null ? NaN : parseFloat(attr);\n  return Number.isFinite(n) ? n : fallback;\n};\n\nfunction buildItems(pool, seg) {\n  const xCols = Array.from({ length: seg }, (_, i) => -37 + i * 2);\n  const evenYs = [-4, -2, 0, 2, 4];\n  const oddYs = [-3, -1, 1, 3, 5];\n\n  const coords = xCols.flatMap((x, c) => {\n    const ys = c % 2 === 0 ? evenYs : oddYs;\n    return ys.map(y => ({ x, y, sizeX: 2, sizeY: 2 }));\n  });\n\n  const totalSlots = coords.length;\n  if (pool.length === 0) {\n    return coords.map(c => ({ ...c, src: '', alt: '' }));\n  }\n  if (pool.length > totalSlots) {\n    console.warn(\n      `[DomeGallery] Provided image count (${pool.length}) exceeds available tiles (${totalSlots}). Some images will not be shown.`\n    );\n  }\n\n  const normalizedImages = pool.map(image => {\n    if (typeof image === 'string') {\n      return { src: image, alt: '' };\n    }\n    return { src: image.src || '', alt: image.alt || '' };\n  });\n\n  const usedImages = Array.from({ length: totalSlots }, (_, i) => normalizedImages[i % normalizedImages.length]);\n\n  for (let i = 1; i < usedImages.length; i++) {\n    if (usedImages[i].src === usedImages[i - 1].src) {\n      for (let j = i + 1; j < usedImages.length; j++) {\n        if (usedImages[j].src !== usedImages[i].src) {\n          const tmp = usedImages[i];\n          usedImages[i] = usedImages[j];\n          usedImages[j] = tmp;\n          break;\n        }\n      }\n    }\n  }\n\n  return coords.map((c, i) => ({\n    ...c,\n    src: usedImages[i].src,\n    alt: usedImages[i].alt\n  }));\n}\n\nfunction computeItemBaseRotation(offsetX, offsetY, sizeX, sizeY, segments) {\n  const unit = 360 / segments / 2;\n  const rotateY = unit * (offsetX + (sizeX - 1) / 2);\n  const rotateX = unit * (offsetY - (sizeY - 1) / 2);\n  return { rotateX, rotateY };\n}\n\nexport default function DomeGallery({\n  images = DEFAULT_IMAGES,\n  fit = 0.5,\n  fitBasis = 'auto',\n  minRadius = 600,\n  maxRadius = Infinity,\n  padFactor = 0.25,\n  overlayBlurColor = '#120F17',\n  maxVerticalRotationDeg = DEFAULTS.maxVerticalRotationDeg,\n  dragSensitivity = DEFAULTS.dragSensitivity,\n  enlargeTransitionMs = DEFAULTS.enlargeTransitionMs,\n  segments = DEFAULTS.segments,\n  dragDampening = 2,\n  openedImageWidth = '400px',\n  openedImageHeight = '400px',\n  imageBorderRadius = '30px',\n  openedImageBorderRadius = '30px',\n  grayscale = true\n}) {\n  const rootRef = useRef(null);\n  const mainRef = useRef(null);\n  const sphereRef = useRef(null);\n  const frameRef = useRef(null);\n  const viewerRef = useRef(null);\n  const scrimRef = useRef(null);\n  const focusedElRef = useRef(null);\n  const originalTilePositionRef = useRef(null);\n\n  const rotationRef = useRef({ x: 0, y: 0 });\n  const startRotRef = useRef({ x: 0, y: 0 });\n  const startPosRef = useRef(null);\n  const draggingRef = useRef(false);\n  const cancelTapRef = useRef(false);\n  const movedRef = useRef(false);\n  const inertiaRAF = useRef(null);\n  const pointerTypeRef = useRef('mouse');\n  const tapTargetRef = useRef(null);\n  const openingRef = useRef(false);\n  const openStartedAtRef = useRef(0);\n  const lastDragEndAt = useRef(0);\n\n  const scrollLockedRef = useRef(false);\n  const lockScroll = useCallback(() => {\n    if (scrollLockedRef.current) return;\n    scrollLockedRef.current = true;\n    document.body.classList.add('dg-scroll-lock');\n  }, []);\n  const unlockScroll = useCallback(() => {\n    if (!scrollLockedRef.current) return;\n    if (rootRef.current?.getAttribute('data-enlarging') === 'true') return;\n    scrollLockedRef.current = false;\n    document.body.classList.remove('dg-scroll-lock');\n  }, []);\n\n  const items = useMemo(() => buildItems(images, segments), [images, segments]);\n\n  const applyTransform = (xDeg, yDeg) => {\n    const el = sphereRef.current;\n    if (el) {\n      el.style.transform = `translateZ(calc(var(--radius) * -1)) rotateX(${xDeg}deg) rotateY(${yDeg}deg)`;\n    }\n  };\n\n  const lockedRadiusRef = useRef(null);\n\n  useEffect(() => {\n    const root = rootRef.current;\n    if (!root) return;\n    const ro = new ResizeObserver(entries => {\n      const cr = entries[0].contentRect;\n      const w = Math.max(1, cr.width),\n        h = Math.max(1, cr.height);\n      const minDim = Math.min(w, h),\n        maxDim = Math.max(w, h),\n        aspect = w / h;\n      let basis;\n      switch (fitBasis) {\n        case 'min':\n          basis = minDim;\n          break;\n        case 'max':\n          basis = maxDim;\n          break;\n        case 'width':\n          basis = w;\n          break;\n        case 'height':\n          basis = h;\n          break;\n        default:\n          basis = aspect >= 1.3 ? w : minDim;\n      }\n      let radius = basis * fit;\n      const heightGuard = h * 1.35;\n      radius = Math.min(radius, heightGuard);\n      radius = clamp(radius, minRadius, maxRadius);\n      lockedRadiusRef.current = Math.round(radius);\n\n      const viewerPad = Math.max(8, Math.round(minDim * padFactor));\n      root.style.setProperty('--radius', `${lockedRadiusRef.current}px`);\n      root.style.setProperty('--viewer-pad', `${viewerPad}px`);\n      root.style.setProperty('--overlay-blur-color', overlayBlurColor);\n      root.style.setProperty('--tile-radius', imageBorderRadius);\n      root.style.setProperty('--enlarge-radius', openedImageBorderRadius);\n      root.style.setProperty('--image-filter', grayscale ? 'grayscale(1)' : 'none');\n      applyTransform(rotationRef.current.x, rotationRef.current.y);\n\n      const enlargedOverlay = viewerRef.current?.querySelector('.enlarge');\n      if (enlargedOverlay && frameRef.current && mainRef.current) {\n        const frameR = frameRef.current.getBoundingClientRect();\n        const mainR = mainRef.current.getBoundingClientRect();\n\n        const hasCustomSize = openedImageWidth && openedImageHeight;\n        if (hasCustomSize) {\n          const tempDiv = document.createElement('div');\n          tempDiv.style.cssText = `position: absolute; width: ${openedImageWidth}; height: ${openedImageHeight}; visibility: hidden;`;\n          document.body.appendChild(tempDiv);\n          const tempRect = tempDiv.getBoundingClientRect();\n          document.body.removeChild(tempDiv);\n\n          const centeredLeft = frameR.left - mainR.left + (frameR.width - tempRect.width) / 2;\n          const centeredTop = frameR.top - mainR.top + (frameR.height - tempRect.height) / 2;\n\n          enlargedOverlay.style.left = `${centeredLeft}px`;\n          enlargedOverlay.style.top = `${centeredTop}px`;\n        } else {\n          enlargedOverlay.style.left = `${frameR.left - mainR.left}px`;\n          enlargedOverlay.style.top = `${frameR.top - mainR.top}px`;\n          enlargedOverlay.style.width = `${frameR.width}px`;\n          enlargedOverlay.style.height = `${frameR.height}px`;\n        }\n      }\n    });\n    ro.observe(root);\n    return () => ro.disconnect();\n  }, [\n    fit,\n    fitBasis,\n    minRadius,\n    maxRadius,\n    padFactor,\n    overlayBlurColor,\n    grayscale,\n    imageBorderRadius,\n    openedImageBorderRadius,\n    openedImageWidth,\n    openedImageHeight\n  ]);\n\n  useEffect(() => {\n    applyTransform(rotationRef.current.x, rotationRef.current.y);\n  }, []);\n\n  const stopInertia = useCallback(() => {\n    if (inertiaRAF.current) {\n      cancelAnimationFrame(inertiaRAF.current);\n      inertiaRAF.current = null;\n    }\n  }, []);\n\n  const startInertia = useCallback(\n    (vx, vy) => {\n      const MAX_V = 1.4;\n      let vX = clamp(vx, -MAX_V, MAX_V) * 80;\n      let vY = clamp(vy, -MAX_V, MAX_V) * 80;\n      let frames = 0;\n      const d = clamp(dragDampening ?? 0.6, 0, 1);\n      const frictionMul = 0.94 + 0.055 * d;\n      const stopThreshold = 0.015 - 0.01 * d;\n      const maxFrames = Math.round(90 + 270 * d);\n      const step = () => {\n        vX *= frictionMul;\n        vY *= frictionMul;\n        if (Math.abs(vX) < stopThreshold && Math.abs(vY) < stopThreshold) {\n          inertiaRAF.current = null;\n          return;\n        }\n        if (++frames > maxFrames) {\n          inertiaRAF.current = null;\n          return;\n        }\n        const nextX = clamp(rotationRef.current.x - vY / 200, -maxVerticalRotationDeg, maxVerticalRotationDeg);\n        const nextY = wrapAngleSigned(rotationRef.current.y + vX / 200);\n        rotationRef.current = { x: nextX, y: nextY };\n        applyTransform(nextX, nextY);\n        inertiaRAF.current = requestAnimationFrame(step);\n      };\n      stopInertia();\n      inertiaRAF.current = requestAnimationFrame(step);\n    },\n    [dragDampening, maxVerticalRotationDeg, stopInertia]\n  );\n\n  useGesture(\n    {\n      onDragStart: ({ event }) => {\n        if (focusedElRef.current) return;\n        stopInertia();\n\n        pointerTypeRef.current = event.pointerType || 'mouse';\n        if (pointerTypeRef.current === 'touch') event.preventDefault();\n        if (pointerTypeRef.current === 'touch') lockScroll();\n        draggingRef.current = true;\n        cancelTapRef.current = false;\n        movedRef.current = false;\n        startRotRef.current = { ...rotationRef.current };\n        startPosRef.current = { x: event.clientX, y: event.clientY };\n        const potential = event.target.closest?.('.item__image');\n        tapTargetRef.current = potential || null;\n      },\n      onDrag: ({ event, last, velocity: velArr = [0, 0], direction: dirArr = [0, 0], movement }) => {\n        if (focusedElRef.current || !draggingRef.current || !startPosRef.current) return;\n\n        if (pointerTypeRef.current === 'touch') event.preventDefault();\n\n        const dxTotal = event.clientX - startPosRef.current.x;\n        const dyTotal = event.clientY - startPosRef.current.y;\n\n        if (!movedRef.current) {\n          const dist2 = dxTotal * dxTotal + dyTotal * dyTotal;\n          if (dist2 > 16) movedRef.current = true;\n        }\n\n        const nextX = clamp(\n          startRotRef.current.x - dyTotal / dragSensitivity,\n          -maxVerticalRotationDeg,\n          maxVerticalRotationDeg\n        );\n        const nextY = startRotRef.current.y + dxTotal / dragSensitivity;\n\n        const cur = rotationRef.current;\n        if (cur.x !== nextX || cur.y !== nextY) {\n          rotationRef.current = { x: nextX, y: nextY };\n          applyTransform(nextX, nextY);\n        }\n\n        if (last) {\n          draggingRef.current = false;\n          let isTap = false;\n\n          if (startPosRef.current) {\n            const dx = event.clientX - startPosRef.current.x;\n            const dy = event.clientY - startPosRef.current.y;\n            const dist2 = dx * dx + dy * dy;\n            const TAP_THRESH_PX = pointerTypeRef.current === 'touch' ? 10 : 6;\n            if (dist2 <= TAP_THRESH_PX * TAP_THRESH_PX) {\n              isTap = true;\n            }\n          }\n\n          let [vMagX, vMagY] = velArr;\n          const [dirX, dirY] = dirArr;\n          let vx = vMagX * dirX;\n          let vy = vMagY * dirY;\n\n          if (!isTap && Math.abs(vx) < 0.001 && Math.abs(vy) < 0.001 && Array.isArray(movement)) {\n            const [mx, my] = movement;\n            vx = (mx / dragSensitivity) * 0.02;\n            vy = (my / dragSensitivity) * 0.02;\n          }\n\n          if (!isTap && (Math.abs(vx) > 0.005 || Math.abs(vy) > 0.005)) {\n            startInertia(vx, vy);\n          }\n          startPosRef.current = null;\n          cancelTapRef.current = !isTap;\n\n          if (isTap && tapTargetRef.current && !focusedElRef.current) {\n            openItemFromElement(tapTargetRef.current);\n          }\n          tapTargetRef.current = null;\n\n          if (cancelTapRef.current) setTimeout(() => (cancelTapRef.current = false), 120);\n          if (movedRef.current) lastDragEndAt.current = performance.now();\n          movedRef.current = false;\n          if (pointerTypeRef.current === 'touch') unlockScroll();\n        }\n      }\n    },\n    { target: mainRef, eventOptions: { passive: false } }\n  );\n\n  useEffect(() => {\n    const scrim = scrimRef.current;\n    if (!scrim) return;\n\n    const close = () => {\n      if (performance.now() - openStartedAtRef.current < 250) return;\n      const el = focusedElRef.current;\n      if (!el) return;\n      const parent = el.parentElement;\n      const overlay = viewerRef.current?.querySelector('.enlarge');\n      if (!overlay) return;\n\n      const refDiv = parent.querySelector('.item__image--reference');\n\n      const originalPos = originalTilePositionRef.current;\n      if (!originalPos) {\n        overlay.remove();\n        if (refDiv) refDiv.remove();\n        parent.style.setProperty('--rot-y-delta', `0deg`);\n        parent.style.setProperty('--rot-x-delta', `0deg`);\n        el.style.visibility = '';\n        el.style.zIndex = 0;\n        focusedElRef.current = null;\n        rootRef.current?.removeAttribute('data-enlarging');\n        openingRef.current = false;\n        return;\n      }\n\n      const currentRect = overlay.getBoundingClientRect();\n      const rootRect = rootRef.current.getBoundingClientRect();\n\n      const originalPosRelativeToRoot = {\n        left: originalPos.left - rootRect.left,\n        top: originalPos.top - rootRect.top,\n        width: originalPos.width,\n        height: originalPos.height\n      };\n\n      const overlayRelativeToRoot = {\n        left: currentRect.left - rootRect.left,\n        top: currentRect.top - rootRect.top,\n        width: currentRect.width,\n        height: currentRect.height\n      };\n\n      const animatingOverlay = document.createElement('div');\n      animatingOverlay.className = 'enlarge-closing';\n      animatingOverlay.style.cssText = `\n        position: absolute;\n        left: ${overlayRelativeToRoot.left}px;\n        top: ${overlayRelativeToRoot.top}px;\n        width: ${overlayRelativeToRoot.width}px;\n        height: ${overlayRelativeToRoot.height}px;\n        z-index: 9999;\n        border-radius: ${openedImageBorderRadius};\n        overflow: hidden;\n        box-shadow: 0 10px 30px rgba(0,0,0,.35);\n        transition: all ${enlargeTransitionMs}ms ease-out;\n        pointer-events: none;\n        margin: 0;\n        transform: none;\n        filter: ${grayscale ? 'grayscale(1)' : 'none'};\n      `;\n\n      const originalImg = overlay.querySelector('img');\n      if (originalImg) {\n        const img = originalImg.cloneNode();\n        img.style.cssText = 'width: 100%; height: 100%; object-fit: cover;';\n        animatingOverlay.appendChild(img);\n      }\n\n      overlay.remove();\n      rootRef.current.appendChild(animatingOverlay);\n\n      void animatingOverlay.getBoundingClientRect();\n\n      requestAnimationFrame(() => {\n        animatingOverlay.style.left = originalPosRelativeToRoot.left + 'px';\n        animatingOverlay.style.top = originalPosRelativeToRoot.top + 'px';\n        animatingOverlay.style.width = originalPosRelativeToRoot.width + 'px';\n        animatingOverlay.style.height = originalPosRelativeToRoot.height + 'px';\n        animatingOverlay.style.opacity = '0';\n      });\n\n      const cleanup = () => {\n        animatingOverlay.remove();\n        originalTilePositionRef.current = null;\n\n        if (refDiv) refDiv.remove();\n        parent.style.transition = 'none';\n        el.style.transition = 'none';\n\n        parent.style.setProperty('--rot-y-delta', `0deg`);\n        parent.style.setProperty('--rot-x-delta', `0deg`);\n\n        requestAnimationFrame(() => {\n          el.style.visibility = '';\n          el.style.opacity = '0';\n          el.style.zIndex = 0;\n          focusedElRef.current = null;\n          rootRef.current?.removeAttribute('data-enlarging');\n\n          requestAnimationFrame(() => {\n            parent.style.transition = '';\n            el.style.transition = 'opacity 300ms ease-out';\n\n            requestAnimationFrame(() => {\n              el.style.opacity = '1';\n              setTimeout(() => {\n                el.style.transition = '';\n                el.style.opacity = '';\n                openingRef.current = false;\n                if (!draggingRef.current && rootRef.current?.getAttribute('data-enlarging') !== 'true')\n                  document.body.classList.remove('dg-scroll-lock');\n              }, 300);\n            });\n          });\n        });\n      };\n\n      animatingOverlay.addEventListener('transitionend', cleanup, {\n        once: true\n      });\n    };\n\n    scrim.addEventListener('click', close);\n    const onKey = e => {\n      if (e.key === 'Escape') close();\n    };\n    window.addEventListener('keydown', onKey);\n\n    return () => {\n      scrim.removeEventListener('click', close);\n      window.removeEventListener('keydown', onKey);\n    };\n  }, [enlargeTransitionMs, openedImageBorderRadius, grayscale]);\n\n  const openItemFromElement = el => {\n    if (openingRef.current) return;\n    openingRef.current = true;\n    openStartedAtRef.current = performance.now();\n    lockScroll();\n    const parent = el.parentElement;\n    focusedElRef.current = el;\n    el.setAttribute('data-focused', 'true');\n\n    const offsetX = getDataNumber(parent, 'offsetX', 0);\n    const offsetY = getDataNumber(parent, 'offsetY', 0);\n    const sizeX = getDataNumber(parent, 'sizeX', 2);\n    const sizeY = getDataNumber(parent, 'sizeY', 2);\n\n    const parentRot = computeItemBaseRotation(offsetX, offsetY, sizeX, sizeY, segments);\n    const parentY = normalizeAngle(parentRot.rotateY);\n    const globalY = normalizeAngle(rotationRef.current.y);\n    let rotY = -(parentY + globalY) % 360;\n    if (rotY < -180) rotY += 360;\n    const rotX = -parentRot.rotateX - rotationRef.current.x;\n\n    parent.style.setProperty('--rot-y-delta', `${rotY}deg`);\n    parent.style.setProperty('--rot-x-delta', `${rotX}deg`);\n\n    const refDiv = document.createElement('div');\n    refDiv.className = 'item__image item__image--reference opacity-0';\n    refDiv.style.transform = `rotateX(${-parentRot.rotateX}deg) rotateY(${-parentRot.rotateY}deg)`;\n    parent.appendChild(refDiv);\n\n    void refDiv.offsetHeight;\n\n    const tileR = refDiv.getBoundingClientRect();\n    const mainR = mainRef.current?.getBoundingClientRect();\n    const frameR = frameRef.current?.getBoundingClientRect();\n\n    if (!mainR || !frameR || tileR.width <= 0 || tileR.height <= 0) {\n      openingRef.current = false;\n      focusedElRef.current = null;\n      parent.removeChild(refDiv);\n      unlockScroll();\n      return;\n    }\n\n    originalTilePositionRef.current = {\n      left: tileR.left,\n      top: tileR.top,\n      width: tileR.width,\n      height: tileR.height\n    };\n\n    el.style.visibility = 'hidden';\n    el.style.zIndex = 0;\n\n    const overlay = document.createElement('div');\n    overlay.className = 'enlarge';\n    overlay.style.position = 'absolute';\n    overlay.style.left = frameR.left - mainR.left + 'px';\n    overlay.style.top = frameR.top - mainR.top + 'px';\n    overlay.style.width = frameR.width + 'px';\n    overlay.style.height = frameR.height + 'px';\n    overlay.style.opacity = '0';\n    overlay.style.zIndex = '30';\n    overlay.style.willChange = 'transform, opacity';\n    overlay.style.transformOrigin = 'top left';\n    overlay.style.transition = `transform ${enlargeTransitionMs}ms ease, opacity ${enlargeTransitionMs}ms ease`;\n    overlay.style.borderRadius = openedImageBorderRadius;\n    overlay.style.overflow = 'hidden';\n    overlay.style.boxShadow = '0 10px 30px rgba(0,0,0,.35)';\n\n    const rawSrc = parent.dataset.src || el.querySelector('img')?.src || '';\n    const rawAlt = parent.dataset.alt || el.querySelector('img')?.alt || '';\n    const img = document.createElement('img');\n    img.src = rawSrc;\n    img.alt = rawAlt;\n    img.style.width = '100%';\n    img.style.height = '100%';\n    img.style.objectFit = 'cover';\n    img.style.filter = grayscale ? 'grayscale(1)' : 'none';\n    overlay.appendChild(img);\n    viewerRef.current.appendChild(overlay);\n\n    const tx0 = tileR.left - frameR.left;\n    const ty0 = tileR.top - frameR.top;\n    const sx0 = tileR.width / frameR.width;\n    const sy0 = tileR.height / frameR.height;\n\n    const validSx0 = isFinite(sx0) && sx0 > 0 ? sx0 : 1;\n    const validSy0 = isFinite(sy0) && sy0 > 0 ? sy0 : 1;\n\n    overlay.style.transform = `translate(${tx0}px, ${ty0}px) scale(${validSx0}, ${validSy0})`;\n\n    setTimeout(() => {\n      if (!overlay.parentElement) return;\n      overlay.style.opacity = '1';\n      overlay.style.transform = 'translate(0px, 0px) scale(1, 1)';\n      rootRef.current?.setAttribute('data-enlarging', 'true');\n    }, 16);\n\n    const wantsResize = openedImageWidth || openedImageHeight;\n    if (wantsResize) {\n      const onFirstEnd = ev => {\n        if (ev.propertyName !== 'transform') return;\n        overlay.removeEventListener('transitionend', onFirstEnd);\n        const prevTransition = overlay.style.transition;\n        overlay.style.transition = 'none';\n        const tempWidth = openedImageWidth || `${frameR.width}px`;\n        const tempHeight = openedImageHeight || `${frameR.height}px`;\n        overlay.style.width = tempWidth;\n        overlay.style.height = tempHeight;\n        const newRect = overlay.getBoundingClientRect();\n        overlay.style.width = frameR.width + 'px';\n        overlay.style.height = frameR.height + 'px';\n        void overlay.offsetWidth;\n        overlay.style.transition = `left ${enlargeTransitionMs}ms ease, top ${enlargeTransitionMs}ms ease, width ${enlargeTransitionMs}ms ease, height ${enlargeTransitionMs}ms ease`;\n        const centeredLeft = frameR.left - mainR.left + (frameR.width - newRect.width) / 2;\n        const centeredTop = frameR.top - mainR.top + (frameR.height - newRect.height) / 2;\n        requestAnimationFrame(() => {\n          overlay.style.left = `${centeredLeft}px`;\n          overlay.style.top = `${centeredTop}px`;\n          overlay.style.width = tempWidth;\n          overlay.style.height = tempHeight;\n        });\n        const cleanupSecond = () => {\n          overlay.removeEventListener('transitionend', cleanupSecond);\n          overlay.style.transition = prevTransition;\n        };\n        overlay.addEventListener('transitionend', cleanupSecond, {\n          once: true\n        });\n      };\n      overlay.addEventListener('transitionend', onFirstEnd);\n    }\n  };\n\n  useEffect(() => {\n    return () => {\n      document.body.classList.remove('dg-scroll-lock');\n    };\n  }, []);\n\n  const cssStyles = `\n    .sphere-root {\n      --radius: 520px;\n      --viewer-pad: 72px;\n      --circ: calc(var(--radius) * 3.14);\n      --rot-y: calc((360deg / var(--segments-x)) / 2);\n      --rot-x: calc((360deg / var(--segments-y)) / 2);\n      --item-width: calc(var(--circ) / var(--segments-x));\n      --item-height: calc(var(--circ) / var(--segments-y));\n    }\n    \n    .sphere-root * {\n      box-sizing: border-box;\n    }\n    .sphere, .sphere-item, .item__image { transform-style: preserve-3d; }\n    \n    .stage {\n      width: 100%;\n      height: 100%;\n      display: grid;\n      place-items: center;\n      position: absolute;\n      inset: 0;\n      margin: auto;\n      perspective: calc(var(--radius) * 2);\n      perspective-origin: 50% 50%;\n    }\n    \n    .sphere {\n      transform: translateZ(calc(var(--radius) * -1));\n      will-change: transform;\n      position: absolute;\n    }\n    \n    .sphere-item {\n      width: calc(var(--item-width) * var(--item-size-x));\n      height: calc(var(--item-height) * var(--item-size-y));\n      position: absolute;\n      top: -999px;\n      bottom: -999px;\n      left: -999px;\n      right: -999px;\n      margin: auto;\n      transform-origin: 50% 50%;\n      backface-visibility: hidden;\n      transition: transform 300ms;\n      transform: rotateY(calc(var(--rot-y) * (var(--offset-x) + ((var(--item-size-x) - 1) / 2)) + var(--rot-y-delta, 0deg))) \n                 rotateX(calc(var(--rot-x) * (var(--offset-y) - ((var(--item-size-y) - 1) / 2)) + var(--rot-x-delta, 0deg))) \n                 translateZ(var(--radius));\n    }\n    \n    .sphere-root[data-enlarging=\"true\"] .scrim {\n      opacity: 1 !important;\n      pointer-events: all !important;\n    }\n    \n    @media (max-aspect-ratio: 1/1) {\n      .viewer-frame {\n        height: auto !important;\n        width: 100% !important;\n      }\n    }\n    \n    // body.dg-scroll-lock {\n    //   position: fixed !important;\n    //   top: 0;\n    //   left: 0;\n    //   width: 100% !important;\n    //   height: 100% !important;\n    //   overflow: hidden !important;\n    //   touch-action: none !important;\n    //   overscroll-behavior: contain !important;\n    // }\n    .item__image {\n      position: absolute;\n      inset: 10px;\n      border-radius: var(--tile-radius, 12px);\n      overflow: hidden;\n      cursor: pointer;\n      backface-visibility: hidden;\n      -webkit-backface-visibility: hidden;\n      transition: transform 300ms;\n      pointer-events: auto;\n      -webkit-transform: translateZ(0);\n      transform: translateZ(0);\n    }\n    .item__image--reference {\n      position: absolute;\n      inset: 10px;\n      pointer-events: none;\n    }\n  `;\n\n  return (\n    <>\n      <style dangerouslySetInnerHTML={{ __html: cssStyles }} />\n      <div\n        ref={rootRef}\n        className=\"sphere-root relative w-full h-full\"\n        style={{\n          ['--segments-x']: segments,\n          ['--segments-y']: segments,\n          ['--overlay-blur-color']: overlayBlurColor,\n          ['--tile-radius']: imageBorderRadius,\n          ['--enlarge-radius']: openedImageBorderRadius,\n          ['--image-filter']: grayscale ? 'grayscale(1)' : 'none'\n        }}\n      >\n        <main\n          ref={mainRef}\n          className=\"absolute inset-0 grid place-items-center overflow-hidden select-none\"\n          style={{\n            touchAction: 'none',\n            WebkitUserSelect: 'none',\n            backgroundColor: `var(--overlay-blur-color, ${overlayBlurColor})`\n          }}\n        >\n          <div className=\"stage\">\n            <div ref={sphereRef} className=\"sphere\">\n              {items.map((it, i) => (\n                <div\n                  key={`${it.x},${it.y},${i}`}\n                  className=\"sphere-item absolute m-auto\"\n                  data-src={it.src}\n                  data-alt={it.alt}\n                  data-offset-x={it.x}\n                  data-offset-y={it.y}\n                  data-size-x={it.sizeX}\n                  data-size-y={it.sizeY}\n                  style={{\n                    ['--offset-x']: it.x,\n                    ['--offset-y']: it.y,\n                    ['--item-size-x']: it.sizeX,\n                    ['--item-size-y']: it.sizeY,\n                    top: '-999px',\n                    bottom: '-999px',\n                    left: '-999px',\n                    right: '-999px'\n                  }}\n                >\n                  <div\n                    className=\"item__image absolute block overflow-hidden cursor-pointer transition-transform duration-300\"\n                    role=\"button\"\n                    tabIndex={0}\n                    aria-label={it.alt || 'Open image'}\n                    onClick={e => {\n                      if (draggingRef.current) return;\n                      if (movedRef.current) return;\n                      if (performance.now() - lastDragEndAt.current < 80) return;\n                      if (openingRef.current) return;\n                      openItemFromElement(e.currentTarget);\n                    }}\n                    onPointerUp={e => {\n                      if (e.pointerType !== 'touch') return;\n                      if (draggingRef.current) return;\n                      if (movedRef.current) return;\n                      if (performance.now() - lastDragEndAt.current < 80) return;\n                      if (openingRef.current) return;\n                      openItemFromElement(e.currentTarget);\n                    }}\n                    style={{\n                      inset: '10px',\n                      borderRadius: `var(--tile-radius, ${imageBorderRadius})`,\n                      backfaceVisibility: 'hidden',\n                      backgroundColor: `var(--overlay-blur-color, ${overlayBlurColor})`\n                    }}\n                  >\n                    <img\n                      src={it.src}\n                      draggable={false}\n                      alt={it.alt}\n                      className=\"w-full h-full object-cover pointer-events-none\"\n                      style={{\n                        backfaceVisibility: 'hidden',\n                        filter: `var(--image-filter, ${grayscale ? 'grayscale(1)' : 'none'})`\n                      }}\n                    />\n                  </div>\n                </div>\n              ))}\n            </div>\n          </div>\n\n          <div\n            className=\"absolute inset-0 m-auto z-[3] pointer-events-none\"\n            style={{\n              backgroundImage: `radial-gradient(rgba(235, 235, 235, 0) 65%, var(--overlay-blur-color, ${overlayBlurColor}) 100%)`\n            }}\n          />\n\n          <div\n            className=\"absolute inset-0 m-auto z-[3] pointer-events-none\"\n            style={{\n              WebkitMaskImage: `radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, ${overlayBlurColor}) 90%)`,\n              maskImage: `radial-gradient(rgba(235, 235, 235, 0) 70%, var(--overlay-blur-color, ${overlayBlurColor}) 90%)`,\n              backdropFilter: 'blur(3px)'\n            }}\n          />\n\n          <div\n            className=\"absolute left-0 right-0 top-0 h-[120px] z-[5] pointer-events-none rotate-180\"\n            style={{\n              background: `linear-gradient(to bottom, transparent, var(--overlay-blur-color, ${overlayBlurColor}))`\n            }}\n          />\n          <div\n            className=\"absolute left-0 right-0 bottom-0 h-[120px] z-[5] pointer-events-none\"\n            style={{\n              background: `linear-gradient(to bottom, transparent, var(--overlay-blur-color, ${overlayBlurColor}))`\n            }}\n          />\n\n          <div\n            ref={viewerRef}\n            className=\"absolute inset-0 z-20 pointer-events-none flex items-center justify-center\"\n            style={{ padding: 'var(--viewer-pad)' }}\n          >\n            <div\n              ref={scrimRef}\n              className=\"scrim absolute inset-0 z-10 pointer-events-none opacity-0 transition-opacity duration-500\"\n              style={{\n                background: 'rgba(0, 0, 0, 0.4)',\n                backdropFilter: 'blur(3px)'\n              }}\n            />\n            <div\n              ref={frameRef}\n              className=\"viewer-frame h-full aspect-square flex\"\n              style={{ borderRadius: `var(--enlarge-radius, ${openedImageBorderRadius})` }}\n            />\n          </div>\n        </main>\n      </div>\n    </>\n  );\n}\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"@use-gesture/react@^10.2.27"
	]
}