{"_id":"@cloudimage/360-view","_rev":"8-446052b68e310ab1f49b153aa3360236","name":"@cloudimage/360-view","dist-tags":{"latest":"4.10.0"},"versions":{"4.9.5":{"name":"@cloudimage/360-view","version":"4.9.5","keywords":["image","images","cloudimage","360","photo","product","view","viewer","360 product view"],"author":{"name":"scaleflex"},"license":"MIT","_id":"@cloudimage/360-view@4.9.5","maintainers":[{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"}],"homepage":"https://github.com/scaleflex/cloudimage-360-view","bugs":{"url":"https://github.com/scaleflex/cloudimage-360-view/issues"},"dist":{"shasum":"5c11af91e90dc76fe5d85cec6bed95d24d83e7dd","tarball":"https://registry.npmjs.org/@cloudimage/360-view/-/360-view-4.9.5.tgz","fileCount":17,"integrity":"sha512-kn268GAVc+Y7SHE8N9IX9KOXODfqD5czLIl0iTV1gQpNr1dplwXBjlvJY1ZQvB3X+cqL9ydyKqY1Ny/oAqq1fg==","signatures":[{"sig":"MEYCIQCbnA20c7Obz2+PCME9Ws2tnTIQ/Y41sQwwijrqr/At4gIhAJw6Kv5JRPqpdvEoLUvexZSCFR5e3nCa5t6HVkLeF7xC","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1423850},"main":"dist/360-view.min.js","style":"dist/react/style.css","types":"src/types/ci360.d.ts","module":"dist/360-view.min.js","exports":{".":{"types":"./src/types/ci360.d.ts","import":"./dist/360-view.min.js","require":"./dist/360-view.min.js"},"./css":"./dist/react/style.css","./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"}},"gitHead":"595e578c97170d7c0ab8887683719d0ae414023c","scripts":{"dev":"vite serve demo --host --port 5175","test":"vitest","build":"npm run build:bundle && npm run build:react","publish":"npm run build:demo && npm run deploy:demo","test:run":"vitest run","build:npm":"npm run clean:demo && vite --config vite-npm.config.js build","dev:react":"npm --prefix demo/react-demo run dev","build:demo":"vite --config vite-demo.config.js build && npm --prefix demo/react-demo run build","clean:demo":"rm -rf demo/dist","clean:dist":"rm -rf dist","build:react":"vite build --config vite-react.config.js && cp src/react/types.d.ts dist/react/index.d.ts","clean:build":"rm -rf dist","deploy:demo":"gh-pages -d dist","build:bundle":"npm run clean:build && vite build","deploy:bundle":"npm run build:bundle && node bin/deploy-bundle","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"},"repository":{"url":"git+https://github.com/scaleflex/cloudimage-360-view.git","type":"git"},"_npmVersion":"11.6.1","description":"<p align=\"center\">   <a href=\"https://www.scaleflex.com/en/home\">     <img width=\"350\" src=\"https://scaleflex.cloudimg.io/v7/plugins/cloudimage/360-view/logo_scaleflex_on_white_bg.jpg?vh=91b12d&w=700\" alt=\"Cloudimage logo\">   </a> </p>","directories":{},"_nodeVersion":"24.11.0","dependencies":{"@popperjs/core":"^2.11.2","lodash.throttle":"^4.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.10","jsdom":"^27.4.0","react":"^18.2.0","dotenv":"^16.0.3","eslint":"^9.12.0","vitest":"^4.0.17","webpack":"^5.95.0","gh-pages":"^5.0.0","prettier":"3.3.3","form-data":"^4.0.0","react-dom":"^18.2.0","@babel/cli":"^7.16.7","@eslint/js":"^9.12.0","css-loader":"^7.1.4","typescript":"^5.3.0","@babel/core":"^7.16.7","webpack-cli":"^5.0.1","@types/react":"^18.2.0","autoprefixer":"^10.4.23","babel-loader":"^8.2.3","highlight.js":"^10.4.1","style-loader":"^3.3.2","mobile-detect":"^1.4.3","vite-plugin-dts":"^4.3.0","@playwright/test":"^1.58.2","@types/react-dom":"^18.2.0","isomorphic-fetch":"^3.0.0","vite-plugin-html":"^3.2.2","@babel/preset-env":"^7.16.5","webpack-dev-server":"^5.2.3","@vitest/coverage-v8":"^4.0.17","babel-preset-minify":"^0.5.0","@vitejs/plugin-react":"^4.2.0","mini-css-extract-plugin":"^2.10.0","vite-plugin-lib-inject-css":"^2.1.1","babel-plugin-array-includes":"^2.0.3","@babel/plugin-transform-runtime":"^7.16.5"},"peerDependencies":{"react":">=17.0.0","react-dom":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/360-view_4.9.5_1774196115891_0.747813258611413","host":"s3://npm-registry-packages-npm-production"}},"4.9.6":{"name":"@cloudimage/360-view","version":"4.9.6","keywords":["image","images","cloudimage","360","photo","product","view","viewer","360 product view"],"author":{"name":"scaleflex"},"license":"MIT","_id":"@cloudimage/360-view@4.9.6","maintainers":[{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"},{"name":"philipka","email":"phil@scaleflex.com"}],"homepage":"https://github.com/scaleflex/cloudimage-360-view","bugs":{"url":"https://github.com/scaleflex/cloudimage-360-view/issues"},"dist":{"shasum":"8bbde247d9fd93410ce97555bf80f630007de49d","tarball":"https://registry.npmjs.org/@cloudimage/360-view/-/360-view-4.9.6.tgz","fileCount":17,"integrity":"sha512-/+LmQrTtHxrZUOMbxECIgZpvk+vNzznAn9iousjslxYRfkTAinvmdEaYVBxjDm6Li0Rweo7EkqQjqWfKM6euOA==","signatures":[{"sig":"MEUCIQD2XNvaGNsuRGM0MtP7YZtzN4eoJNFThLmYB5I51dOhJQIgFpGz6F6/77enhWCWC8ur0UySlO7EQPX875CCAB3ahIg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1423788},"main":"dist/360-view.min.js","style":"dist/react/style.css","types":"src/types/ci360.d.ts","module":"dist/360-view.min.js","exports":{".":{"types":"./src/types/ci360.d.ts","import":"./dist/360-view.min.js","require":"./dist/360-view.min.js"},"./css":"./dist/react/style.css","./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"}},"gitHead":"0d7c041e4d0f15a031c84078841fcb77b9f5d5c0","scripts":{"dev":"vite serve demo --host --port 5175","test":"vitest","build":"npm run build:bundle && npm run build:react","publish":"npm run build:demo && npm run deploy:demo","release":"node scripts/release-cdn.mjs plugin","test:run":"vitest run","build:npm":"npm run clean:demo && vite --config vite-npm.config.js build","dev:react":"npm --prefix demo/react-demo run dev","build:demo":"vite --config vite-demo.config.js build && npm --prefix demo/react-demo run build","clean:demo":"rm -rf demo/dist","clean:dist":"rm -rf dist","build:react":"vite build --config vite-react.config.js && cp src/react/types.d.ts dist/react/index.d.ts","clean:build":"rm -rf dist","deploy:demo":"gh-pages -d dist","build:bundle":"npm run clean:build && vite build","deploy:bundle":"npm run build:bundle && node bin/deploy-bundle","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"},"repository":{"url":"git+https://github.com/scaleflex/cloudimage-360-view.git","type":"git"},"_npmVersion":"11.11.0","description":"<p align=\"center\">   <a href=\"https://www.scaleflex.com/en/home\">     <img width=\"350\" src=\"https://scaleflex.cloudimg.io/v7/plugins/scaleflex/logo.png?vh=b0a502&radius=25&w=700\" alt=\"Cloudimage logo\">   </a> </p>","directories":{},"_nodeVersion":"24.14.1","dependencies":{"@popperjs/core":"^2.11.2","lodash.throttle":"^4.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.10","jsdom":"^27.4.0","react":"^18.2.0","dotenv":"^16.0.3","eslint":"^9.12.0","vitest":"^4.0.17","webpack":"^5.95.0","gh-pages":"^5.0.0","prettier":"3.3.3","form-data":"^4.0.0","react-dom":"^18.2.0","@babel/cli":"^7.16.7","@eslint/js":"^9.12.0","css-loader":"^7.1.4","typescript":"^5.3.0","@babel/core":"^7.16.7","webpack-cli":"^5.0.1","@types/react":"^18.2.0","autoprefixer":"^10.4.23","babel-loader":"^8.2.3","highlight.js":"^10.4.1","style-loader":"^3.3.2","mobile-detect":"^1.4.3","vite-plugin-dts":"^4.3.0","@playwright/test":"^1.58.2","@types/react-dom":"^18.2.0","isomorphic-fetch":"^3.0.0","vite-plugin-html":"^3.2.2","@babel/preset-env":"^7.16.5","webpack-dev-server":"^5.2.3","@vitest/coverage-v8":"^4.0.17","babel-preset-minify":"^0.5.0","@vitejs/plugin-react":"^4.2.0","mini-css-extract-plugin":"^2.10.0","vite-plugin-lib-inject-css":"^2.1.1","babel-plugin-array-includes":"^2.0.3","@babel/plugin-transform-runtime":"^7.16.5"},"peerDependencies":{"react":">=17.0.0","react-dom":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/360-view_4.9.6_1775042170341_0.08550225739181871","host":"s3://npm-registry-packages-npm-production"}},"4.10.0":{"name":"@cloudimage/360-view","version":"4.10.0","keywords":["image","images","cloudimage","360","photo","product","view","viewer","360 product view"],"author":{"name":"scaleflex"},"license":"MIT","_id":"@cloudimage/360-view@4.10.0","maintainers":[{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"},{"name":"philipka","email":"phil@scaleflex.com"}],"homepage":"https://github.com/scaleflex/cloudimage-360-view","bugs":{"url":"https://github.com/scaleflex/cloudimage-360-view/issues"},"dist":{"shasum":"66ca1f3988b854042d9d021d5a74d206e1f4de86","tarball":"https://registry.npmjs.org/@cloudimage/360-view/-/360-view-4.10.0.tgz","fileCount":17,"integrity":"sha512-GRJBSrvP8hkCsgwskfMz5bDRoDA5O4McLB0b60+o37dB3smPuGJ3JXQXPMbEvBAw2+Ni8AbCF/ckno4fS3FKJg==","signatures":[{"sig":"MEYCIQCqiShWWLjc3311NzYqrNVlJ176DxoyFS5dfGh+FbYgZwIhALafO6z0S+1q34XtMEnKPxEA9w9gHNjWwVyKrNT6Osm9","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":1428451},"main":"dist/360-view.min.js","style":"dist/react/style.css","types":"src/types/ci360.d.ts","module":"dist/360-view.min.js","exports":{".":{"types":"./src/types/ci360.d.ts","import":"./dist/360-view.min.js","require":"./dist/360-view.min.js"},"./css":"./dist/react/style.css","./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"}},"gitHead":"7813131973602107704dad6502871b3d3a762a69","scripts":{"dev":"vite serve demo --host --port 5175","test":"vitest","build":"npm run build:bundle && npm run build:react","publish":"npm run build:demo && npm run deploy:demo","release":"node scripts/release-cdn.mjs plugin","test:run":"vitest run","build:npm":"npm run clean:demo && vite --config vite-npm.config.js build","dev:react":"npm --prefix demo/react-demo run dev","build:demo":"vite --config vite-demo.config.js build && npm --prefix demo/react-demo run build","clean:demo":"rm -rf demo/dist","clean:dist":"rm -rf dist","build:react":"vite build --config vite-react.config.js && cp src/react/types.d.ts dist/react/index.d.ts","clean:build":"rm -rf dist","deploy:demo":"gh-pages -d dist","build:bundle":"npm run clean:build && vite build","deploy:bundle":"npm run build:bundle && node bin/deploy-bundle","test:coverage":"vitest run --coverage"},"_npmUser":{"name":"dmitry.stremous","email":"dmitry@scaleflex.com"},"repository":{"url":"git+https://github.com/scaleflex/cloudimage-360-view.git","type":"git"},"_npmVersion":"11.11.0","description":"<p align=\"center\">   <a href=\"https://www.scaleflex.com/en/home\">     <img width=\"350\" src=\"https://scaleflex.cloudimg.io/v7/plugins/scaleflex/logo.png?vh=b0a502&radius=25&w=700\" alt=\"Cloudimage logo\">   </a> </p>","directories":{},"_nodeVersion":"24.14.1","dependencies":{"@popperjs/core":"^2.11.2","lodash.throttle":"^4.1.1"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^5.4.10","jsdom":"^27.4.0","react":"^18.2.0","dotenv":"^16.0.3","eslint":"^9.12.0","vitest":"^4.0.17","webpack":"^5.95.0","gh-pages":"^5.0.0","prettier":"3.3.3","form-data":"^4.0.0","react-dom":"^18.2.0","@babel/cli":"^7.16.7","@eslint/js":"^9.12.0","css-loader":"^7.1.4","typescript":"^5.3.0","@babel/core":"^7.16.7","webpack-cli":"^5.0.1","@types/react":"^18.2.0","autoprefixer":"^10.4.23","babel-loader":"^8.2.3","highlight.js":"^10.4.1","style-loader":"^3.3.2","mobile-detect":"^1.4.3","vite-plugin-dts":"^4.3.0","@playwright/test":"^1.58.2","@types/react-dom":"^18.2.0","isomorphic-fetch":"^3.0.0","vite-plugin-html":"^3.2.2","@babel/preset-env":"^7.16.5","webpack-dev-server":"^5.2.3","@vitest/coverage-v8":"^4.0.17","babel-preset-minify":"^0.5.0","@vitejs/plugin-react":"^4.2.0","mini-css-extract-plugin":"^2.10.0","vite-plugin-lib-inject-css":"^2.1.1","babel-plugin-array-includes":"^2.0.3","@babel/plugin-transform-runtime":"^7.16.5"},"peerDependencies":{"react":">=17.0.0","react-dom":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/360-view_4.10.0_1775114687656_0.3387483648137801","host":"s3://npm-registry-packages-npm-production"}}},"time":{"created":"2026-03-22T16:15:15.829Z","modified":"2026-08-26T14:01:51.479Z","4.9.5":"2026-03-22T16:15:16.121Z","4.9.6":"2026-04-01T11:16:10.533Z","4.10.0":"2026-04-02T07:24:47.842Z"},"bugs":{"url":"https://github.com/scaleflex/cloudimage-360-view/issues"},"author":{"name":"scaleflex"},"license":"MIT","homepage":"https://github.com/scaleflex/cloudimage-360-view","keywords":["image","images","cloudimage","360","photo","product","view","viewer","360 product view"],"repository":{"url":"git+https://github.com/scaleflex/cloudimage-360-view.git","type":"git"},"description":"<p align=\"center\">   <a href=\"https://www.scaleflex.com/en/home\">     <img width=\"350\" src=\"https://scaleflex.cloudimg.io/v7/plugins/scaleflex/logo.png?vh=b0a502&radius=25&w=700\" alt=\"Cloudimage logo\">   </a> </p>","maintainers":[{"email":"dmytromusiiko@gmail.com","name":"dmytrimusiiko"},{"email":"dmitry@scaleflex.com","name":"dmitry.stremous"},{"email":"phil@scaleflex.com","name":"philipka"},{"email":"jesus.vista@scaleflex.com","name":"jesus.vista08"},{"email":"nikjordanov@gmail.com","name":"flamboyantn"},{"email":"ltinser@gmail.com","name":"ltins"},{"email":"vitaly.shaposhnik@gmail.com","name":"vitaly.shaposhnik"}],"readme":"<p align=\"center\">\n  <a href=\"https://www.scaleflex.com/en/home\">\n    <img width=\"350\" src=\"https://scaleflex.cloudimg.io/v7/plugins/scaleflex/logo.png?vh=b0a502&radius=25&w=700\" alt=\"Cloudimage logo\">\n  </a>\n</p>\n\n<h1 align=\"center\">Cloudimage 360 View</h1>\n\n<p align=\"center\">\n  <strong>A powerful JavaScript library for creating interactive 360-degree product views</strong>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://github.com/scaleflex/cloudimage-360-view/releases\">\n    <img src=\"https://img.shields.io/github/v/release/scaleflex/cloudimage-360-view\" alt=\"Release\">\n  </a>\n  <a href=\"https://img.shields.io/bundlephobia/min/@cloudimage/360-view\">\n    <img src=\"https://img.shields.io/bundlephobia/min/@cloudimage/360-view\" alt=\"Size\">\n  </a>\n  <a href=\"https://img.shields.io/npm/dt/@cloudimage/360-view?logoColor=orange\">\n    <img src=\"https://img.shields.io/npm/dt/@cloudimage/360-view?logoColor=orange\" alt=\"Downloads\">\n  </a>\n  <a href=\"https://opensource.org/licenses/MIT\">\n    <img src=\"https://img.shields.io/badge/license-MIT-blue.svg\" alt=\"License\">\n  </a>\n  <a href=\"https://www.cloudimage.io/en/home\">\n    <img src=\"https://img.shields.io/badge/Powered%20by-Cloudimage-blue\" alt=\"Cloudimage\">\n  </a>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://scaleflex.github.io/cloudimage-360-view/\">View Demo</a> ·\n  <a href=\"https://codesandbox.io/p/sandbox/github/scaleflex/cloudimage-360-view/tree/master/codesandbox/react\">React CodeSandbox</a> ·\n  <a href=\"https://codesandbox.io/p/sandbox/github/scaleflex/cloudimage-360-view/tree/master/codesandbox/vanilla\">Vanilla CodeSandbox</a> ·\n  <a href=\"https://github.com/scaleflex/cloudimage-360-view/issues\">Report Bug</a>\n</p>\n\n---\n\n## Table of Contents\n\n- [Overview](#overview)\n- [Features](#features)\n- [Quick Start](#quick-start)\n- [Installation](#installation)\n- [Usage](#usage)\n  - [2D Grid Mode](#2d-grid-mode)\n- [React / Next.js](#react--nextjs)\n- [Configuration Options](#configuration-options)\n- [Event Callbacks](#event-callbacks)\n- [Hotspots](#hotspots)\n- [Interaction Hints](#interaction-hints)\n- [Styling & Theming](#styling--theming)\n- [Methods](#methods)\n- [Cloudimage Integration](#cloudimage-integration)\n- [Browser Support](#browser-support)\n- [Migration Guide (v3 → v4)](#migration-guide-v3--v4)\n- [Contributing](#contributing)\n- [Support](#support)\n- [License](#license)\n\n---\n\n## Overview\n\nCloudimage 360 View enables you to create stunning, interactive 360-degree product views for your website. Perfect for e-commerce platforms, virtual tours, and product showcases, it provides an immersive viewing experience that lets users explore products from every angle.\n\n### Why Choose This Library?\n\n- **Easy Integration** - Get started in minutes with CDN or npm\n- **Fully Customizable** - CSS variables, callbacks, and extensive configuration options\n- **Mobile-Friendly** - Touch and swipe support out of the box\n- **Performance Optimized** - Lazy loading, responsive images, and efficient rendering\n- **Feature Rich** - Hotspots, zoom, fullscreen, autoplay, and more\n\n---\n\n## Features\n\n| Feature | Description |\n|---------|-------------|\n| **360° Rotation** | Smooth horizontal and vertical rotation with customizable speed |\n| **2D Grid Mode** | True two-axis viewing for images organized as a horizontal/vertical matrix |\n| **Touch & Drag** | Intuitive mouse and touch controls with inertia/momentum |\n| **Pinch-to-Zoom** | Natural pinch gesture zooming on mobile devices |\n| **Autoplay** | Automatic rotation with configurable behavior and direction |\n| **Zoom** | Double-click, Ctrl+scroll, or pinch-to-zoom with pan controls |\n| **Fullscreen** | Immersive fullscreen mode with ESC key support |\n| **Hotspots** | Interactive markers with tooltips for highlighting features |\n| **Keyboard Navigation** | Arrow key support for accessibility |\n| **Lazy Loading** | Optimized loading for better performance |\n| **Responsive** | Works on all screen sizes with Cloudimage CDN integration |\n| **Theming** | CSS variables for easy customization |\n| **Event Callbacks** | Hook into viewer lifecycle and user interactions |\n\n---\n\n## Quick Start\n\nAdd the library via CDN and create your first 360 viewer in seconds:\n\n```html\n<!-- Add the library (CSS is auto-injected) -->\n<script src=\"https://cdn.cloudimage.io/360-view/4.10.0/360-view.min.js\"></script>\n\n<!-- Create a container with data attributes -->\n<div\n  class=\"cloudimage-360\"\n  data-folder=\"https://scaleflex.cloudimg.io/v7/demo/360-car/\"\n  data-filename-x=\"car-{index}.jpg\"\n  data-amount-x=\"36\"\n></div>\n\n<!-- Initialize -->\n<script>\n  const viewer = new window.CI360();\n  viewer.initAll();\n</script>\n```\n\n---\n\n## Installation\n\n### Option 1: CDN (Recommended for Quick Setup)\n\n```html\n<script src=\"https://cdn.cloudimage.io/360-view/4.10.0/360-view.min.js\"></script>\n```\n\n> **Note:** CSS is automatically injected by the script - no separate stylesheet needed.\n\n### Option 2: Package Manager\n\n```bash\n# npm\nnpm install @cloudimage/360-view\n\n# yarn\nyarn add @cloudimage/360-view\n\n# pnpm\npnpm add @cloudimage/360-view\n```\n\nThen import in your JavaScript:\n\n```javascript\nimport CI360 from '@cloudimage/360-view';\nimport '@cloudimage/360-view/css';\n```\n\n---\n\n## Usage\n\n### Method 1: Data Attributes (Declarative)\n\nThe simplest way to create a 360 viewer using HTML data attributes:\n\n```html\n<div\n  id=\"my-360-viewer\"\n  class=\"cloudimage-360\"\n  data-folder=\"https://your-domain.com/images/\"\n  data-filename-x=\"{index}.jpg\"\n  data-amount-x=\"36\"\n  data-autoplay\n  data-fullscreen\n  data-zoom-max=\"3\"\n></div>\n\n<script>\n  const viewer = new CI360();\n  viewer.initAll(); // Initializes all elements with class \"cloudimage-360\"\n</script>\n```\n\n### Method 2: JavaScript Configuration (Programmatic)\n\nFor more control, initialize with a JavaScript configuration object:\n\n```javascript\nconst viewer = new CI360();\n\nconst container = document.getElementById('product-viewer');\n\nconst config = {\n  folder: 'https://your-domain.com/images/',\n  filenameX: 'product-{index}.jpg',\n  amountX: 36,\n  autoplay: true,\n  speed: 100,\n  dragSpeed: 150,\n  fullscreen: true,\n  zoomMax: 3,\n  inertia: true,\n\n  // Event callbacks\n  onReady: () => console.log('Viewer ready!'),\n  onSpin: (e) => console.log(`Frame: ${e.activeImageX + 1}/${e.amountX}`),\n};\n\nviewer.init(container, config);\n```\n\n### X and Y Axis Rotation\n\nSupport 360° rotation on both axes for full product exploration:\n\n```javascript\nconst config = {\n  folder: 'https://your-domain.com/images/',\n  filenameX: 'product-x-{index}.jpg',\n  filenameY: 'product-y-{index}.jpg',\n  amountX: 36,\n  amountY: 18,\n  autoplayBehavior: 'spin-xy', // Options: 'spin-x', 'spin-y', 'spin-xy', 'spin-yx'\n};\n```\n\n### 2D Grid Mode\n\nFor products photographed at multiple horizontal **and** vertical angles, grid mode provides true two-axis viewing. Instead of independent X/Y strips, the displayed image is determined by both axes simultaneously — dragging horizontally changes X, vertically changes Y, and diagonal drags update both.\n\nGrid mode is auto-detected when `filenameGrid` or `imageListGrid` is provided.\n\n**Using a filename pattern:**\n\n```javascript\nCI360.init(document.getElementById('viewer'), {\n  folder: 'https://your-domain.com/images/',\n  filenameGrid: 'product_{indexY}_{indexX}.jpg',\n  amountX: 24,        // horizontal angles\n  amountY: 4,         // vertical angles\n  indexZeroBase: 3,    // zero-pad to 3 digits: 001, 002, ...\n  stopAtEdgesY: true,  // prevent Y from looping\n});\n```\n\nPlaceholders `{indexX}` and `{indexY}` are replaced with 1-based indices (respecting `indexZeroBase` for zero-padding). Images are stored internally as a flat array with the formula `imagesGrid[y * amountX + x]`.\n\n**Using an explicit image list:**\n\n```javascript\nCI360.init(document.getElementById('viewer'), {\n  imageListGrid: [\n    // y=0 row (all X frames at first vertical angle)\n    'img_001_001.jpg', 'img_001_002.jpg', /* ... */\n    // y=1 row\n    'img_002_001.jpg', 'img_002_002.jpg', /* ... */\n  ],\n  amountX: 24,\n  amountY: 4,\n});\n```\n\n**Autoplay** works with all four behaviors: `spin-x` (horizontal only), `spin-y` (vertical only), `spin-xy` (row-scan: X advances each tick, Y advances when X wraps), and `spin-yx` (column-scan).\n\n**Per-axis edge stopping** — use `stopAtEdgesX` and `stopAtEdgesY` to control looping independently. This is useful when one axis has full 360° coverage but the other has limited angles:\n\n```javascript\n{\n  stopAtEdgesY: true,  // Y has only 4 angles, don't loop\n  // X loops freely (default)\n}\n```\n\n**React:**\n\n```tsx\n<CI360Viewer\n  folder=\"https://your-domain.com/images/\"\n  filenameGrid=\"product_{indexY}_{indexX}.jpg\"\n  amountX={24}\n  amountY={4}\n  indexZeroBase={3}\n  stopAtEdgesY\n/>\n```\n\n---\n\n## React / Next.js\n\nThe library provides a React wrapper for seamless integration with React and Next.js applications.\n\n### Installation\n\n```bash\nnpm install @cloudimage/360-view\n```\n\n### Basic Usage\n\n```tsx\nimport { CI360Viewer } from '@cloudimage/360-view/react';\nimport '@cloudimage/360-view/css';\n\nfunction ProductView() {\n  return (\n    <CI360Viewer\n      folder=\"https://example.com/images/\"\n      filenameX=\"product-{index}.jpg\"\n      amountX={36}\n      autoplay\n      fullscreen\n      aspectRatio=\"16/9\"\n      style={{ width: '100%', maxWidth: 800 }}\n    />\n  );\n}\n```\n\n### Imperative Control with Ref\n\nUse a ref to control the viewer programmatically:\n\n```tsx\nimport { useRef } from 'react';\nimport { CI360Viewer, CI360ViewerRef } from '@cloudimage/360-view/react';\nimport '@cloudimage/360-view/css';\n\nfunction ProductView() {\n  const viewerRef = useRef<CI360ViewerRef>(null);\n\n  return (\n    <>\n      <CI360Viewer\n        ref={viewerRef}\n        folder=\"https://example.com/images/\"\n        filenameX=\"{index}.jpg\"\n        amountX={36}\n        onSpin={(e) => console.log(`Frame: ${e.activeImageX}`)}\n      />\n      <button onClick={() => viewerRef.current?.play()}>Play</button>\n      <button onClick={() => viewerRef.current?.stop()}>Stop</button>\n      <button onClick={() => viewerRef.current?.goToFrame(17)}>Go to Frame 17</button>\n    </>\n  );\n}\n```\n\n### Available Ref Methods\n\n| Method | Description |\n|--------|-------------|\n| `play()` | Start autoplay |\n| `stop()` | Stop autoplay |\n| `moveLeft(steps?)` | Move left by specified frames (default: 1) |\n| `moveRight(steps?)` | Move right by specified frames (default: 1) |\n| `moveTop(steps?)` | Move up on Y-axis (default: 1) |\n| `moveBottom(steps?)` | Move down on Y-axis (default: 1) |\n| `zoomIn()` | Toggle zoom in |\n| `zoomOut()` | Zoom out |\n| `goToFrame(frame, hotspotId?)` | Animate to specific frame |\n| `getViewer()` | Get underlying viewer instance |\n\n### With Hotspots\n\n```tsx\nimport { CI360Viewer, Hotspot } from '@cloudimage/360-view/react';\n\nconst hotspots: Hotspot[] = [\n  {\n    id: 'feature-1',\n    label: 'Engine',\n    orientation: 'x',\n    containerSize: [1200, 800],\n    positions: { 0: { x: 500, y: 300 } },\n    content: '<div>Engine details</div>',\n  },\n];\n\nfunction ProductView() {\n  return (\n    <CI360Viewer\n      folder=\"https://example.com/images/\"\n      filenameX=\"{index}.jpg\"\n      amountX={36}\n      hotspots={hotspots}\n    />\n  );\n}\n```\n\n### Next.js (SSR)\n\nFor Next.js applications, use dynamic import to disable server-side rendering:\n\n```tsx\nimport dynamic from 'next/dynamic';\nimport '@cloudimage/360-view/css';\n\nconst CI360Viewer = dynamic(\n  () => import('@cloudimage/360-view/react').then(mod => mod.CI360Viewer),\n  { ssr: false }\n);\n\nexport default function ProductPage() {\n  return (\n    <CI360Viewer\n      folder=\"https://example.com/images/\"\n      filenameX=\"{index}.jpg\"\n      amountX={36}\n    />\n  );\n}\n```\n\n### useCI360 Hook\n\nFor advanced use cases, you can use the `useCI360` hook directly:\n\n```tsx\nimport { useRef } from 'react';\nimport { useCI360 } from '@cloudimage/360-view/react';\n\nfunction CustomViewer() {\n  const containerRef = useRef<HTMLDivElement>(null);\n  const { viewer, isReady } = useCI360(containerRef, {\n    folder: 'https://example.com/images/',\n    filenameX: '{index}.jpg',\n    amountX: 36,\n    onReady: () => console.log('Viewer ready!'),\n  });\n\n  return (\n    <div>\n      <div ref={containerRef} style={{ width: '100%', maxWidth: 800, aspectRatio: '16/9' }} />\n      {isReady && <p>Viewer is ready!</p>}\n    </div>\n  );\n}\n```\n\n### TypeScript Support\n\nThe React wrapper is fully typed. Import types as needed:\n\n```tsx\nimport type {\n  CI360ViewerProps,\n  CI360ViewerRef,\n  CI360Config,\n  SpinEventData,\n  Hotspot,\n} from '@cloudimage/360-view/react';\n```\n\n---\n\n## Configuration Options\n\nAll options can be set via JavaScript config or HTML data attributes.\n\n### Image Source Options\n\n| Option | Data Attribute | Default | Description |\n|--------|----------------|---------|-------------|\n| `folder` | `data-folder` | `'/'` | Path to the folder containing images |\n| `filenameX` | `data-filename-x` | `'image-{index}.jpg'` | Filename pattern for X-axis images. Use `{index}` as placeholder |\n| `filenameY` | `data-filename-y` | `null` | Filename pattern for Y-axis images |\n| `imageListX` | `data-image-list-x` | `null` | Array of image URLs for X-axis (alternative to folder/filename) |\n| `imageListY` | `data-image-list-y` | `null` | Array of image URLs for Y-axis |\n| `filenameGrid` | `data-filename-grid` | `null` | Filename pattern for 2D grid mode. Uses `{indexX}` and `{indexY}` placeholders |\n| `imageListGrid` | `data-image-list-grid` | `null` | Array of image URLs for 2D grid (flat or 2D array, Y-outer X-inner order) |\n| `amountX` | `data-amount-x` | `0` | Total number of X-axis images |\n| `amountY` | `data-amount-y` | `0` | Total number of Y-axis images |\n| `indexZeroBase` | `data-index-zero-base` | `0` | Starting index for image filenames |\n\n### Behavior Options\n\n| Option | Data Attribute | Default | Description |\n|--------|----------------|---------|-------------|\n| `autoplay` | `data-autoplay` | `false` | Enable automatic rotation |\n| `autoplayBehavior` | `data-autoplay-behavior` | `'spin-x'` | Autoplay pattern: `'spin-x'`, `'spin-y'`, `'spin-xy'`, `'spin-yx'` |\n| `autoplayReverse` | `data-autoplay-reverse` | `false` | Reverse autoplay direction |\n| `playOnce` | `data-play-once` | `false` | Stop after one complete rotation |\n| `speed` | `data-speed` | `80` | Autoplay speed (ms between frames) |\n| `inertia` | `data-inertia` | `false` | Enable momentum after drag release |\n\n### Control Options\n\n| Option | Data Attribute | Default | Description |\n|--------|----------------|---------|-------------|\n| `draggable` | `data-draggable` | `true` | Enable mouse drag rotation |\n| `swipeable` | `data-swipeable` | `true` | Enable touch swipe rotation |\n| `dragSpeed` | `data-drag-speed` | `150` | Drag sensitivity |\n| `dragReverse` | `data-drag-reverse` | `false` | Reverse drag direction |\n| `keys` | `data-keys` | `false` | Enable keyboard arrow navigation |\n| `keysReverse` | `data-keys-reverse` | `false` | Reverse keyboard direction |\n| `stopAtEdges` | `data-stop-at-edges` | `false` | Stop rotation at first/last frame (both axes) |\n| `stopAtEdgesX` | `data-stop-at-edges-x` | `null` | Stop X-axis at edges. Overrides `stopAtEdges` for X when set |\n| `stopAtEdgesY` | `data-stop-at-edges-y` | `null` | Stop Y-axis at edges. Overrides `stopAtEdges` for Y when set |\n| `pinchZoom` | `data-pinch-zoom` | `true` | Enable pinch-to-zoom on touch devices |\n\n### Display Options\n\n| Option | Data Attribute | Default | Description |\n|--------|----------------|---------|-------------|\n| `aspectRatio` | `data-aspect-ratio` | `null` | Aspect ratio for the container (e.g., `\"16/9\"`, `\"4/3\"`, `\"1/1\"`) |\n| `fullscreen` | `data-fullscreen` | `false` | Show fullscreen button |\n| `zoomMax` | `data-zoom-max` | `5` | Maximum zoom level (1-10) |\n| `zoomStep` | `data-zoom-step` | `0.5` | Zoom increment per step |\n| `zoomControls` | `data-zoom-controls` | `true` | Show zoom control buttons (zoom in, zoom out, reset) |\n| `zoomControlsPosition` | `data-zoom-controls-position` | `'bottom-right'` | Position of zoom controls toolbar. Values: `'top-left'`, `'top-center'`, `'top-right'`, `'bottom-left'`, `'bottom-center'`, `'bottom-right'` |\n| `scrollHint` | `data-scroll-hint` | `true` | Show \"Ctrl + scroll to zoom\" hint on scroll |\n| `bottomCircle` | `data-bottom-circle` | `false` | Show 360° progress indicator |\n| `bottomCircleOffset` | `data-bottom-circle-offset` | `5` | Progress indicator offset (px) |\n| `initialIconShown` | `data-initial-icon` | `true` | Show 360° icon on load |\n| `initOnClick` | `data-init-on-click` | `false` | If true, images load only after the user clicks on the viewer. Shows a first-frame preview with a click overlay. Useful for grids with multiple 360 views |\n| `lazyload` | `data-lazyload` | `true` | Enable lazy loading |\n| `hints` | `data-hints` | `true` | Show interaction hints on load |\n| `theme` | `data-theme` | `null` | Color theme: `'light'` or `'dark'` |\n| `hotspotTrigger` | `data-hotspot-trigger` | `'hover'` | Hotspot trigger mode: `'hover'` or `'click'` |\n| `hotspotTimelineOnClick` | `data-hotspot-timeline-on-click` | `true` | Show hotspot popup when clicking timeline dot |\n| `markerTheme` | `data-marker-theme` | `null` | Hotspot marker theme: `'default'`, `'inverted'`, or `'brand'` |\n| `brandColor` | `data-brand-color` | `null` | Brand accent color for `'brand'` marker theme (e.g. `'#ff6600'`) |\n\n### Cloudimage CDN Options\n\n| Option | Data Attribute | Default | Description |\n|--------|----------------|---------|-------------|\n| `ciToken` | `data-responsive` | `null` | Cloudimage token for responsive images |\n| `ciFilters` | `data-filters` | `null` | Cloudimage filters |\n| `ciTransformation` | `data-transformation` | `null` | Cloudimage transformations |\n\n---\n\n## Event Callbacks\n\nHook into viewer events for custom functionality. Callbacks are only available via JavaScript configuration.\n\n| Callback | Event Data | Description |\n|----------|------------|-------------|\n| `onReady` | `{ viewerId }` | Viewer initialized and ready |\n| `onLoad` | `{ viewerId, imagesX, imagesY }` | All images loaded |\n| `onSpin` | `{ viewerId, direction, activeImageX, activeImageY, amountX, amountY }` | Each rotation frame |\n| `onAutoplayStart` | `{ viewerId }` | Autoplay started |\n| `onAutoplayStop` | `{ viewerId }` | Autoplay stopped |\n| `onDragStart` | `{ viewerId }` | User started dragging |\n| `onDragEnd` | `{ viewerId }` | User stopped dragging |\n| `onZoomIn` | `{ viewerId, zoomLevel }` | Pointer zoom activated |\n| `onZoomOut` | `{ viewerId }` | Pointer zoom deactivated |\n| `onFullscreenOpen` | `{ viewerId }` | Fullscreen mode opened |\n| `onFullscreenClose` | `{ viewerId }` | Fullscreen mode closed |\n\n### Example\n\n```javascript\nconst config = {\n  folder: 'https://example.com/images/',\n  filenameX: '{index}.jpg',\n  amountX: 36,\n\n  onReady: (e) => {\n    console.log(`Viewer ${e.viewerId} is ready`);\n  },\n\n  onSpin: (e) => {\n    // Update custom progress indicator\n    const progress = ((e.activeImageX + 1) / e.amountX * 100).toFixed(0);\n    document.getElementById('progress').textContent = `${progress}%`;\n  },\n\n  onFullscreenOpen: () => {\n    // Pause background video when entering fullscreen\n    document.getElementById('bg-video')?.pause();\n  },\n};\n```\n\n---\n\n## Hotspots\n\nAdd interactive markers to highlight product features.\n\n### Configuration\n\n```javascript\nconst hotspots = [\n  {\n    id: 'feature-1',\n    orientation: 'x',\n    containerSize: [1200, 800], // Reference container size for positioning\n    positions: {\n      0: { x: 500, y: 300 },\n      1: { x: 520, y: 300 },\n      2: { x: 540, y: null }, // null inherits from previous frame\n      3: { x: 560, y: null },\n      // ... positions for frames where hotspot is visible\n    },\n    content: '<div class=\"tooltip\"><strong>Premium Feature</strong><p>Description here</p></div>',\n    onClick: () => {\n      console.log('Hotspot clicked!');\n    },\n  },\n];\n\nconst config = {\n  folder: 'https://example.com/images/',\n  filenameX: '{index}.jpg',\n  amountX: 36,\n  hotspots: hotspots,\n};\n```\n\n### Hotspot Properties\n\n| Property | Required | Description |\n|----------|----------|-------------|\n| `id` | Yes | Unique identifier |\n| `orientation` | Yes | `'x'` or `'y'` axis |\n| `containerSize` | No | `[width, height]` reference dimensions for pixel-based positions. When omitted, positions are treated as percentages (0-100) of the image draw area |\n| `positions` | Yes | Object mapping frame index to `{ x, y }` coordinates |\n| `content` | No | HTML content for the tooltip |\n| `label` | No | Short label for the hotspot (used in timeline tooltips) |\n| `onClick` | No | Click handler function |\n| `keepOpen` | No | If `true`, the popover stays open until explicitly closed |\n| `className` | No | Custom CSS class(es) to add to the hotspot element |\n| `markerTheme` | No | Per-hotspot theme override: `'default'`, `'inverted'`, or `'brand'` |\n| `navigateTo` | No | Scene ID to navigate to on click (turns hotspot into a navigation pin) |\n| `arrowDirection` | No | Rotation angle (degrees) for the navigation arrow icon. Only applies when `navigateTo` is set |\n\n### Hotspot Timeline\n\nWhen hotspots are configured, a timeline navigation bar automatically appears below the viewer. This timeline shows:\n\n- **Position indicator** - Shows current rotation position\n- **Hotspot dots** - One dot per hotspot at its center frame position\n- **Hover tooltips** - If a hotspot has a `label`, hovering over its dot shows a tooltip\n\nClicking a dot animates the viewer to that hotspot's position and optionally shows its popup.\n\n#### Timeline Tooltips\n\nTooltips display the hotspot's `label` property when hovering over a timeline dot:\n\n```javascript\nconst hotspots = [\n  {\n    id: 'engine',\n    label: 'Engine Bay',  // This text appears in the tooltip\n    orientation: 'x',\n    containerSize: [1200, 800],\n    positions: { 0: { x: 500, y: 300 }, /* ... */ },\n    content: '<div>Full hotspot content here</div>',\n  },\n];\n```\n\n**Tooltip behavior:**\n- Appears after a **400ms hover delay** to prevent accidental triggers\n- Positioned above the dot with an arrow pointer\n- Hidden on mouse leave or click (navigation)\n\n#### Timeline Configuration\n\n| Option | Default | Description |\n|--------|---------|-------------|\n| `hotspotTimelineOnClick` | `true` | Show hotspot popup when clicking a timeline dot |\n\n```javascript\nconst config = {\n  hotspots: [...],\n  hotspotTimelineOnClick: true,  // Show popup on click (default)\n  // or\n  hotspotTimelineOnClick: false, // Only navigate, don't show popup\n};\n```\n\n#### Timeline CSS Variables\n\nCustomize the timeline appearance with CSS variables:\n\n```css\n:root {\n  /* Timeline track */\n  --ci360-timeline-height: 6px;\n  --ci360-timeline-track-bg: rgba(0, 0, 0, 0.12);\n\n  /* Hotspot dots */\n  --ci360-timeline-dot-size: 18px;\n  --ci360-timeline-dot-color: var(--ci360-hotspot-color);\n  --ci360-timeline-dot-border: 2px solid #fff;\n\n  /* Position indicator */\n  --ci360-timeline-indicator-size: 12px;\n  --ci360-timeline-indicator-color: #333333;\n\n  /* Tooltip styling (matches theme) */\n  --ci360-timeline-tooltip-bg: rgba(255, 255, 255, 0.95);\n  --ci360-timeline-tooltip-color: #333333;\n}\n\n/* Dark theme uses dark tooltip */\n.ci360-theme-dark {\n  --ci360-timeline-tooltip-bg: rgba(40, 40, 45, 0.95);\n  --ci360-timeline-tooltip-color: #e0e0e0;\n}\n```\n\n**Custom tooltip styling example:**\n\n```css\n/* Increase tooltip font size */\n.cloudimage-360-hotspot-timeline-tooltip {\n  font-size: 14px;\n  padding: 8px 16px;\n}\n\n/* Brand-colored tooltip */\n.my-viewer {\n  --ci360-timeline-tooltip-bg: #2563eb;\n  --ci360-timeline-tooltip-color: #ffffff;\n}\n```\n\n### Marker Themes\n\nControl the visual appearance of hotspot markers at the viewer level or per-hotspot.\n\n**Viewer-level theme** applies to all hotspots:\n\n```javascript\nconst config = {\n  hotspots: [...],\n  markerTheme: 'inverted', // 'default', 'inverted', or 'brand'\n  brandColor: '#ff6600',   // Used when markerTheme is 'brand'\n};\n```\n\n**Per-hotspot theme** overrides the viewer-level theme for individual markers:\n\n```javascript\nconst hotspots = [\n  {\n    id: 'highlight',\n    markerTheme: 'brand', // Override for this hotspot only\n    positions: { 0: { x: 500, y: 300 } },\n    content: '<div>Highlighted feature</div>',\n    // ...\n  },\n  {\n    id: 'subtle',\n    markerTheme: 'inverted', // Different override\n    positions: { 5: { x: 200, y: 150 } },\n    content: '<div>Subtle marker</div>',\n    // ...\n  },\n];\n```\n\n| Theme | Description |\n|-------|-------------|\n| `'default'` | Light marker on light backgrounds (default) |\n| `'inverted'` | Dark marker that blends with dark backgrounds |\n| `'brand'` | Uses `brandColor` as the marker accent color |\n\nAll themes adapt automatically to the `theme: 'dark'` setting.\n\n### Navigation Hotspots\n\nHotspots with a `navigateTo` property become navigation pins that link between scenes:\n\n```javascript\nconst hotspots = [\n  {\n    id: 'go-to-interior',\n    navigateTo: 'interior-scene',\n    label: 'View Interior',\n    arrowDirection: 90, // Point downward (default is right)\n    positions: { 10: { x: 600, y: 400 } },\n    // ...\n  },\n];\n\nconst config = {\n  hotspots,\n  onNavigate: (sceneId) => {\n    // Handle scene transition\n    console.log(`Navigate to: ${sceneId}`);\n  },\n};\n```\n\nNavigation hotspots display a directional arrow icon. Use `arrowDirection` to rotate the arrow (in degrees, default `0` points right).\n\n---\n\n## Interaction Hints\n\nThe viewer displays helpful hints at the bottom showing users how to interact with the 360° view. Hints are automatically generated based on enabled features and hide after the first interaction.\n\n### Configuration\n\n```javascript\nconst config = {\n  // Auto-detect hints based on enabled features (default)\n  hints: true,\n\n  // Disable hints\n  hints: false,\n\n  // Custom hints array\n  hints: ['drag', 'dblclick', 'keys'],\n};\n```\n\n### Available Hint Types\n\n| Type | Desktop | Mobile | Description |\n|------|---------|--------|-------------|\n| `drag` | ✓ | - | \"Drag to rotate\" |\n| `swipe` | - | ✓ | \"Swipe to rotate\" |\n| `dblclick` | ✓ | - | \"Double-click to zoom\" |\n| `pinch` | - | ✓ | \"Pinch to zoom\" (when pinchZoom enabled) |\n| `keys` | ✓ | - | \"Use arrow keys\" (when keys enabled) |\n\n---\n\n## Styling & Theming\n\n### Built-in Themes\n\nApply a theme by setting the `theme` option or using the `ci360-theme-dark` class:\n\n```javascript\n// Via config\nconst config = {\n  theme: 'dark', // or 'light'\n  // ...other options\n};\n\n// Or via HTML\n<div class=\"cloudimage-360 ci360-theme-dark\" ...></div>\n```\n\n### CSS Variables (Recommended)\n\nThe easiest way to customize the viewer appearance:\n\n```css\n:root {\n  /* Buttons */\n  --ci360-button-bg: #f0f0f0;\n  --ci360-button-bg-hover: #e0e0e0;\n  --ci360-button-size: 40px;\n  --ci360-button-border-radius: 6px;\n  --ci360-button-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n\n  /* Icons */\n  --ci360-icon-color: #37414b;\n  --ci360-icon-color-hover: #1a1f24;\n  --ci360-icon-size: 20px;\n\n  /* 360° Indicator */\n  --ci360-initial-icon-bg: rgba(255, 255, 255, 0.9);\n  --ci360-initial-icon-color: #505050;\n  --ci360-initial-icon-size: 80px;\n  --ci360-initial-icon-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);\n\n  /* Loading Spinner */\n  --ci360-spinner-color: #fff;\n  --ci360-spinner-accent: #a3a3a3;\n  --ci360-spinner-size: 30px;\n\n  /* Fullscreen */\n  --ci360-fullscreen-bg: #fff;\n\n  /* Zoom Controls */\n  --ci360-zoom-controls-bg: rgba(255, 255, 255, 0.9);\n  --ci360-zoom-controls-bg-hover: rgba(255, 255, 255, 1);\n  --ci360-zoom-controls-color: #505050;\n  --ci360-zoom-controls-color-hover: #333333;\n  --ci360-zoom-controls-border-radius: 8px;\n  --ci360-zoom-controls-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);\n\n  /* Scroll Hint */\n  --ci360-scroll-hint-bg: rgba(0, 0, 0, 0.7);\n  --ci360-scroll-hint-color: #ffffff;\n  --ci360-scroll-hint-border-radius: 20px;\n\n  /* Hotspots */\n  --ci360-hotspot-color: #00aaff;\n  --ci360-hotspot-border: 1px solid #fff;\n  --ci360-hotspot-size: 18px;\n\n  /* Tooltips */\n  --ci360-popper-bg: rgba(255, 255, 255, 0.95);\n  --ci360-popper-color: #333;\n  --ci360-popper-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n  --ci360-popper-border-radius: 6px;\n\n  /* Hints Overlay */\n  --ci360-hints-bg: rgba(0, 0, 0, 0.75);\n  --ci360-hints-color: #ffffff;\n  --ci360-hints-font-size: 14px;\n  --ci360-hints-border-radius: 12px;\n\n  /* Bottom Circle Indicator */\n  --ci360-circle-color-start: rgba(0, 0, 0, 0.05);\n  --ci360-circle-color-mid: rgba(0, 0, 0, 0.3);\n  --ci360-circle-color-end: rgba(0, 0, 0, 0.05);\n  --ci360-circle-dot-color: rgba(0, 0, 0, 0.4);\n\n  /* Other */\n  --ci360-focus-color: #0066cc;\n  --ci360-overlay-bg: rgba(255, 255, 255, 1);\n}\n```\n\n### Custom Dark Theme Example\n\nIf you prefer to customize beyond the built-in dark theme:\n\n```css\n.my-dark-viewer {\n  --ci360-button-bg: rgba(30, 30, 35, 0.9);\n  --ci360-button-bg-hover: rgba(45, 45, 50, 0.95);\n  --ci360-icon-color: #e0e0e0;\n  --ci360-icon-color-hover: #ffffff;\n  --ci360-fullscreen-bg: #1a1a1f;\n  --ci360-initial-icon-bg: rgba(30, 30, 35, 0.9);\n  --ci360-initial-icon-color: #e0e0e0;\n  --ci360-popper-bg: rgba(40, 40, 45, 0.95);\n  --ci360-popper-color: #e0e0e0;\n  --ci360-hints-bg: rgba(255, 255, 255, 0.12);\n  --ci360-circle-color-mid: rgba(255, 255, 255, 0.25);\n  --ci360-circle-dot-color: rgba(255, 255, 255, 0.4);\n  --ci360-overlay-bg: rgba(26, 26, 31, 1);\n}\n```\n\n### Scope to Specific Viewer\n\n```css\n#my-special-viewer {\n  --ci360-button-bg: #4a90d9;\n  --ci360-icon-color: #ffffff;\n  --ci360-hotspot-color: #ff6b6b;\n}\n```\n\n### CSS Classes Reference\n\n| Class | Description |\n|-------|-------------|\n| `.cloudimage-360` | Main container |\n| `.cloudimage-360-inner-box` | Inner container |\n| `.cloudimage-360-button` | Control buttons |\n| `.cloudimage-360-icons-container` | Button container |\n| `.cloudimage-initial-icon` | 360° indicator icon |\n| `.cloudimage-360-view-360-circle` | Bottom progress indicator |\n| `.cloudimage-loading-spinner` | Loading spinner |\n| `.cloudimage-360-fullscreen-modal` | Fullscreen container |\n| `.cloudimage-360-zoom-controls` | Zoom controls toolbar |\n| `.cloudimage-360-hotspot` | Hotspot marker |\n| `.cloudimage-360-popper` | Hotspot tooltip |\n| `.cloudimage-360-hints-overlay` | Hints overlay container |\n| `.cloudimage-360-hints-container` | Hints content box |\n| `.cloudimage-360-hotspot-timeline` | Hotspot timeline container |\n| `.cloudimage-360-hotspot-timeline-track` | Timeline track |\n| `.cloudimage-360-hotspot-timeline-dot` | Timeline hotspot dot |\n| `.cloudimage-360-hotspot-timeline-indicator` | Timeline position indicator |\n| `.cloudimage-360-hotspot-timeline-tooltip` | Timeline dot tooltip (appears on hover) |\n| `.ci360-theme-dark` | Dark theme class |\n\n---\n\n## Methods\n\n### Instance Methods\n\n```javascript\nconst viewer = new CI360();\n\n// Initialize all viewers with class \"cloudimage-360\"\nviewer.initAll();\n\n// Initialize a specific container\nviewer.init(containerElement, config);\n\n// Get a viewer by its container ID\nconst view = viewer.getViewById('my-viewer');\n\n// Get all viewer instances\nconst allViews = viewer.getViews();\n\n// Update a viewer's configuration\nviewer.updateView('my-viewer', { speed: 50, autoplay: true });\n```\n\n### View Methods\n\n```javascript\nconst view = viewer.getViewById('my-viewer');\n\n// Playback control\nview.play();                    // Start autoplay\nview.stopAutoplay();            // Stop autoplay\n\n// Rotation (stopAtEdges: boolean, steps: number)\nview.moveLeft(false, 5);        // Rotate left by 5 frames\nview.moveRight(false, 5);       // Rotate right by 5 frames\nview.moveTop(false, 1);         // Rotate up by 1 frame (Y-axis)\nview.moveBottom(false, 1);      // Rotate down by 1 frame (Y-axis)\n\n// Navigation\nview.animateToFrame(36);        // Animate to frame 36\nview.animateToFrame(10, 'hotspot-1'); // Go to frame and show hotspot\n\n// UI control\nview.hideAllIcons();            // Hide all overlay icons\n\n// State\nview.activeImageX;              // Current X-axis frame (0-indexed)\nview.activeImageY;              // Current Y-axis frame (0-indexed)\nview.amountX;                   // Total X-axis frames\nview.amountY;                   // Total Y-axis frames\n\n// Cleanup\nview.destroy();                 // Destroy the viewer\n```\n\n---\n\n## Cloudimage Integration\n\nEnhance performance with [Cloudimage](https://cloudimage.io) CDN for responsive, optimized images.\n\n### Setup\n\n1. Register at [cloudimage.io](https://cloudimage.io) to get your token\n2. Add the token to your viewer configuration:\n\n```javascript\nconst config = {\n  folder: 'https://your-domain.com/images/',\n  filenameX: '{index}.jpg',\n  amountX: 36,\n  ciToken: 'your-cloudimage-token', // or use data-responsive attribute\n};\n```\n\n### Benefits\n\n- **25GB free CDN traffic** per month\n- **Automatic optimization** - WebP, AVIF conversion\n- **Responsive images** - Serve the right size for each device\n- **Global CDN** - Fast delivery worldwide\n- **Image transformations** - Resize, crop, filters on-the-fly\n\n---\n\n## Browser Support\n\n| Browser | Version |\n|---------|---------|\n| Chrome | 69+ |\n| Firefox | 105+ |\n| Safari | 16.4+ |\n| Edge | 79+ |\n| iOS Safari | 16.4+ |\n| Android Chrome | 69+ |\n\n> **Note:** This library uses OffscreenCanvas for optimal performance, which requires the browser versions listed above.\n\n---\n\n## Mobile Considerations\n\n### Memory Limitations\n\nMobile browsers (especially Safari) have strict memory limits that can cause tab crashes when loading many high-resolution images. The library includes built-in optimizations for mobile that are **automatically enabled**:\n\n- **Sequential image loading** (3 concurrent on mobile vs 6 on desktop)\n- **Main-thread canvas rendering** (avoids OffscreenCanvas memory issues on Safari)\n- **Reduced touch event rate** (30fps vs 100fps on desktop)\n- **Capped device pixel ratio** (max 2x on mobile)\n- **Automatic memory management** (releases off-screen viewers, frees memory when page backgrounded)\n\n### Recommended Settings for Mobile\n\n| Setting | Desktop | Mobile | Notes |\n|---------|---------|--------|-------|\n| `amountX` | 60-100+ | 30-40 max | Each image uses ~4MB GPU memory |\n| `zoomMax` | `3-5` | `2-3` | Higher zoom loads more pixels |\n\n### Detecting Mobile Devices\n\nThe library automatically detects mobile devices, but you can also adjust your configuration:\n\n```javascript\nconst isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(\n  navigator.userAgent\n);\n\nconst viewer = new CI360();\nviewer.init(container, {\n  folder: 'https://example.com/images/',\n  filenameX: '{index}.jpg',\n  amountX: isMobile ? 36 : 72,           // Fewer images on mobile\n  zoomMax: isMobile ? 2 : 5,             // Lower zoom on mobile\n});\n```\n\n### Memory Management API\n\nMemory management is **automatically enabled on mobile**. For desktop or manual control:\n\n```javascript\nconst viewer = new CI360();\nviewer.initAll();\n\n// Manually enable (already auto-enabled on mobile)\nviewer.enableMemoryManagement();\n\n// Disable if needed\nviewer.disableMemoryManagement();\n```\n\nThis uses IntersectionObserver to:\n- Release memory when viewers scroll off-screen\n- Reload images when viewers become visible again\n- Release all viewer memory when the page is backgrounded\n\n---\n\n## Migration Guide (v3 → v4)\n\nVersion 4 introduces significant improvements in performance, customization, and developer experience. This guide helps you upgrade from v3.\n\n### Breaking Changes\n\n#### 1. CSS Handling\n\nFor CDN users, CSS is now auto-injected (same as v3):\n\n```html\n<!-- v4: Just include the script -->\n<script src=\".../360-view.min.js\"></script>\n```\n\nFor npm/bundler users, import CSS separately:\n\n```javascript\nimport CI360 from '@cloudimage/360-view';\nimport '@cloudimage/360-view/css';\n```\n\n#### 2. Initialization API Changed\n\n```javascript\n// v3\nwindow.CI360.init();\nwindow.CI360.add('my-viewer');\nwindow.CI360.update('my-viewer', true);\nwindow.CI360.destroy();\n\n// v4\nconst viewer = new CI360();\nviewer.initAll();                           // Initialize all\nviewer.init(container, config);             // Initialize specific container\nviewer.updateView('my-viewer', newConfig);  // Update with new config\nviewer.getViewById('my-viewer').destroy();  // Destroy specific viewer\n```\n\n#### 3. Browser Requirements Changed\n\nv4 uses OffscreenCanvas for performance, requiring newer browsers:\n\n| Browser | v3 | v4 |\n|---------|-----|-----|\n| Safari | 12+ | **16.4+** |\n| iOS Safari | 12+ | **16.4+** |\n| Firefox | 55+ | **105+** |\n| Chrome | 60+ | 69+ |\n\n### Deprecated Configuration Options\n\nThe following options have been removed in v4:\n\n| v3 Option | v4 Alternative |\n|-----------|----------------|\n| `data-box-shadow` | Use CSS: `.cloudimage-360 { box-shadow: ... }` |\n| `data-ratio` | Container automatically maintains aspect ratio |\n| `data-lazy-selector` | Use `data-lazyload` (boolean) |\n| `data-hide-360-logo` | Use `data-initial-icon` (boolean, inverted) |\n| `data-logo-src` | Custom logos not supported; use CSS to hide |\n| `data-image-info` | Removed |\n| `data-request-responsive-images` | Removed |\n| `data-disable-drag` | Use `data-draggable` (inverted: `draggable=\"false\"`) |\n| `data-spin-reverse` | Use `data-drag-reverse` and `data-autoplay-reverse` |\n\n### Hotspot Configuration Changes\n\nHotspot properties have been simplified:\n\n```javascript\n// v3 - Multiple specific properties\nconst hotspot = {\n  id: 'feature-1',\n  title: 'Feature Title',\n  description: 'Description text',\n  url: 'https://example.com',\n  newTab: true,\n  moreDetailsUrl: 'https://example.com/details',\n  moreDetailsTitle: 'Learn More',\n  popupSelector: '#custom-popup',\n  arrow: true,\n  placement: 'top',\n  offset: [0, 10],\n  positions: { 0: { x: 100, y: 200 } },\n};\n\n// v4 - Flexible HTML content\nconst hotspot = {\n  id: 'feature-1',\n  orientation: 'x',\n  containerSize: [1200, 800],\n  positions: { 0: { x: 100, y: 200 } },\n  content: `\n    <div class=\"my-tooltip\">\n      <h3>Feature Title</h3>\n      <p>Description text</p>\n      <a href=\"https://example.com\" target=\"_blank\">Learn More</a>\n    </div>\n  `,\n  onClick: () => console.log('Clicked!'),\n};\n```\n\n| v3 Property | v4 Alternative |\n|-------------|----------------|\n| `title`, `description` | Use `content` with HTML |\n| `url`, `newTab` | Include `<a>` tag in `content` |\n| `moreDetailsUrl`, `moreDetailsTitle` | Include in `content` HTML |\n| `popupSelector` | Use `content` with your HTML |\n| `arrow`, `placement`, `offset` | Popper.js handles positioning automatically |\n| `open` | Removed; hotspots open on click/hover |\n\n### New Features in v4\n\nTake advantage of these new capabilities:\n\n#### CSS Variables for Theming\n\n```css\n:root {\n  --ci360-button-bg: #f0f0f0;\n  --ci360-icon-color: #333;\n  --ci360-hotspot-color: #00aaff;\n}\n```\n\n#### Built-in Themes\n\n```html\n<div class=\"cloudimage-360 ci360-theme-dark\" ...></div>\n```\n\n#### Event Callbacks\n\n```javascript\nconst config = {\n  onReady: (e) => console.log('Ready'),\n  onSpin: (e) => console.log(`Frame: ${e.activeImageX}`),\n  onFullscreenOpen: () => console.log('Fullscreen'),\n};\n```\n\n#### Interaction Hints\n\n```javascript\nconst config = {\n  hints: true,  // Auto-detect hints\n  // or\n  hints: ['drag', 'dblclick', 'keys'],  // Custom hints\n};\n```\n\n#### Pinch-to-Zoom (Mobile)\n\n```javascript\nconst config = {\n  pinchZoom: true,  // Enabled by default\n};\n```\n\n### Quick Migration Checklist\n\n- [ ] Add CSS file import alongside JS\n- [ ] Update initialization code to use `new CI360()`\n- [ ] Replace `data-disable-drag` with `data-draggable=\"false\"`\n- [ ] Replace `data-spin-reverse` with `data-drag-reverse`\n- [ ] Replace `data-hide-360-logo` with `data-initial-icon=\"false\"`\n- [ ] Update hotspot configs to use `content` instead of individual properties\n- [ ] Test on Safari 16.4+ (older versions not supported)\n- [ ] Consider adding CSS variables for customization\n- [ ] Consider adding event callbacks for analytics/tracking\n\n---\n\n## Contributing\n\nWe welcome contributions! Here's how you can help:\n\n- **[Report bugs](https://github.com/scaleflex/cloudimage-360-view/issues)** - Found a bug? Let us know!\n- **[Request features](https://github.com/scaleflex/cloudimage-360-view/issues)** - Have an idea? Share it!\n- **[Submit PRs](https://github.com/scaleflex/cloudimage-360-view/pulls)** - Code contributions are welcome!\n- **[Join discussions](https://github.com/scaleflex/cloudimage-360-view/discussions)** - Ask questions, share insights\n\n### Development Setup\n\n```bash\ngit clone https://github.com/scaleflex/cloudimage-360-view.git\ncd cloudimage-360-view\nnpm install\nnpm run dev\n```\n\n<details>\n<summary><strong>Contributors</strong></summary>\n<br>\n<a href=\"https://github.com/scaleflex/cloudimage-360-view/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=scaleflex/cloudimage-360-view\" alt=\"Contributors\">\n</a>\n</details>\n\n---\n\n## Support\n\nIf this library helped your project, consider buying me a coffee!\n\n<a href=\"https://buymeacoffee.com/dzmitry.stramavus\">\n  <img src=\"https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black\" alt=\"Buy Me A Coffee\">\n</a>\n\n---\n\n## License\n\nThis project is licensed under the [MIT License]([https://opensource.org/licenses/MIT](https://github.com/scaleflex/cloudimage-360-view/blob/master/LICENSE)).\n\n---\n\n<p align=\"center\">\n  Made with care by the <a href=\"https://www.scaleflex.com\">Scaleflex</a> team\n</p>\n","readmeFilename":"README.md"}