# Shopify Integration

Adding a 3D viewer to your Shopify store is a three-part process. First, you set up the custom data fields. Then, you add the Hangr block to your theme. Finally, you link each product to its 3D model. That's it.

This guide walks you through both steps. No coding required.

{% hint style="warning" %}
**Duplicate your theme before starting — and work only on the copy**

Do not make changes to your live theme. Before you begin, go to **Online Store → Themes**. Find your current active theme — it will be marked as such at the top of the page. **Click the ... menu** next to its **Customize** button and select **Duplicate**.

Shopify will create a new theme called **Copy of \[your theme name]**. Everything in this guide is done on that copy. Your live store will not be affected until you publish it at the end.
{% endhint %}

## What you need before starting

Before you begin, make sure you have the following ready.

* **A Hangr account with at least one 3D model generated**\
  Log into your Hangr dashboard, open the model you want to use, toggle to "Public", and copy the viewer URL. It will look something like `https://hangrsolutions.com/viewer/your-model-id`. You will paste this into Shopify later.
* **A Shopify 2.0 theme**\
  The Hangr block only works with modern Shopify themes (released after 2021). If you are unsure whether your theme is compatible, check the [Theme Compatibility](#theme-compatibility) section before continuing.
* **Access to your Shopify admin**\
  You will need to be logged into your Shopify store as an owner or staff member with theme editing permissions.

## Part 1: Create the custom data fields

This is a one-time setup that adds a 3D Viewer URL field to every product in your store. You must do this before adding the block — the "View in 3D" button will only appear on products that have a URL saved in this field.

{% stepper %}
{% step %}

### Go to Metafield Definitions

Go to **Shopify Admin → Settings → Metafields and Metaobjects** **→** Under **Metafield Definitions → Products**.&#x20;

Click **Add definition**.
{% endstep %}

{% step %}

### Create the main viewer URL field

Fill in the fields as follows:

<table><thead><tr><th width="228.31640625">Field</th><th>What to enter</th></tr></thead><tbody><tr><td>Name</td><td>3D Viewer URL</td></tr><tr><td>Namespace and key</td><td><code>custom.3d_viewer_url</code></td></tr><tr><td>Type</td><td>Single-line text</td></tr></tbody></table>

{% hint style="info" %}
**Cannot edit the namespace and key?** Click directly on the auto-generated text in that field — it does not look like a standard input box, but it is editable.
{% endhint %}

Click **Save**.&#x20;

A field called "3D Viewer URL" will now appear at the bottom of every product page in your admin.
{% endstep %}
{% endstepper %}

That is all you need for now. If you want to show multiple 3D models on a single product (for example, different variations of the same item), see [Showing multiple 3D models on one product](#showing-multiple-3d-models-on-one-product) for the additional fields to create.

## Part 2: Add the Hangr block to your theme

Think of this as installing the 3D viewer feature into your store. You only need to do this once.

{% stepper %}
{% step %}

### Open your theme's code editor

In your Shopify admin, go to **Online Store → Themes**.&#x20;

Find your **Copy of \[theme name]** (it will not be marked as your current theme) **→** **Click ... → Edit code**.

This opens a file manager for your theme. It might look technical, but you only need to find one folder.
{% endstep %}

{% step %}

### Create a new block file

In the left sidebar, look for a folder called **Blocks**. Click on it to expand it

Right-click the blocks header and **add a New File**.

A small input box will appear. **Type immediately** — if you click away before typing anything, the input will disappear and no file will be created.

**Type exactly:**

```
view-in-3d.liquid
```

Press **Enter** or click **Done**. A blank file will open on the right.
{% endstep %}

{% step %}

### Paste the block code

**Press the** <i class="fa-copy">:copy:</i> **on the full block code** from the [**Block Code**](/hangr-docs/integrations/shopify-integration.md#block-code) section at the bottom of this page and paste it into the blank file.&#x20;

Click **Save** in the top right corner.

{% hint style="warning" %}
**The tab may turn red after you paste the code — this is normal.**&#x20;

Shopify highlights certain tags as it parses the file. This does not mean the file was deleted or rejected. Click **Save** in the top-right corner and the file will be saved correctly. If you see a `FileSaveError: Tag 'schema' is missing` message, this usually means the file was not yet fully pasted — make sure the full block code is present before saving.
{% endhint %}

{% hint style="info" %}
Always use the latest version to make sure you have the latest features and bug fixes.
{% endhint %}
{% endstep %}

{% step %}

### Add the block to your product pages

Now you need to tell your theme where to show the "View in 3D" button on product pages.

Go to **Online Store → Themes** and find your **Copy of \[theme name]**.

Click the button next to it to open the Theme Editor. Depending on your store setup, this button may say **Customize** or **Review**. Either one opens the same editor.

**In the Theme Editor**, you will land on your homepage by default. You need to navigate to a product page:

* In the preview area, click through to a product from your homepage, **or**
* Use the page picker at the top of the editor and select **Products → Default product**

Once you are on a product page, look at the left sidebar. Find the section called **Product information** and click on it. Scroll down inside that section and click **Add block**.

A list of available blocks will appear. The Hangr block may not be visible immediately — either **type "Hangr"** in the search box, or click **Show all** and scroll down to find **View in 3D by Hangr**.

Select it. Once it appears in the sidebar, drag it to sit just below your Add to Cart button, or wherever you would like it.

Click **Save**.
{% endstep %}

{% step %}

### Check it worked

At this point you will **likely not see the "View in 3D" button** in the preview — that is expected.&#x20;

The button only appears on products that have a 3D Viewer URL saved in their metafield. You will add those in Part 3.
{% endstep %}
{% endstepper %}

## Part 3: Link your products to their 3D models

Now that the metafield and block are both set up, you need to tell each product which 3D model to show.

{% stepper %}
{% step %}

### Add the viewer URL to a product

Go to **Shopify Admin → Products** and open a product that has a generated 3D model in Hangr.

Scroll all the way to the bottom of the product page. You will see a section called **Metafields**. Find the **3D Viewer URL** field and paste in the viewer URL from your Hangr dashboard.

Click **Save**.
{% endstep %}

{% step %}

### Preview on your duplicate theme

Go back to **Online Store → Themes** and find your **Copy of \[theme name]** — not your live theme.&#x20;

**Click ... → Preview** to open a preview of your duplicate theme with Hangr installed.

Navigate to the product you just updated. You should now see the **"View in 3D" button**.&#x20;

Clicking it will open the 3D viewer.

{% hint style="warning" %}
**Make sure you are previewing the duplicate, not your live theme.** The live theme does not have the Hangr block installed yet. If you do not see the button, confirm you clicked Preview on the **Copy of \[theme name]**, not on your active theme.
{% endhint %}

{% hint style="success" %}
**Repeat Step 1 for each product.** You only need to set up the block and metafield definition once. For every additional product, go to that product in Shopify admin and paste its viewer URL into the 3D Viewer URL field.
{% endhint %}
{% endstep %}
{% endstepper %}

## Part 4: Publish your duplicate theme

Once everything looks good — the button appears, the viewer loads, labels are correct — you are ready to make your duplicate theme live.

{% stepper %}
{% step %}

### Publish the duplicate

Go to **Online Store → Themes**.&#x20;

Find your **Copy of \[theme name]** and rename to **\[theme name] (Hangr Installed).**

Click **Publish**.

Shopify will ask you to confirm. Once confirmed, **the duplicate becomes your active theme** and your live store will now **show the 3D viewer button** on all products that have a viewer URL set.
{% endstep %}

{% step %}

### If you need to undo

Your old theme is still in your Themes list — it **has not been deleted**. If you want to revert, simply go back to **Online Store → Themes**, find your previous theme, and **click ... → Publish** on that one instead.

You will not lose any of the changes you made to your original theme. The Hangr block and metafield definitions will also remain in place even if you switch back, so **you can re-publish the updated theme at any time.**
{% endstep %}
{% endstepper %}

## Showing multiple 3D models on one product

Some products come in different sizes or configurations — like a luggage brand that sells a cabin bag and a checked bag as one product listing. Hangr supports this by letting shoppers toggle between different 3D models inside the viewer.

To set this up, you need to **add extra custom data fields** for the additional views.

{% stepper %}
{% step %}

### Create the additional fields

Go back to **Settings → Metafields and Metaobjects** **→** under **Metafield Definitions → Products**.&#x20;

Click **Add definition** and add each of the following, **one at a time** and **arrange them accordingly, using the** <i class="fa-grip-dots-vertical">:grip-dots-vertical:</i> **at the side**:

<table><thead><tr><th width="226.71875">Name</th><th width="296.98828125">Namespace and key</th><th>Type</th></tr></thead><tbody><tr><td><mark style="color:green;">3D Viewer URL</mark></td><td><mark style="color:green;"><code>custom.3d_viewer_url</code></mark></td><td><mark style="color:green;">Single-line text</mark></td></tr><tr><td>3D Viewer Label — Default</td><td><code>custom.3d_viewer_label_default</code></td><td>Single-line text</td></tr><tr><td>3D Viewer URL — View 1</td><td><code>custom.3d_viewer_url_view1</code></td><td>Single-line text</td></tr><tr><td>3D Viewer Label — View 1</td><td><code>custom.3d_viewer_label_view1</code></td><td>Single-line text</td></tr><tr><td>3D Viewer URL — View 2</td><td><code>custom.3d_viewer_url_view2</code></td><td>Single-line text</td></tr><tr><td>3D Viewer Label — View 2</td><td><code>custom.3d_viewer_label_view2</code></td><td>Single-line text</td></tr></tbody></table>

The <mark style="color:green;">green row</mark> is the **main URL field from Part 1** — you already have this one. **Only add the remaining five.**

{% hint style="info" %}
**Cannot edit the namespace and key?**

Click directly on the auto-generated text in the namespace and key field to make it editable. It will not appear as a standard input box — you need to click on the value itself to change it.
{% endhint %}
{% endstep %}

{% step %}

### Fill them in on your product

Go to **Shopify Admin → Products** and open a product **scroll down to the Metafields** section. You will now see all the new fields. Paste the appropriate viewer URLs and enter labels so shoppers know what each view represents.

For example, a luggage product might look like this:

<table><thead><tr><th width="258.5859375">Field</th><th>Value</th></tr></thead><tbody><tr><td>3D Viewer URL</td><td><code>https://hangrsolutions.com/v/luggage-cabin</code></td></tr><tr><td>3D Viewer Label — Default</td><td>Cabin</td></tr><tr><td>3D Viewer URL — View 1</td><td><code>https://hangrsolutions.com/v/luggage-checked</code></td></tr><tr><td>3D Viewer Label — View 1</td><td>Checked</td></tr></tbody></table>

When a shopper opens the viewer, they will see buttons at the bottom letting them switch between the Cabin and Checked models.

{% hint style="info" %}
**Labels you leave blank will use the default**

If you do not fill in a label field, the viewer will use the default label set in your Theme Editor block settings. You only need to fill in label fields when you want a custom name for a specific product.
{% endhint %}
{% endstep %}
{% endstepper %}

## Changing how the button looks

The "View in 3D" button automatically matches your theme's existing button style, so it should already look consistent with the rest of your store.

If you want to adjust it, open the Theme Editor (**Online Store → Themes → Customize**), click the Hangr block in the left sidebar, and you will see the following options.

**Style Preset**\
Choose from Primary, Secondary, or Outline. These use your theme's existing button styles so the button always fits in with your design.

**Enable Custom Styling**\
Turn this on if you want full control over the button's colours, font size, padding, border, and more. Everything can be changed from the Theme Editor — no code needed.

{% hint style="warning" %}
If you enable custom styling and later switch to a different Shopify theme, you will need to re-check the button colours as they will not carry over automatically.
{% endhint %}

## Theme compatibility

The Hangr block works with Shopify 2.0 themes — all themes released from 2021 onwards that support the blocks system in the Theme Editor.

**Not sure if your theme qualifies?** In your Shopify admin, go to **Online Store → Themes → ... → Edit code** on your theme. In the left sidebar, look for a folder called **Blocks**. If that folder exists, your theme supports the Hangr block.

| Theme              | Compatible                   |
| ------------------ | ---------------------------- |
| Horizon            | Yes                          |
| Savour             | Yes                          |
| Atelier            | Yes                          |
| Ritual             | Yes                          |
| Fabric             | Yes                          |
| Tinker             | Yes                          |
| Rise               | Yes                          |
| Trade              | Yes                          |
| Publisher          | Yes                          |
| Craft (Horizon)    | Yes                          |
| Vessel             | Yes                          |
| Dawn               | No                           |
| Sense              | No                           |
| Craft (Original)   | No                           |
| Refresh            | No                           |
| Ride               | No                           |
| Studio             | No                           |
| Crave              | No                           |
| Colorblock         | No                           |
| Taste              | No                           |
| Origin             | No                           |
| Debut              | No                           |
| Brooklyn           | No                           |
| Boundless          | No                           |
| Narrative          | No                           |
| Simple             | No                           |
| Venture            | No                           |
| Supply             | No                           |
| Third-party themes | Likely – contact theme owner |

If your theme is not on this list or currently unsupported, contact us at [**hello@hangr.tech**](mailto:hello@hangr.tech) and we can help you out.

## Something not working?

<details>

<summary>The "View in 3D" button is not showing up</summary>

The most common reason is that the product does not have a viewer URL saved. Go to that product in Shopify admin, scroll to Metafields at the bottom of the page, and confirm that the **3D Viewer URL** field has a URL in it.

Also check that you are previewing your **Copy of \[theme name]**, not your live theme. The live theme does not have the Hangr block installed until you publish.

</details>

<details>

<summary>The block does not appear in the "Add block" list</summary>

The block file may have been saved in the wrong folder. Go back to **Edit code** and confirm that `view-in-3d.liquid` is inside the **Blocks** folder — not Snippets or Sections. If it is in the wrong place, delete it and create it again in the Blocks folder.

If the file is in the right place but still not visible when adding a block, try typing "**Hangr**" in the search box, or click **Show all** and scroll to the bottom of the list.

</details>

<details>

<summary>The viewer opens but shows a blank screen</summary>

Check that the URL you pasted into the product's metafield is the full viewer URL from Hangr, starting with `https://`. An incomplete or incorrect URL will cause a blank viewer. Also confirm that your model's visibility toggle is set to **Public** in your Hangr dashboard.

</details>

<details>

<summary>The new file tab turns red when I paste the code</summary>

This is normal behaviour in Shopify's code editor. The red colouring appears as Shopify parses the file — it does not mean the file was deleted or rejected. Click **Save** and the file will be saved correctly. If you see a `FileSaveError: Tag 'schema' is missing` message, make sure the full block code is pasted in and nothing is missing at the end.

</details>

<details>

<summary>My theme is not on the compatibility list</summary>

Contact us at [**hello@hangr.tech**](mailto:hello@hangr.tech) with your theme name and we will confirm whether it is compatible and guide you through the installation.

</details>

## Block code (v2.4.4)

***Updated: 23 April 2026***

Paste the full code below into your `view-in-3d.liquid` block file in in Part 2, Step 3.

{% code title="view-in-3d.liquid" overflow="wrap" expandable="true" %}

```liquid
{% comment %}
  ============================================================================
  View in 3D Button Block (Pop Up) by Hangr Solutions
  ============================================================================
  
  Displays a button that opens a 3D viewer in a modal popup
  Uses product metafield: custom.viewer_3d_url
  
  Created by Hangr Solutions - https://hangrsolutions.com
  
  ============================================================================
  VERSION: 2.4.4
  ============================================================================
  ============================================================================
  USAGE NOTES:
  ============================================================================
  
  METAFIELD SETUP:
  URLs:
  - Primary (Default View): custom.3d_viewer_url  (fallbacks: custom.viewer_3d_url, custom.threed_viewer_url)
  - View 1: custom.3d_viewer_url_view1  (optional, enables View 1 toggle chip)
  - View 2: custom.3d_viewer_url_view2  (optional, enables View 2 toggle chip)

  View Labels (optional — overrides theme block label settings per product):
  - Default View label: custom.3d_viewer_label_default
  - View 1 label:      custom.3d_viewer_label_view1
  - View 2 label:      custom.3d_viewer_label_view2
  If a label metafield is blank, the theme block setting is used as the fallback.
  
  TOGGLE BEHAVIOUR:
  - If only the Default URL is set: no toggle chips are shown
  - If Default + View 1 are set: two chips appear (toggling between them)
  - If all three are set: two chips always visible showing the non-active views
  
  STYLING:
  - Default: Uses theme's button styles (recommended)
  - Custom: Enable "Enable Custom Styling" for full control
  
  COMPATIBILITY:
  - Shopify 2.0 themes
  - Works with Shopify's native button classes
  - Responsive: Desktop, tablet, and mobile optimized
  
  ============================================================================
{% endcomment %}

{% liquid
  # Get product-specific 3D URL from metafields (Default View)
  assign product_3d_url = product.metafields.custom['3d_viewer_url']
  if product_3d_url == blank
    assign product_3d_url = product.metafields.custom.viewer_3d_url
  endif
  if product_3d_url == blank
    assign product_3d_url = product.metafields.custom.threed_viewer_url
  endif

  # Get View 1 and View 2 URLs
  assign product_3d_url_view1 = product.metafields.custom['3d_viewer_url_view1']
  assign product_3d_url_view2 = product.metafields.custom['3d_viewer_url_view2']

  # Resolve view labels: per-product metafield takes priority, falls back to theme block setting
  assign label_default = product.metafields.custom['3d_viewer_label_default']
  if label_default == blank
    assign label_default = block.settings.toggle_label_default
  endif
  assign label_view1 = product.metafields.custom['3d_viewer_label_view1']
  if label_view1 == blank
    assign label_view1 = block.settings.toggle_label_view1
  endif
  assign label_view2 = product.metafields.custom['3d_viewer_label_view2']
  if label_view2 == blank
    assign label_view2 = block.settings.toggle_label_view2
  endif

  # Generate unique ID for this block instance
  assign unique_id = 'view3d-' | append: block.id
  
  # Determine button class based on style preset
  assign button_class = 'view3d-button'
  if block.settings.button_style == 'primary'
    assign button_class = button_class | append: ' button'
  elsif block.settings.button_style == 'secondary'
    assign button_class = button_class | append: ' button-secondary'
  elsif block.settings.button_style == 'outline'
    assign button_class = button_class | append: ' button-outline'
  endif
  
  # Check if style overrides are enabled
  assign use_overrides = block.settings.enable_style_overrides

  # Determine if any alternate views exist
  assign has_view1 = false
  assign has_view2 = false
  if product_3d_url_view1 != blank
    assign has_view1 = true
  endif
  if product_3d_url_view2 != blank
    assign has_view2 = true
  endif
%}

{% if product_3d_url != blank %}
<div class="view-3d-block" data-block-id="{{ block.id }}">
  <button 
    id="{{ unique_id }}-btn" 
    class="{{ button_class }}"
    type="button"
    aria-label="{{ block.settings.button_text }}"
    {% if use_overrides %}
    style="
      --button-bg-override: {{ block.settings.button_background }};
      --button-text-override: {{ block.settings.button_text_color }};
      --button-border-override: {{ block.settings.button_border_color }};
      --button-hover-bg-override: {{ block.settings.button_hover_background }};
      --button-hover-text-override: {{ block.settings.button_hover_text_color }};
      --button-font-size-override: {{ block.settings.font_size }}px;
      --button-font-weight-override: {{ block.settings.font_weight }};
      --button-border-width-override: {{ block.settings.border_width }}px;
      --button-border-radius-override: {{ block.settings.border_radius }}px;
      --button-padding-v-override: {{ block.settings.padding_vertical }}px;
      --button-padding-h-override: {{ block.settings.padding_horizontal }}px;
      --button-text-transform-override: {{ block.settings.text_transform }};
      --button-letter-spacing-override: {{ block.settings.letter_spacing }}px;
    "
    {% endif %}
  >
    {% if block.settings.show_icon %}
      <svg width="24" height="13" viewBox="0 0 122.88 65.79" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-right: 8px;">
        <path d="M13.37,31.32c-22.23,12.2,37.65,19.61,51.14,19.49v-7.44l11.21,11.2L64.51,65.79v-6.97 C37.4,59.85-26.41,42.4,11.97,27.92c0.36,1.13,0.8,2.2,1.3,3.2L13.37,31.32L13.37,31.32z M108.36,8.31c0-2.61,0.47-4.44,1.41-5.48 c0.94-1.04,2.37-1.56,4.3-1.56c0.92,0,1.69,0.12,2.28,0.34c0.59,0.23,1.08,0.52,1.45,0.89c0.38,0.36,0.67,0.75,0.89,1.15 c0.22,0.4,0.39,0.87,0.52,1.41c0.26,1.02,0.38,2.09,0.38,3.21c0,2.49-0.42,4.32-1.27,5.47c-0.84,1.15-2.29,1.73-4.36,1.73 c-1.15,0-2.09-0.19-2.8-0.55c-0.71-0.37-1.3-0.91-1.75-1.62c-0.33-0.51-0.59-1.2-0.77-2.07C108.45,10.34,108.36,9.38,108.36,8.31 L108.36,8.31z M26.47,10.49l-9-1.6c0.75-2.86,2.18-5.06,4.31-6.59C23.9,0.77,26.91,0,30.8,0c4.47,0,7.69,0.83,9.69,2.5 c1.99,1.67,2.98,3.77,2.98,6.29c0,1.48-0.41,2.82-1.21,4.01c-0.81,1.2-2.02,2.25-3.65,3.15c1.32,0.33,2.34,0.71,3.03,1.15 c1.14,0.7,2.02,1.63,2.65,2.77c0.63,1.15,0.95,2.51,0.95,4.1c0,2-0.52,3.91-1.56,5.75c-1.05,1.83-2.55,3.24-4.51,4.23 c-1.96,0.99-4.54,1.48-7.74,1.48c-3.11,0-5.57-0.37-7.36-1.1c-1.8-0.73-3.28-1.8-4.44-3.22c-1.16-1.41-2.05-3.19-2.67-5.33 l9.53-1.27c0.38,1.92,0.95,3.26,1.74,4.01c0.78,0.74,1.78,1.12,3,1.12c1.27,0,2.33-0.47,3.18-1.4c0.85-0.93,1.27-2.18,1.27-3.74 c0-1.59-0.41-2.82-1.22-3.69c-0.81-0.87-1.92-1.31-3.32-1.31c-0.74,0-1.77,0.18-3.07,0.56l0.49-6.81c0.52,0.08,0.93,0.12,1.22,0.12 c1.23,0,2.26-0.4,3.08-1.19c0.82-0.79,1.24-1.72,1.24-2.81c0-1.05-0.31-1.88-0.93-2.49c-0.62-0.62-1.48-0.93-2.55-0.93 c-1.12,0-2.02,0.34-2.72,1.01C27.19,7.62,26.72,8.8,26.47,10.49L26.47,10.49z M75.15,8.27l-9.48,1.16 c-0.25-1.32-0.66-2.24-1.24-2.78c-0.59-0.54-1.31-0.81-2.16-0.81c-1.54,0-2.74,0.77-3.59,2.33c-0.62,1.13-1.09,3.52-1.38,7.19 c1.14-1.16,2.31-2.01,3.5-2.56c1.2-0.55,2.59-0.83,4.16-0.83c3.06,0,5.64,1.09,7.75,3.27c2.11,2.19,3.17,4.96,3.17,8.31 c0,2.26-0.53,4.32-1.6,6.2c-1.07,1.87-2.55,3.29-4.44,4.25c-1.9,0.96-4.27,1.44-7.13,1.44c-3.43,0-6.18-0.58-8.25-1.76 c-2.07-1.17-3.73-3.03-4.97-5.59c-1.24-2.56-1.86-5.95-1.86-10.18c0-6.18,1.3-10.71,3.91-13.59C54.13,1.44,57.74,0,62.36,0 c2.73,0,4.88,0.31,6.46,0.94c1.58,0.63,2.9,1.56,3.94,2.76C73.81,4.92,74.61,6.44,75.15,8.27L75.15,8.27z M57.62,23.55 c0,1.86,0.47,3.31,1.4,4.36c0.94,1.05,2.08,1.58,3.44,1.58c1.25,0,2.3-0.48,3.14-1.43c0.84-0.95,1.26-2.37,1.26-4.26 c0-1.93-0.44-3.41-1.31-4.42c-0.88-1.01-1.96-1.52-3.26-1.52c-1.32,0-2.44,0.49-3.34,1.48C58.06,20.32,57.62,21.72,57.62,23.55 L57.62,23.55z M77.91,17.57c0-6.51,1.17-11.07,3.52-13.67C83.77,1.3,87.35,0,92.14,0c2.31,0,4.2,0.29,5.68,0.85 c1.48,0.57,2.69,1.31,3.62,2.22c0.94,0.91,1.68,1.87,2.21,2.87c0.54,1.01,0.97,2.18,1.3,3.52c0.64,2.55,0.96,5.22,0.96,8 c0,6.22-1.05,10.76-3.16,13.64c-2.1,2.88-5.72,4.32-10.87,4.32c-2.88,0-5.21-0.46-6.99-1.38c-1.78-0.92-3.23-2.27-4.37-4.05 c-0.82-1.26-1.47-2.98-1.93-5.17C78.14,22.64,77.91,20.22,77.91,17.57L77.91,17.57z M87.34,17.59c0,4.36,0.38,7.34,1.16,8.94 c0.77,1.6,1.89,2.39,3.36,2.39c0.97,0,1.8-0.34,2.51-1.01c0.71-0.68,1.23-1.76,1.56-3.22c0.34-1.47,0.5-3.75,0.5-6.85 c0-4.55-0.38-7.6-1.16-9.18c-0.77-1.56-1.93-2.35-3.47-2.35c-1.58,0-2.71,0.8-3.42,2.39C87.69,10.31,87.34,13.27,87.34,17.59 L87.34,17.59z M112.14,8.32c0,1.75,0.15,2.94,0.46,3.58c0.31,0.64,0.76,0.96,1.35,0.96c0.39,0,0.72-0.13,1.01-0.41 c0.28-0.27,0.49-0.7,0.63-1.29c0.13-0.59,0.2-1.5,0.2-2.74c0-1.82-0.15-3.05-0.46-3.68c-0.31-0.63-0.77-0.94-1.39-0.94 c-0.63,0-1.09,0.32-1.37,0.96C112.28,5.4,112.14,6.59,112.14,8.32L112.14,8.32z M109.3,30.23c10.56,5.37,8.04,12.99-10.66,17.62 c-5.3,1.31-11.29,2.5-17.86,2.99v6.05c7.31-0.51,14.11-2.19,20.06-3.63c28.12-6.81,27.14-18.97,9.36-25.83 C109.95,28.42,109.65,29.35,109.3,30.23L109.3,30.23z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"/>
      </svg>
    {% endif %}
    {{ block.settings.button_text }}
  </button>

  <!-- Modal Overlay -->
  <div id="{{ unique_id }}-modal" class="view3d-modal"
    data-primary-url="{{ product_3d_url }}"
    data-view1-url="{{ product_3d_url_view1 }}"
    data-view2-url="{{ product_3d_url_view2 }}"
    data-label-default="{{ label_default }}"
    data-label-view1="{{ label_view1 }}"
    data-label-view2="{{ label_view2 }}"
  >
    <div class="view3d-modal-content">
      <!-- Close Button -->
      <button class="view3d-close" type="button" aria-label="Close 3D viewer">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M18 6L6 18M6 6L18 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
      </button>

      <!-- 3D Viewer Iframes — all preloaded, opacity toggled by JS -->
      <div class="view3d-iframe-stack">
        <iframe
          id="{{ unique_id }}-iframe-default"
          src=""
          data-view="default"
          frameborder="0"
          allowfullscreen
          allowtransparency="true"
          title="3D Product Viewer — Default View"
          style="background: transparent;"
        ></iframe>
        {% if has_view1 %}
        <iframe
          id="{{ unique_id }}-iframe-view1"
          src=""
          data-view="view1"
          frameborder="0"
          allowfullscreen
          title="3D Product Viewer — View 1"
        ></iframe>
        {% endif %}
        {% if has_view2 %}
        <iframe
          id="{{ unique_id }}-iframe-view2"
          src=""
          data-view="view2"
          frameborder="0"
          allowfullscreen
          title="3D Product Viewer — View 2"
        ></iframe>
        {% endif %}

        <!-- Desktop-only toggle chips — overlaid on viewer, hidden on mobile -->
        {% if has_view1 or has_view2 %}
        <div class="view3d-toggle-group view3d-toggle-group--desktop" id="{{ unique_id }}-toggle-group-desktop">
          <!-- Chips injected by JS -->
        </div>
        {% endif %}
      </div>

      <!-- Mobile-only toggle chips — below iframe stack, horizontally scrollable -->
      {% if has_view1 or has_view2 %}
      <div class="view3d-toggle-strip" id="{{ unique_id }}-toggle-group-mobile">
        <!-- Chips injected by JS -->
      </div>
      {% endif %}

    </div>
  </div>
</div>

<style>
  /* Block Container */
  .view-3d-block {
    display: block;
    width: 100%;
  }

  /* Button Base Styles - Inherits from theme */
  .view3d-button {
    width: 100%;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    transition: all 0.3s ease;
    font-family: inherit;
  }

  .view3d-button svg {
    flex-shrink: 0;
  }

  /* Style Overrides - Only apply if enabled */
  {% if use_overrides %}
  .view3d-button {
    background: var(--button-bg-override) !important;
    color: var(--button-text-override) !important;
    border: var(--button-border-width-override) solid var(--button-border-override) !important;
    padding: var(--button-padding-v-override) var(--button-padding-h-override) !important;
    font-size: var(--button-font-size-override) !important;
    font-weight: var(--button-font-weight-override) !important;
    border-radius: var(--button-border-radius-override) !important;
    text-transform: var(--button-text-transform-override) !important;
    letter-spacing: var(--button-letter-spacing-override) !important;
  }

  .view3d-button:hover {
    background: var(--button-hover-bg-override) !important;
    color: var(--button-hover-text-override) !important;
    transform: translateY(-2px);
  }

  .view3d-button:active {
    transform: translateY(0);
  }
  {% endif %}

  /* Modal Styles */
  .view3d-modal {
    display: none;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    width: 100%;
    height: 100%;
    background-color: transparent;
    z-index: 9999;
    opacity: 0;
    transition: opacity 0.3s ease;
    overflow: hidden;
  }

  .view3d-modal.active {
    opacity: 1;
  }

  .view3d-modal-content {
    position: absolute;
    width: 90%;
    max-width: 1200px;
    height: 80%;
    max-height: 800px;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(0.8);
    background: rgba(200, 200, 200, 0.3);
    backdrop-filter: blur(20px);
    -webkit-backdrop-filter: blur(20px);
    border: 1px solid rgba(255, 255, 255, 0.2);
    border-radius: 12px;
    box-shadow: 0 25px 50px rgba(0, 0, 0, 0.3);
    transition: transform 0.3s ease;
    isolation: isolate;
    display: flex;
    flex-direction: column;
  }

  .view3d-modal.active .view3d-modal-content {
    transform: translate(-50%, -50%) scale(1);
  }

  /* Close Button */
  .view3d-close {
    position: absolute;
    top: 20px;
    right: 20px;
    background: rgba(80, 80, 80, 0.8);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: white;
    border: none;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    cursor: pointer;
    z-index: 10001;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s ease;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
  }

  .view3d-close:hover {
    background: rgba(100, 100, 100, 0.9);
    transform: scale(1.1);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
  }

  /* View Toggle Group — desktop only: bottom-right overlay on viewer */
  .view3d-toggle-group--desktop {
    position: absolute;
    bottom: 20px;
    right: 20px;
    display: flex;
    flex-direction: row;
    gap: 8px;
    z-index: 10001;
    align-items: center;
  }

  /* Mobile chip strip — sits below the iframe, horizontally scrollable */
  .view3d-toggle-strip {
    display: none; /* shown only on mobile via media query */
    flex-shrink: 0;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    scrollbar-width: none; /* Firefox */
    padding: 10px 16px;
    padding-bottom: max(10px, env(safe-area-inset-bottom, 10px));
    gap: 8px;
    align-items: center;
    /* Fully transparent — inherits glassmorphism from parent modal container.
       Applying its own backdrop-filter here would blur the modal content
       sitting behind the strip, making it look darker than the rest. */
    background: transparent;
    border-top: 0px solid rgba(255, 255, 255, 0);
  }

  .view3d-toggle-strip::-webkit-scrollbar {
    display: none; /* Chrome/Safari */
  }

  /* Individual view chip — same pill aesthetic as the "Drag to rotate / Scroll to zoom" bar */
  .view3d-chip {
    background: rgba(80, 80, 80, 0.65);
    backdrop-filter: blur(10px);
    -webkit-backdrop-filter: blur(10px);
    color: rgba(255, 255, 255, 0.75);
    border: 1px solid rgba(255, 255, 255, 0.15);
    padding: 8px 14px;
    border-radius: 999px;
    cursor: pointer;
    font-size: 13px;
    font-weight: 500;
    font-family: inherit;
    letter-spacing: 0.02em;
    white-space: nowrap;
    /* Fixed minimum width prevents layout shift between view labels */
    min-width: 80px;
    text-align: center;
    transition: background 0.2s ease, transform 0.15s ease, box-shadow 0.2s ease, color 0.2s ease;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
    line-height: 1;
  }

  /* Active chip — darker, full opacity, not interactive */
  .view3d-chip.active {
    background: rgba(15, 15, 15, 0.92);
    color: rgba(255, 255, 255, 1);
    border-color: rgba(255, 255, 255, 0.3);
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
    cursor: default;
    pointer-events: none;
  }

  .view3d-chip:not(.active):hover {
    background: rgba(110, 110, 110, 0.85);
    color: rgba(255, 255, 255, 0.95);
    transform: translateY(-1px);
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.35);
  }

  .view3d-chip:not(.active):active {
    transform: translateY(0);
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
  }

  /* Iframe Stack — all iframes fill the same space, opacity toggled by JS */
  .view3d-iframe-stack {
    position: relative;
    flex: 1;
    border-radius: 12px;
    overflow: hidden;
    min-height: 0; /* required for flex child to shrink properly */
  }

  .view3d-iframe-stack iframe {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    border: none;
    background: transparent !important;
    border-radius: 12px;
    clip-path: inset(0 round 12px);
    opacity: 0;
    /* Keep in render tree so WebGL context stays warm */
    pointer-events: none;
    transition: opacity 0.2s ease;
  }

  .view3d-iframe-stack iframe.active {
    opacity: 1;
    pointer-events: auto;
  }

  /* Mobile Responsive */
  @media (max-width: 768px) {
    .view3d-modal-content {
      width: 100%;
      height: 100%;
      max-height: 100%;
      margin: 0;
      border-radius: 0;
      border: none;
      /* MOBILE: blur restored — safe with single WebGL context architecture */
      backdrop-filter: blur(20px);
      -webkit-backdrop-filter: blur(20px);
      background: rgba(200, 200, 200, 0.3);
      /* isolation: isolate stays off — was compounding compositing issues */
      isolation: auto;
      /* Flex column: iframe fills remaining space, chip strip sits below */
      display: flex;
      flex-direction: column;
    }

    .view3d-close {
      /* MOBILE FIX: safe-area-inset keeps button clear of notch / Dynamic Island */
      top: max(10px, env(safe-area-inset-top, 10px));
      right: max(10px, env(safe-area-inset-right, 10px));
      background: rgba(0, 0, 0, 0.8);
      /* MOBILE: blur restored — safe with single WebGL context */
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      z-index: 10002;
    }

    /* Hide desktop overlay chips on mobile */
    .view3d-toggle-group--desktop {
      display: none !important;
    }

    /* Show mobile chip strip */
    .view3d-toggle-strip {
      display: flex;
      /* Strip is transparent — it sits inside the modal which provides the blur.
         No separate backdrop-filter here to avoid blurring modal content. */
      background: transparent;
      border-top: 1px solid rgba(255, 255, 255, 0.2);
    }

    .view3d-chip {
      font-size: 12px;
      padding: 7px 12px;
      min-width: 68px;
      /* MOBILE FIX: iOS HIG minimum touch target is 44px */
      min-height: 44px;
      /* MOBILE: blur restored — safe with single WebGL context */
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
      /* Prevent chips from shrinking in scroll container */
      flex-shrink: 0;
    }

    /* Chips disabled during iframe load — prevent mid-load switching */
    .view3d-chip.loading {
      opacity: 0.4;
      cursor: not-allowed;
      pointer-events: none;
    }

    .view3d-iframe-stack {
      border-radius: 0;
      /* iframe stack fills remaining flex space above chip strip */
      flex: 1;
      min-height: 0;
      position: relative;
    }

    /* Mobile iframes are created dynamically — these styles apply to the
       single live iframe injected by JS */
    .view3d-iframe-stack iframe {
      height: 100%;
      border-radius: 0;
      clip-path: none;
    }
  }

  @media (max-width: 480px) {
    .view3d-button svg {
      width: 20px;
      height: 11px;
    }
  }
</style>

<script>
(function() {
  const uniqueId = '{{ unique_id }}';
  const button   = document.getElementById(uniqueId + '-btn');
  const modal    = document.getElementById(uniqueId + '-modal');
  const closeBtn = modal ? modal.querySelector('.view3d-close') : null;
  const iframeStack      = modal ? modal.querySelector('.view3d-iframe-stack') : null;
  const toggleGroupDesktop = document.getElementById(uniqueId + '-toggle-group-desktop');
  const toggleGroupMobile  = document.getElementById(uniqueId + '-toggle-group-mobile');

  if (!button || !modal) return;

  const primaryUrl   = modal.dataset.primaryUrl  || '';
  const view1Url     = modal.dataset.view1Url     || '';
  const view2Url     = modal.dataset.view2Url     || '';
  const labelDefault = modal.dataset.labelDefault || 'Default View';
  const labelView1   = modal.dataset.labelView1   || 'View 1';
  const labelView2   = modal.dataset.labelView2   || 'View 2';

  // View config — URLs and labels only; iframes are managed dynamically on mobile
  const views = {
    default: { url: primaryUrl, label: labelDefault },
    view1:   { url: view1Url,   label: labelView1   },
    view2:   { url: view2Url,   label: labelView2   }
  };

  // Desktop iframe references (static DOM elements, stacked architecture)
  const desktopIframes = {
    default: document.getElementById(uniqueId + '-iframe-default'),
    view1:   document.getElementById(uniqueId + '-iframe-view1'),
    view2:   document.getElementById(uniqueId + '-iframe-view2')
  };

  let currentView  = 'default';
  let isLoading    = false; // true while active mobile iframe is loading
  let mobileIframe = null;  // the single live iframe on mobile

  const isMobile = function() { return window.innerWidth <= 768; };

  // ─── CHIP RENDERING ────────────────────────────────────────────────────────

  function renderChipsInto(container) {
    if (!container) return;
    container.innerHTML = '';
    Object.keys(views).forEach(function(key) {
      if (!views[key].url) return;
      const chip = document.createElement('button');
      chip.type = 'button';
      chip.textContent = views[key].label;
      if (key === currentView) {
        chip.className = 'view3d-chip active';
        chip.setAttribute('aria-current', 'true');
        chip.setAttribute('aria-label', views[key].label + ' (active)');
      } else {
        chip.className = 'view3d-chip' + (isLoading ? ' loading' : '');
        chip.setAttribute('aria-label', 'Switch to ' + views[key].label);
        if (!isLoading) {
          chip.addEventListener('click', function() { switchView(key); });
        }
      }
      container.appendChild(chip);
    });
  }

  function renderChips() {
    renderChipsInto(toggleGroupDesktop);
    renderChipsInto(toggleGroupMobile);
    // Auto-scroll active chip into view in the mobile strip
    if (toggleGroupMobile) {
      var activeChip = toggleGroupMobile.querySelector('.view3d-chip.active');
      if (activeChip) {
        activeChip.scrollIntoView({ behavior: 'smooth', inline: 'nearest', block: 'nearest' });
      }
    }
  }

  // ─── DESKTOP — stacked iframe architecture (unchanged) ────────────────────

  function desktopShowView(key) {
    Object.keys(desktopIframes).forEach(function(k) {
      var el = desktopIframes[k];
      if (!el) return;
      el.classList.toggle('active', k === key);
    });
  }

  function desktopSeedIframes() {
    Object.keys(desktopIframes).forEach(function(key) {
      var el = desktopIframes[key];
      if (!el || !views[key].url) return;
      if (!el.src || el.src === window.location.href) {
        el.src = views[key].url;
        el.setAttribute('allowtransparency', 'true');
      }
    });
  }

  function desktopClearIframes() {
    setTimeout(function() {
      Object.keys(desktopIframes).forEach(function(key) {
        var el = desktopIframes[key];
        if (el) { el.src = ''; el.classList.remove('active'); }
      });
    }, 0);
  }

  function desktopSwitchView(targetKey) {
    if (!views[targetKey] || !views[targetKey].url) return;
    var el = desktopIframes[targetKey];
    if (el && (!el.src || el.src === window.location.href)) {
      el.src = views[targetKey].url;
      el.setAttribute('allowtransparency', 'true');
    }
    currentView = targetKey;
    desktopShowView(targetKey);
    renderChips();
  }

  // ─── MOBILE — single iframe architecture ──────────────────────────────────
  // Only one iframe (one WebGL context) lives in the DOM at a time.
  // On switch: old iframe is removed, new one is created and appended.
  // Chips are disabled (loading state) while the iframe is loading.

  function mobileCreateIframe(url) {
    var iframe = document.createElement('iframe');
    iframe.src = url;
    iframe.frameBorder = '0';
    iframe.allowFullscreen = true;
    iframe.title = '3D Product Viewer';
    iframe.className = 'active';
    iframe.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:none;border-radius:0;clip-path:none;opacity:1;pointer-events:auto;background:transparent;';
    return iframe;
  }

  function mobileDestroyIframe() {
    if (mobileIframe) {
      // Blank src first to stop any in-flight WebGL load before removal
      mobileIframe.src = '';
      if (mobileIframe.parentNode) mobileIframe.parentNode.removeChild(mobileIframe);
      mobileIframe = null;
    }
  }

  function mobileSwitchView(targetKey) {
    if (isLoading) return; // hard block — chip click handler also checks this
    if (!views[targetKey] || !views[targetKey].url) return;

    isLoading = true;
    currentView = targetKey;
    renderChips(); // chips go into loading state immediately

    // Destroy old iframe, create new one for target view
    mobileDestroyIframe();
    mobileIframe = mobileCreateIframe(views[targetKey].url);

    mobileIframe.addEventListener('load', function() {
      isLoading = false;
      renderChips(); // re-enable chips
    }, { once: true });

    if (iframeStack) iframeStack.appendChild(mobileIframe);
  }

  function mobileOpen() {
    isLoading = true;
    currentView = 'default';
    renderChips();

    mobileDestroyIframe(); // safety — clear any leftover
    mobileIframe = mobileCreateIframe(views['default'].url);

    mobileIframe.addEventListener('load', function() {
      isLoading = false;
      renderChips();
    }, { once: true });

    if (iframeStack) iframeStack.appendChild(mobileIframe);
  }

  function mobileClose() {
    isLoading = false;
    // Yield to browser before destroying to avoid WKWebView navigation abort
    setTimeout(function() { mobileDestroyIframe(); }, 0);
  }

  // ─── UNIFIED switchView ───────────────────────────────────────────────────

  function switchView(targetKey) {
    if (isMobile()) {
      mobileSwitchView(targetKey);
    } else {
      desktopSwitchView(targetKey);
    }
  }

  // ─── MODAL OPEN / CLOSE ───────────────────────────────────────────────────

  var modalOriginalParent = modal.parentNode;

  function openModal() {
    if (!primaryUrl || primaryUrl.trim() === '') {
      console.warn('[Hangr] No 3D viewer URL configured for this product.');
      return;
    }

    currentView = 'default';

    // Z-INDEX FIX: teleport modal to document.body to escape theme stacking contexts
    if (modal.parentNode !== document.body) {
      document.body.appendChild(modal);
    }

    modal.style.display = 'block';
    document.body.style.overflow = 'hidden';

    if (isMobile()) {
      document.body.style.position = 'fixed';
      document.body.style.width = '100%';
      mobileOpen();
    } else {
      desktopSeedIframes();
      desktopShowView('default');
    }

    renderChips();

    requestAnimationFrame(function() {
      modal.classList.add('active');
    });
  }

  function closeModal() {
    modal.classList.remove('active');

    setTimeout(function() {
      modal.style.display = 'none';
      document.body.style.overflow = '';
      document.body.style.position = '';
      document.body.style.width = '';
      currentView = 'default';

      if (isMobile()) {
        mobileClose();
      } else {
        desktopClearIframes();
      }

      // Z-INDEX FIX: restore modal to original DOM position
      if (modal.parentNode !== modalOriginalParent) {
        modalOriginalParent.appendChild(modal);
      }
    }, 300);
  }

  // ─── EVENT LISTENERS ──────────────────────────────────────────────────────

  button.addEventListener('click', openModal);
  if (closeBtn) closeBtn.addEventListener('click', closeModal);

  modal.addEventListener('click', function(e) {
    if (e.target === modal) closeModal();
  });

  document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && modal.classList.contains('active')) closeModal();
  });
})();
</script>
{% endif %}

{% schema %}
{
  "name": "View in 3D by Hangr",
  "tag": null,
  "settings": [
    {
      "type": "paragraph",
      "content": "3D Viewer by Hangr Solutions — Opens a 3D viewer in a popup. Set the Default View URL in the product metafield: custom.3d_viewer_url. Optionally add View 1 and View 2 URLs to enable view switching."
    },
    {
      "type": "header",
      "content": "Button Content"
    },
    {
      "type": "text",
      "id": "button_text",
      "label": "Button Text",
      "default": "View in 360°"
    },
    {
      "type": "checkbox",
      "id": "show_icon",
      "label": "Show 360° Icon",
      "default": true
    },
    {
      "type": "header",
      "content": "View Toggle Labels"
    },
    {
      "type": "paragraph",
      "content": "Labels shown on the toggle chips inside the viewer. Each chip switches to that view. Chips only appear when the corresponding metafield URL is set: custom.3d_viewer_url_view1 and custom.3d_viewer_url_view2."
    },
    {
      "type": "text",
      "id": "toggle_label_default",
      "label": "Default View Label",
      "default": "Default View",
      "info": "Label for the chip that switches back to the default 3D model. Override per-product via metafield: custom.3d_viewer_label_default"
    },
    {
      "type": "text",
      "id": "toggle_label_view1",
      "label": "View 1 Label",
      "default": "View 1",
      "info": "Label for the chip that switches to View 1 (requires custom.3d_viewer_url_view1). Override per-product via metafield: custom.3d_viewer_label_view1"
    },
    {
      "type": "text",
      "id": "toggle_label_view2",
      "label": "View 2 Label",
      "default": "View 2",
      "info": "Label for the chip that switches to View 2 (requires custom.3d_viewer_url_view2). Override per-product via metafield: custom.3d_viewer_label_view2"
    },
    {
      "type": "header",
      "content": "Button Style"
    },
    {
      "type": "paragraph",
      "content": "Choose a preset style that matches your store's theme, or enable style overrides below for full customization."
    },
    {
      "type": "select",
      "id": "button_style",
      "label": "Style Preset",
      "options": [
        {
          "value": "primary",
          "label": "Primary"
        },
        {
          "value": "secondary",
          "label": "Secondary"
        },
        {
          "value": "outline",
          "label": "Outline"
        }
      ],
      "default": "secondary",
      "info": "Uses your theme's button styles"
    },
    {
      "type": "header",
      "content": "Style Overrides (Optional)"
    },
    {
      "type": "paragraph",
      "content": "Enable this to fully customize the button appearance. When disabled, the button uses your theme's style preset above."
    },
    {
      "type": "checkbox",
      "id": "enable_style_overrides",
      "label": "Enable Custom Styling",
      "default": false,
      "info": "Turn this on to customize colors, spacing, and typography below"
    },
    {
      "type": "header",
      "content": "Colors"
    },
    {
      "type": "color",
      "id": "button_background",
      "label": "Background Color",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "button_text_color",
      "label": "Text Color",
      "default": "#ffffff"
    },
    {
      "type": "color",
      "id": "button_border_color",
      "label": "Border Color",
      "default": "#000000"
    },
    {
      "type": "color",
      "id": "button_hover_background",
      "label": "Hover Background",
      "default": "#333333"
    },
    {
      "type": "color",
      "id": "button_hover_text_color",
      "label": "Hover Text Color",
      "default": "#ffffff"
    },
    {
      "type": "header",
      "content": "Typography"
    },
    {
      "type": "range",
      "id": "font_size",
      "label": "Font Size",
      "min": 12,
      "max": 24,
      "step": 1,
      "unit": "px",
      "default": 16
    },
    {
      "type": "select",
      "id": "font_weight",
      "label": "Font Weight",
      "options": [
        {
          "value": "400",
          "label": "Normal"
        },
        {
          "value": "500",
          "label": "Medium"
        },
        {
          "value": "600",
          "label": "Semi Bold"
        },
        {
          "value": "700",
          "label": "Bold"
        }
      ],
      "default": "600"
    },
    {
      "type": "select",
      "id": "text_transform",
      "label": "Text Transform",
      "options": [
        {
          "value": "none",
          "label": "None"
        },
        {
          "value": "uppercase",
          "label": "Uppercase"
        },
        {
          "value": "lowercase",
          "label": "Lowercase"
        },
        {
          "value": "capitalize",
          "label": "Capitalize"
        }
      ],
      "default": "uppercase"
    },
    {
      "type": "range",
      "id": "letter_spacing",
      "label": "Letter Spacing",
      "min": 0,
      "max": 3,
      "step": 0.5,
      "unit": "px",
      "default": 1
    },
    {
      "type": "header",
      "content": "Border & Spacing"
    },
    {
      "type": "range",
      "id": "border_width",
      "label": "Border Width",
      "min": 0,
      "max": 5,
      "step": 1,
      "unit": "px",
      "default": 2
    },
    {
      "type": "range",
      "id": "border_radius",
      "label": "Border Radius",
      "min": 0,
      "max": 50,
      "step": 1,
      "unit": "px",
      "default": 5
    },
    {
      "type": "range",
      "id": "padding_vertical",
      "label": "Vertical Padding",
      "min": 8,
      "max": 30,
      "step": 2,
      "unit": "px",
      "default": 12
    },
    {
      "type": "range",
      "id": "padding_horizontal",
      "label": "Horizontal Padding",
      "min": 12,
      "max": 50,
      "step": 2,
      "unit": "px",
      "default": 24
    }
  ],
  "presets": [
    {
      "name": "View in 3D by Hangr",
      "category": "Product"
    }
  ]
}
{% endschema %}

```

{% endcode %}

## Metafield reference

A full list of every custom data field the Hangr block uses.

<table><thead><tr><th width="151.890625">Field name</th><th width="300.6953125">Namespace and key</th><th width="109.99609375">Required</th><th>What it does</th></tr></thead><tbody><tr><td>3D Viewer URL</td><td><code>custom.3d_viewer_url</code></td><td>Required</td><td>The main viewer URL for this product. The button will not appear without this.</td></tr><tr><td>3D Viewer Label — Default</td><td><code>custom.3d_viewer_label_default</code></td><td>Optional</td><td>Custom name for the default view, shown on this product only.</td></tr><tr><td>3D Viewer URL — View 1</td><td><code>custom.3d_viewer_url_view1</code></td><td>Optional</td><td>A second 3D model URL. Adds a toggle inside the viewer.</td></tr><tr><td>3D Viewer Label — View 1</td><td><code>custom.3d_viewer_label_view1</code></td><td>Optional</td><td>Custom name for View 1, shown on this product only.</td></tr><tr><td>3D Viewer URL — View 2</td><td><code>custom.3d_viewer_url_view2</code></td><td>Optional</td><td>A third 3D model URL. Adds a second toggle inside the viewer.</td></tr><tr><td>3D Viewer Label — View 2</td><td><code>custom.3d_viewer_label_view2</code></td><td>Optional</td><td>Custom name for View 2, shown on this product only.</td></tr></tbody></table>

That covers everything you need to get the 3D viewer live on your store. If you run into anything not covered here or need a hand with your setup, reach out to us at [**hello@hangr.tech**](mailto:hello@hangr.tech) and we will get back to you.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://hangr.gitbook.io/hangr-docs/integrations/shopify-integration.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
