Codrops 원본

From Flat to Spatial: Creating a 3D Product Grid with React Three Fiber

섹션 · MIT

섹션 더 보기
ORIGINAL PREVIEW
From Flat to Spatial: Creating a 3D Product Grid with React Three Fiber 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

29개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

backend/shoes.json
파일 저장

[
  {
    "title": "Nike Dunk Low 'Black White'",
    "price": "$69",
    "image_url": "/shoes/shoe-000.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-black-white-dd1391-100",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#434344"
  },
  {
    "title": "Travis Scott x Playstation x Nike Dunk Low",
    "price": null,
    "image_url": "/shoes/shoe-001.png",
    "product_url": "https://www.goat.com/sneakers/travis-scott-x-playstation-x-dunk-low-travis-ps-dunk",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#5b5451"
  },
  {
    "title": "The Powerpuff Girls x Nike Dunk Low Pro SB QS PS 'Bubbles'",
    "price": "$179",
    "image_url": "/shoes/shoe-002.png",
    "product_url": "https://www.goat.com/sneakers/the-powerpuff-girls-x-dunk-low-pro-sb-qs-ps-bubbles-fz8833-400",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#d3b61b"
  },
  {
    "title": "Concepts x Nike Dunk Low SB 'Orange Lobster'",
    "price": "$307",
    "image_url": "/shoes/shoe-003.png",
    "product_url": "https://www.goat.com/sneakers/concepts-x-dunk-low-sb-orange-lobster-bv1310-orange",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#ec6f3f"
  },
  {
    "title": "Undefeated x Nike Dunk Low 'Dunk vs AF1'",
    "price": "$199",
    "image_url": "/shoes/shoe-004.png",
    "product_url": "https://www.goat.com/sneakers/undefeated-x-dunk-low-dunk-vs-af1-do9329-001",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#2f3c5e"
  },
  {
    "title": "Nike Dunk Low 'Grey Fog'",
    "price": "$99",
    "image_url": "/shoes/shoe-005.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-grey-fog-dd1391-103",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#bcbcbc"
  },
  {
    "title": "Nike Dunk Low LTD 'Wizard'",
    "price": "$82",
    "image_url": "/shoes/shoe-006.png",
    "product_url": "https://www.goat.com/sneakers/nike-dunk-low-ltd-wizard-ib2267-001",
    "brand": "Nike",
    "primary_color": "purple",
    "primary_color_hex": "#372946"
  },
  {
    "title": "Stranger Things x Nike Dunk Low 'Phantom'",
    "price": "$243",
    "image_url": "/shoes/shoe-007.png",
    "product_url": "https://www.goat.com/sneakers/stranger-things-x-dunk-low-phantom-ih6766-001",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#c4a484"
  },
  {
    "title": "Nike Dunk Low SE 'Triple Black'",
    "price": "$116",
    "image_url": "/shoes/shoe-008.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-se-triple-black-ib6651-001",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#707579"
  },
  {
    "title": "Ben & Jerry's x Nike Dunk Low SB 'Chunky Dunky'",
    "price": "$1,188",
    "image_url": "/shoes/shoe-009.png",
    "product_url": "https://www.goat.com/sneakers/ben-jerry-s-x-dunk-low-sb-chunky-dunky-cu3244-100",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#fbb807"
  },
  {
    "title": "Cactus Plant Flea Market x Nike Dunk Low 'Swamp Sponge Pack - Psychic Purple'",
    "price": "$227",
    "image_url": "/shoes/shoe-010.png",
    "product_url": "https://www.goat.com/sneakers/cactus-plant-flea-market-x-dunk-low-swamp-sponge-pack-psychic-purple-ih5094-500",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#f4a105"
  },
  {
    "title": "Nike Dunk Low 'Frankenstein'",
    "price": "$109",
    "image_url": "/shoes/shoe-011.png",
    "product_url": "https://www.goat.com/sneakers/nike-dunk-low-frankenstein-hv4452-300",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#5b4f30"
  },
  {
    "title": "Nike Dunk Low Pro SB 'Freddy Krueger'",
    "price": "$25,059",
    "image_url": "/shoes/shoe-012.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-pro-sb-freddy-krueger-313170-202",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#dcccae"
  },
  {
    "title": "League of Legends x Nike Dunk Low",
    "price": "$159",
    "image_url": "/shoes/shoe-013.png",
    "product_url": "https://www.goat.com/sneakers/league-of-legends-x-dunk-low-black-do2327-011",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#acb8af"
  },
  {
    "title": "Nike Dunk Low Premium 'Setsubun'",
    "price": "$98",
    "image_url": "/shoes/shoe-014.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-premium-setsubun-dq5009-268",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#9c8566"
  },
  {
    "title": "Futura Laboratories x Nike Dunk Low SB 'Bleached Aqua'",
    "price": "$269",
    "image_url": "/shoes/shoe-015.png",
    "product_url": "https://www.goat.com/sneakers/futura-laboratories-x-dunk-low-sb-bleached-aqua-hf6061-400",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#bcbcc3"
  },
  {
    "title": "Nike Dunk Low 'Athletic Department - Deep Jungle'",
    "price": "$66",
    "image_url": "/shoes/shoe-016.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-athletic-department-deep-jungle-fq8080-133",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#384243"
  },
  {
    "title": "Nike Dunk Low 'Midnight Navy Smoke Grey'",
    "price": "$103",
    "image_url": "/shoes/shoe-017.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-midnight-navy-smoke-grey-fd9749-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#31405e"
  },
  {
    "title": "Nike Dunk Low GS 'Year of the Dragon'",
    "price": "$99",
    "image_url": "/shoes/shoe-018.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-gs-year-of-the-dragon-fz5528-101",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#c93d3b"
  },
  {
    "title": "Nike Dunk Low 'Silver Surfer' 2024",
    "price": "$39",
    "image_url": "/shoes/shoe-019.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-silver-surfer-2024-hf0391-001",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#808388"
  },
  {
    "title": "Nike Dunk Low 'Year of the Snake'",
    "price": "$143",
    "image_url": "/shoes/shoe-020.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-year-of-the-snake-hv5980-231",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#797d6e"
  },
  {
    "title": "Nike Dunk Low 'Reverse Panda'",
    "price": "$109",
    "image_url": "/shoes/shoe-021.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-reverse-panda-dj6188-101",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#8a8888"
  },
  {
    "title": "Off-White x Nike Rubber Dunk TD 'Green Strike'",
    "price": "$145",
    "image_url": "/shoes/shoe-022.png",
    "product_url": "https://www.goat.com/sneakers/off-white-x-rubber-dunk-td-green-strike-ow-dunk-td-grn",
    "brand": "Nike",
    "primary_color": "green",
    "primary_color_hex": "#7ae27e"
  },
  {
    "title": "Nike Dunk Low Next Nature 'Baroque Brown'",
    "price": "$138",
    "image_url": "/shoes/shoe-023.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-next-nature-cacao-wow-hf4292-200",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#d4ccbb"
  },
  {
    "title": "The Powerpuff Girls x Nike Dunk Low Pro SB QS 'Buttercup'",
    "price": "$274",
    "image_url": "/shoes/shoe-024.png",
    "product_url": "https://www.goat.com/sneakers/the-powerpuff-girls-x-dunk-low-pro-sb-qs-buttercup-fz8319-300",
    "brand": "Nike",
    "primary_color": "green",
    "primary_color_hex": "#6fa94b"
  },
  {
    "title": "Nike Dunk Low ‘What the Duck - University of Oregon Alternate’ PE",
    "price": "$300",
    "image_url": "/shoes/shoe-025.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-what-the-phk-university-of-oregon-pe-hv1470-001",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#3a3c42"
  },
  {
    "title": "Cactus Plant Flea Market x Nike Dunk Low 'Swamp Sponge Pack - Photo Blue'",
    "price": "$249",
    "image_url": "/shoes/shoe-026.png",
    "product_url": "https://www.goat.com/sneakers/cactus-plant-flea-market-x-dunk-low-swamp-sponge-pack-photo-blue-ih5094-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#0472be"
  },
  {
    "title": "Verdy x Nike Dunk Low SB 'Visty'",
    "price": "$239",
    "image_url": "/shoes/shoe-027.png",
    "product_url": "https://www.goat.com/sneakers/verdy-x-dunk-low-sb-visty-fn6040-400",
    "brand": "Nike",
    "primary_color": "green",
    "primary_color_hex": "#bad1c5"
  },
  {
    "title": "Nike Dunk Low 'Kentucky' 2025",
    "price": "$105",
    "image_url": "/shoes/shoe-028.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-kentucky-2025-hf5441-112",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#043182"
  },
  {
    "title": "Union LA x Nike Dunk Low 'Passport Pack - Argon'",
    "price": "$165",
    "image_url": "/shoes/shoe-029.png",
    "product_url": "https://www.goat.com/sneakers/union-la-x-dunk-low-dj9649-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#6e829a"
  },
  {
    "title": "Nike Dunk Low 'Valerian Blue'",
    "price": "$79",
    "image_url": "/shoes/shoe-030.png",
    "product_url": "https://www.goat.com/sneakers/dunk-low-usa-dd1391-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#2c3d4c"
  },
  {
    "title": "Nike Wmns Dunk Low 'Cacao Wow'",
    "price": "$86",
    "image_url": "/shoes/shoe-031.png",
    "product_url": "https://www.goat.com/sneakers/wmns-dunk-low-cacao-wow-dd1503-124",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#d4d4bc"
  },
  {
    "title": "Limosine Skateboards x Nike Dunk Low SB 'Football'",
    "price": "$87",
    "image_url": "/shoes/shoe-032.png",
    "product_url": "https://www.goat.com/sneakers/limosine-skateboards-x-dunk-low-sb-basketball-leather-hj4131-200",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#371718"
  },
  {
    "title": "Supreme x Nike Dunk Low SB 'Ocean Fog'",
    "price": "$316",
    "image_url": "/shoes/shoe-033.png",
    "product_url": "https://www.goat.com/sneakers/supreme-x-dunk-low-sb-ocean-fog-hq8487-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#3c4c58"
  },
  {
    "title": "Nike Dunk Low SB 'Sandy Bodecker'",
    "price": "$142",
    "image_url": "/shoes/shoe-034.png",
    "product_url": "https://www.goat.com/sneakers/ebay-x-dunk-low-sb-sandy-bodecker-fd8777-100",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#e6b211"
  },
  {
    "title": "Jarritos x Nike Dunk Low SB",
    "price": "$404",
    "image_url": "/shoes/shoe-035.png",
    "product_url": "https://www.goat.com/sneakers/jarritos-x-dunk-low-sb-fd0860-001",
    "brand": "Nike",
    "primary_color": "teal",
    "primary_color_hex": "#1f735d"
  },
  {
    "title": "Undefeated x Air Jordan 4 Retro 2025",
    "price": "$262",
    "image_url": "/shoes/shoe-036.png",
    "product_url": "https://www.goat.com/sneakers/undefeated-x-air-jordan-4-retro-ib1519-200",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#838367"
  },
  {
    "title": "Air Jordan 4 Retro 'Rare Air - White Lettering'",
    "price": "$193",
    "image_url": "/shoes/shoe-037.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-rare-air-fv5029-003",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#19346f"
  },
  {
    "title": "Wmns Air Jordan 4 Retro TEX 'Worn Blue Denim'",
    "price": "$153",
    "image_url": "/shoes/shoe-038.png",
    "product_url": "https://www.goat.com/sneakers/wmns-air-jordan-4-retro-tex-worn-blue-denim-ib6716-100",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#315472"
  },
  {
    "title": "Air Jordan 4 Retro 'Black Cat' 2025",
    "price": "$277",
    "image_url": "/shoes/shoe-039.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-black-cat-2025-fv5029-010",
    "brand": "Nike",
    "primary_color": "black",
    "primary_color_hex": "#0c040b"
  },
  {
    "title": "Air Jordan 4 Retro OG 'White Cement' 2025",
    "price": "$232",
    "image_url": "/shoes/shoe-040.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-og-white-cement-2025-fv5029-100",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#8a898b"
  },
  {
    "title": "Nike SB x Air Jordan 4 Retro SP 'Navy'",
    "price": "$166",
    "image_url": "/shoes/shoe-041.png",
    "product_url": "https://www.goat.com/sneakers/nike-sb-x-air-jordan-4-retro-sp-navy-dr5415-100",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#e3a97b"
  },
  {
    "title": "Air Jordan 4 Retro 'Cave Stone'",
    "price": "$179",
    "image_url": "/shoes/shoe-042.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-cave-stone-fq8138-200",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#4c443f"
  },
  {
    "title": "Nigel Sylvester x Air Jordan 4 Retro OG SP 'Brick By Brick'",
    "price": "$304",
    "image_url": "/shoes/shoe-043.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-og-sp-firewood-orange-hf4340-800",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#c53f3b"
  },
  {
    "title": "Jan 17",
    "price": "$243",
    "image_url": "/shoes/shoe-044.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-flight-club-im4002-100",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#858483"
  },
  {
    "title": "Air Jordan 4 Retro 'White Thunder'",
    "price": "$294",
    "image_url": "/shoes/shoe-045.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-white-thunder-fq8138-001",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#414143"
  },
  {
    "title": "Air Jordan 4 Retro 'Bred Reimagined'",
    "price": "$244",
    "image_url": "/shoes/shoe-046.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-bred-reimagined-fv5029-006",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#b4b3bb"
  },
  {
    "title": "Air Jordan 4 Retro 'Military Blue' 2024",
    "price": "$214",
    "image_url": "/shoes/shoe-047.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-military-blue-2024-fv5029-141",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#bcbebf"
  },
  {
    "title": "Air Jordan 4 Retro GS 'Black Cat' 2025",
    "price": "$158",
    "image_url": "/shoes/shoe-048.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-jordan-4-retro-gs-black-cat-2025-ib4171-010",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#444c4d"
  },
  {
    "title": "A Ma Maniére x Air Jordan 4 Retro 'Dark Mocha'",
    "price": "$269",
    "image_url": "/shoes/shoe-049.png",
    "product_url": "https://www.goat.com/sneakers/a-ma-maniere-x-air-jordan-4-retro-dark-mocha-if3102-200",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#4c392f"
  },
  {
    "title": "Air Jordan 4 Retro 'Red Thunder'",
    "price": "$401",
    "image_url": "/shoes/shoe-050.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-red-thunder-ct8527-016",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#e2262a"
  },
  {
    "title": "Nike SB x Air Jordan 4 Retro SP 'Pine Green'",
    "price": "$319",
    "image_url": "/shoes/shoe-051.png",
    "product_url": "https://www.goat.com/sneakers/nike-sb-x-air-jordan-4-retro-pine-green-dr5415-103",
    "brand": "Nike",
    "primary_color": "teal",
    "primary_color_hex": "#07845c"
  },
  {
    "title": "Air Jordan 4 Retro SE 'Wet Cement'",
    "price": "$298",
    "image_url": "/shoes/shoe-052.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-se-smoke-grey-fq7928-001",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#b2b3b7"
  },
  {
    "title": "Air Jordan 4 Retro OG 'Bred' 2019",
    "price": "$389",
    "image_url": "/shoes/shoe-053.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-bred-2019-308497-060",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#a95c60"
  },
  {
    "title": "A Ma Maniére x Wmns Air Jordan 4 Retro 'While You Were Sleeping'",
    "price": "$155",
    "image_url": "/shoes/shoe-054.png",
    "product_url": "https://www.goat.com/sneakers/a-ma-maniere-x-wmns-air-jordan-4-retro-fossil-stone-fz4810-200",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#cdb9ac"
  },
  {
    "title": "Air Jordan 4 Retro 'Fear' 2024",
    "price": "$226",
    "image_url": "/shoes/shoe-055.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-fear-2024-fq8138-002",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#7b7a7a"
  },
  {
    "title": "Air Jordan 4 Retro OG 'Fire Red' 2020",
    "price": "$311",
    "image_url": "/shoes/shoe-056.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-og-fire-red-2020-dc7770-160",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#b91817"
  },
  {
    "title": "Wmns Air Jordan 4 Retro 'Orchid'",
    "price": "$307",
    "image_url": "/shoes/shoe-057.png",
    "product_url": "https://www.goat.com/sneakers/wmns-air-jordan-4-retro-orchid-aq9129-501",
    "brand": "Nike",
    "primary_color": "pink",
    "primary_color_hex": "#cca5bc"
  },
  {
    "title": "Air Jordan 4 Retro 'Thunder' 2023",
    "price": "$291",
    "image_url": "/shoes/shoe-058.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-thunder-2023-dh6927-017",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#494233"
  },
  {
    "title": "Air Jordan 4 Retro SE 'Black Canvas'",
    "price": "$333",
    "image_url": "/shoes/shoe-059.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-black-canvas-dh7138-006",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#bdc1c4"
  },
  {
    "title": "Off-White x Wmns Air Jordan 4 Retro SP 'Sail'",
    "price": "$743",
    "image_url": "/shoes/shoe-060.png",
    "product_url": "https://www.goat.com/sneakers/off-white-x-wmns-air-jordan-4-sp-sail-cv9388-100",
    "brand": "Nike",
    "primary_color": "orange",
    "primary_color_hex": "#ccc4b4"
  },
  {
    "title": "Air Jordan 4 Retro 'Toro Bravo' 2026",
    "price": null,
    "image_url": "/shoes/shoe-061.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-toro-bravo-2026-fq8138-600",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#a41d2b"
  },
  {
    "title": "Air Jordan 4 Retro 'University Blue'",
    "price": "$390",
    "image_url": "/shoes/shoe-062.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-university-blue-ct8527-400",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#557fa9"
  },
  {
    "title": "Air Jordan 4 Retro 'Military Black'",
    "price": "$335",
    "image_url": "/shoes/shoe-063.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-military-black-dh6927-111",
    "brand": "Nike",
    "primary_color": "black",
    "primary_color_hex": "#2e2c2c"
  },
  {
    "title": "Air Jordan 4 Retro 'Lightning' 2021",
    "price": "$250",
    "image_url": "/shoes/shoe-064.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-lightning-2021-ct8527-700",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#e0b92e"
  },
  {
    "title": "Air Jordan 4 Retro 'Red Cement'",
    "price": "$218",
    "image_url": "/shoes/shoe-065.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-red-cement-dh6927-161",
    "brand": "Nike",
    "primary_color": "red",
    "primary_color_hex": "#af2530"
  },
  {
    "title": "Air Jordan 4 Retro 'Midnight Navy'",
    "price": "$281",
    "image_url": "/shoes/shoe-066.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-midnight-navy-dh6927-140",
    "brand": "Nike",
    "primary_color": "blue",
    "primary_color_hex": "#2e354c"
  },
  {
    "title": "Air Jordan 4 Retro '25th Silver Anniversary'",
    "price": "$135",
    "image_url": "/shoes/shoe-067.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-silver-anniversary-408202-101",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#c5c2ca"
  },
  {
    "title": "A Ma Maniére x Air Jordan 4 Retro 'Violet Ore'",
    "price": "$198",
    "image_url": "/shoes/shoe-068.png",
    "product_url": "https://www.goat.com/sneakers/a-ma-maniere-x-air-jordan-4-retro-violet-ore-dv6773-220",
    "brand": "Nike",
    "primary_color": "yellow",
    "primary_color_hex": "#dbd1b3"
  },
  {
    "title": "Air Jordan 4 Retro 'White Oreo'",
    "price": "$382",
    "image_url": "/shoes/shoe-069.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-oreo-ct8527-100",
    "brand": "Nike",
    "primary_color": "dark_gray",
    "primary_color_hex": "#494948"
  },
  {
    "title": "Air Jordan 4 Retro 'Taupe Haze'",
    "price": "$352",
    "image_url": "/shoes/shoe-070.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-taupe-haze-db0732-200",
    "brand": "Nike",
    "primary_color": "gray",
    "primary_color_hex": "#83776e"
  },
  {
    "title": "Air Jordan 4 Retro OG GS 'White Cement' 2025",
    "price": "$149",
    "image_url": "/shoes/shoe-071.png",
    "product_url": "https://www.goat.com/sneakers/air-jordan-4-retro-og-gs-white-cement-2025-ib4171-100",
    "brand": "Nike",
    "primary_color": "light_gray",
    "primary_color_hex": "#c8cccc"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Rain Cloud'",
    "price": "$118",
    "image_url": "/shoes/shoe-072.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-rain-cloud-m2002rda",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#c7baa4"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Phantom'",
    "price": "$138",
    "image_url": "/shoes/shoe-073.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-phantom-m2002rdb",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#434648"
  },
  {
    "title": "New Balance 9060 'Black Cat'",
    "price": "$165",
    "image_url": "/shoes/shoe-074.png",
    "product_url": "https://www.goat.com/sneakers/9060-black-grey-u9060zge",
    "brand": "New Balance",
    "primary_color": "blue",
    "primary_color_hex": "#343c44"
  },
  {
    "title": "New Balance 1906R 'Silver Metallic Black'",
    "price": "$105",
    "image_url": "/shoes/shoe-075.png",
    "product_url": "https://www.goat.com/sneakers/1906r-silver-metallic-black-m1906rer",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#7b7978"
  },
  {
    "title": "New Balance 740v2 'Black Silver Metallic'",
    "price": "$99",
    "image_url": "/shoes/shoe-076.png",
    "product_url": "https://www.goat.com/sneakers/740-black-silver-u740bm2",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#3b3c44"
  },
  {
    "title": "New Balance 9060 'Suede Pack - Sea Salt'",
    "price": "$145",
    "image_url": "/shoes/shoe-077.png",
    "product_url": "https://www.goat.com/sneakers/9060-white-grey-u9060hsc",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#82807b"
  },
  {
    "title": "New Balance 1906R 'Silver Metallic Cream'",
    "price": "$75",
    "image_url": "/shoes/shoe-078.png",
    "product_url": "https://www.goat.com/sneakers/1906r-silver-metallic-cream-m1906ree",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#8a8983"
  },
  {
    "title": "New Balance 1906A 'Black'",
    "price": "$184",
    "image_url": "/shoes/shoe-079.png",
    "product_url": "https://www.goat.com/sneakers/1906-black-dark-silver-metallic-m1906af",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#424243"
  },
  {
    "title": "New Balance 850 'Grey'",
    "price": "$99",
    "image_url": "/shoes/shoe-080.png",
    "product_url": "https://www.goat.com/sneakers/850-grey-ml850cf",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#bdbaba"
  },
  {
    "title": "New Balance 9060 'Triple Black Suede'",
    "price": "$125",
    "image_url": "/shoes/shoe-081.png",
    "product_url": "https://www.goat.com/sneakers/9060-triple-black-u9060bpm",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#3e3f3f"
  },
  {
    "title": "New Balance 530 'White Natural Indigo'",
    "price": "$84",
    "image_url": "/shoes/shoe-082.png",
    "product_url": "https://www.goat.com/sneakers/mr530sg-white-mr530sg",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#403e41"
  },
  {
    "title": "New Balance 1906R 'Metallic Silver Metallic Gold'",
    "price": "$79",
    "image_url": "/shoes/shoe-083.png",
    "product_url": "https://www.goat.com/sneakers/1906r-white-gold-m1906ra",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#bcbcbe"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Sea Salt'",
    "price": "$125",
    "image_url": "/shoes/shoe-084.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-sea-salt-m2002rdc",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#c2baa7"
  },
  {
    "title": "New Balance 1906R 'Pink Taffy'",
    "price": "$79",
    "image_url": "/shoes/shoe-085.png",
    "product_url": "https://www.goat.com/sneakers/1906r-pink-taffy-u1906rcu",
    "brand": "New Balance",
    "primary_color": "red",
    "primary_color_hex": "#925e6a"
  },
  {
    "title": "New Balance 9060 'Black Castlerock'",
    "price": "$118",
    "image_url": "/shoes/shoe-086.png",
    "product_url": "https://www.goat.com/sneakers/9060-black-castlerock-u9060blk",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#434444"
  },
  {
    "title": "New Balance 1000 'Black Cat'",
    "price": "$133",
    "image_url": "/shoes/shoe-087.png",
    "product_url": "https://www.goat.com/sneakers/1000-triple-black-m1000la",
    "brand": "New Balance",
    "primary_color": "black",
    "primary_color_hex": "#04040c"
  },
  {
    "title": "New Balance 9060 'Arctic Grey'",
    "price": "$157",
    "image_url": "/shoes/shoe-088.png",
    "product_url": "https://www.goat.com/sneakers/9060-washed-blue-u9060ib",
    "brand": "New Balance",
    "primary_color": "blue",
    "primary_color_hex": "#747e91"
  },
  {
    "title": "New Balance 1906R 'Silver Metallic Deep Ocean'",
    "price": "$59",
    "image_url": "/shoes/shoe-089.png",
    "product_url": "https://www.goat.com/sneakers/1906r-silver-metallic-deep-ocean-u1906rce",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#c1c7c8"
  },
  {
    "title": "New Balance BAPE x 2002R 'Apes Together Strong - Black Camo'",
    "price": "$431",
    "image_url": "/shoes/shoe-090.png",
    "product_url": "https://www.goat.com/sneakers/bape-x-2002r-apes-together-strong-camo-m2002r-bape-camo",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#7d7c74"
  },
  {
    "title": "New Balance 2002R 'Black Cat'",
    "price": "$134",
    "image_url": "/shoes/shoe-091.png",
    "product_url": "https://www.goat.com/sneakers/2002r-triple-black-suede-u2002rbl",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#414141"
  },
  {
    "title": "New Balance 1906L 'Silver Shadow Grey'",
    "price": "$136",
    "image_url": "/shoes/shoe-092.png",
    "product_url": "https://www.goat.com/sneakers/1906l-silver-shadow-grey-u1906lae",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#868280"
  },
  {
    "title": "New Balance 1906D 'Protection Pack - Triple Black'",
    "price": "$119",
    "image_url": "/shoes/shoe-093.png",
    "product_url": "https://www.goat.com/sneakers/1906d-protection-pack-triple-black-m1906df",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#3c3c3c"
  },
  {
    "title": "New Balance 1000 'Mallard Green Sea Salt'",
    "price": "$133",
    "image_url": "/shoes/shoe-094.png",
    "product_url": "https://www.goat.com/sneakers/1000-green-grey-m1000ma",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#7c8e7c"
  },
  {
    "title": "New Balance 550 'White Timberwolf'",
    "price": "$73",
    "image_url": "/shoes/shoe-095.png",
    "product_url": "https://www.goat.com/sneakers/550-white-timberwolf-bb550pwg",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#ccbda2"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Lunar New Year'",
    "price": "$176",
    "image_url": "/shoes/shoe-096.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-lunar-new-year-m2002rdy",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#888382"
  },
  {
    "title": "New Balance 2002R 'Black Gunmetal'",
    "price": "$109",
    "image_url": "/shoes/shoe-097.png",
    "product_url": "https://www.goat.com/sneakers/2002r-black-gunmetal-m2002rbk",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#434040"
  },
  {
    "title": "New Balance 530 'Raincloud'",
    "price": "$95",
    "image_url": "/shoes/shoe-098.png",
    "product_url": "https://www.goat.com/sneakers/530-raincloud-mr530ck",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#bbbcba"
  },
  {
    "title": "New Balance 1906A 'Black Dragon Berry'",
    "price": "$133",
    "image_url": "/shoes/shoe-099.png",
    "product_url": "https://www.goat.com/sneakers/1906a-black-pink-u1906ad",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#493c43"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Eclipse'",
    "price": "$99",
    "image_url": "/shoes/shoe-100.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-eclipse-m2002rdo",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#8f8c84"
  },
  {
    "title": "New Balance 1906A 'Silver Gold Metallic'",
    "price": "$149",
    "image_url": "/shoes/shoe-101.png",
    "product_url": "https://www.goat.com/sneakers/1906-silver-gold-metallic-m1906ad",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#cdc2b1"
  },
  {
    "title": "New Balance 1906D 'Protection Pack - White Turtledove'",
    "price": "$183",
    "image_url": "/shoes/shoe-102.png",
    "product_url": "https://www.goat.com/sneakers/1906d-protection-pack-triple-white-m1906de",
    "brand": "New Balance",
    "primary_color": "yellow",
    "primary_color_hex": "#c7c4b4"
  },
  {
    "title": "New Balance 2002R 'Protection Pack - Brown' size? Exclusive",
    "price": "$207",
    "image_url": "/shoes/shoe-103.png",
    "product_url": "https://www.goat.com/sneakers/2002r-protection-pack-brown-size-exclusive-m2002rd6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#957b6a"
  },
  {
    "title": "New Balance 9060 'Quartz Grey'",
    "price": "$139",
    "image_url": "/shoes/shoe-104.png",
    "product_url": "https://www.goat.com/sneakers/9060-quartz-grey-u9060hsa",
    "brand": "New Balance",
    "primary_color": "yellow",
    "primary_color_hex": "#dcd7c6"
  },
  {
    "title": "New Balance 1906R 'Black Silver'",
    "price": "$173",
    "image_url": "/shoes/shoe-105.png",
    "product_url": "https://www.goat.com/sneakers/1906r-primaloft-black-silver-u1906ros",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#3c4142"
  },
  {
    "title": "New Balance 740v2 'White Navy Shadow Grey'",
    "price": "$102",
    "image_url": "/shoes/shoe-106.png",
    "product_url": "https://www.goat.com/sneakers/740v2-navy-white-shadow-grey-u740wn2",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#c1c1c1"
  },
  {
    "title": "New Balance 9060 'Pink Overdye' ASOS Exclusive",
    "price": "$197",
    "image_url": "/shoes/shoe-107.png",
    "product_url": "https://www.goat.com/sneakers/9060-pink-overdye-asos-exclusive-u9060app",
    "brand": "New Balance",
    "primary_color": "pink",
    "primary_color_hex": "#d19eb3"
  },
  {
    "title": "New Balance 990v4 Made in USA 'Grey Silver'",
    "price": "$123",
    "image_url": "/shoes/shoe-108.png",
    "product_url": "https://www.goat.com/sneakers/990v4-made-in-usa-grey-silver-u990gr4",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#807f80"
  },
  {
    "title": "New Balance 990v6 Made in USA 'Castlerock'",
    "price": "$137",
    "image_url": "/shoes/shoe-109.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-castlerock-m990gl6",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#bcbcbd"
  },
  {
    "title": "New Balance 990v3 Made in USA 'Grey'",
    "price": "$208",
    "image_url": "/shoes/shoe-110.png",
    "product_url": "https://www.goat.com/sneakers/990v3-made-in-usa-grey-m990gy3",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#828485"
  },
  {
    "title": "Teddy Santis x New Balance 990v4 Made in USA 'Arctic Grey Black'",
    "price": "$179",
    "image_url": "/shoes/shoe-111.png",
    "product_url": "https://www.goat.com/sneakers/990v4-made-in-usa-arctic-grey-black-u990bb4",
    "brand": "New Balance",
    "primary_color": "blue",
    "primary_color_hex": "#738290"
  },
  {
    "title": "New Balance 990v3 Made In USA 'Black'",
    "price": "$159",
    "image_url": "/shoes/shoe-112.png",
    "product_url": "https://www.goat.com/sneakers/990v3-made-in-usa-black-m990bs3",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#444343"
  },
  {
    "title": "Bodega x New Balance 990v3 Made In USA 'Anniversary'",
    "price": "$175",
    "image_url": "/shoes/shoe-113.png",
    "product_url": "https://www.goat.com/sneakers/bodega-x-990v3-made-in-usa-15th-anniversary-bodega-990v3",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#cbc5bd"
  },
  {
    "title": "Action Bronson x New Balance 990v6 Made in USA 'Amazõnia'",
    "price": "$175",
    "image_url": "/shoes/shoe-114.png",
    "product_url": "https://www.goat.com/sneakers/action-bronson-x-990v6-made-in-usa-amazonia-00011-10000abx9mi",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#bcb4a5"
  },
  {
    "title": "Action Bronson x New Balance 990v6 Made in USA 'Lapis Lazuli'",
    "price": "$173",
    "image_url": "/shoes/shoe-115.png",
    "product_url": "https://www.goat.com/sneakers/action-bronson-x-990v6-made-in-usa-lapis-lazuli-m990ac6",
    "brand": "New Balance",
    "primary_color": "blue",
    "primary_color_hex": "#687eb2"
  },
  {
    "title": "New Balance 990v4 Made In USA 'Red Label - Grey'",
    "price": "$268",
    "image_url": "/shoes/shoe-116.png",
    "product_url": "https://www.goat.com/sneakers/990v4-made-in-usa-red-label-grey-m990vs4",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#b6b8bc"
  },
  {
    "title": "New Balance 990v4 Made in USA 'Black Silver' 2023",
    "price": "$154",
    "image_url": "/shoes/shoe-117.png",
    "product_url": "https://www.goat.com/sneakers/990v4-made-in-usa-black-silver-2023-u990bl4",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#474646"
  },
  {
    "title": "Action Bronson x New Balance 990v6 Made in USA 'Untitled'",
    "price": "$229",
    "image_url": "/shoes/shoe-118.png",
    "product_url": "https://www.goat.com/sneakers/action-bronson-x-990v6-made-in-usa-multi-color-u990at6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#d4c49e"
  },
  {
    "title": "New Balance Teddy Santis x 990v4 Made in USA 'Grey Black'",
    "price": "$132",
    "image_url": "/shoes/shoe-119.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v4-made-in-usa-grey-cream-u990tg4",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#7f7c7a"
  },
  {
    "title": "Teddy Santis x New Balance 990v3 Made in USA 'Tan Orange'",
    "price": "$166",
    "image_url": "/shoes/shoe-120.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v3-made-in-usa-khaki-orange-m990bt3",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#9f855e"
  },
  {
    "title": "Teddy Santis x New Balance 990v4 Made in USA 'Plum Purple'",
    "price": "$147",
    "image_url": "/shoes/shoe-121.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v4-made-in-usa-purple-suede-u990tb4",
    "brand": "New Balance",
    "primary_color": "purple",
    "primary_color_hex": "#3c2c5c"
  },
  {
    "title": "New Balance 990v6 Made in USA 'Workwear'",
    "price": "$69",
    "image_url": "/shoes/shoe-122.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-workwear-grey-u990tn6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#b98651"
  },
  {
    "title": "Teddy Santis x New Balance 990v3 Made in USA 'Green Gold'",
    "price": "$183",
    "image_url": "/shoes/shoe-123.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v3-made-in-usa-green-yellow-m990gg3",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#cfc4a4"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Reflection Marblehead'",
    "price": "$149",
    "image_url": "/shoes/shoe-124.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-reflection-marblehead-u990nc6",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#787573"
  },
  {
    "title": "Action Bronson x New Balance 990v6 Made in USA 'Baklava'",
    "price": "$215",
    "image_url": "/shoes/shoe-125.png",
    "product_url": "https://www.goat.com/sneakers/action-bronson-x-990v6-made-in-usa-baklava-m990ab6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#d0b587"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Paris'",
    "price": "$723",
    "image_url": "/shoes/shoe-126.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v6-made-in-usa-paris-u990pa6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#544639"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Community Pack - Red'",
    "price": "$98",
    "image_url": "/shoes/shoe-127.png",
    "product_url": "https://www.goat.com/sneakers/action-bronson-x-990v6-made-in-usa-community-red-u990rt6",
    "brand": "New Balance",
    "primary_color": "red",
    "primary_color_hex": "#dd817f"
  },
  {
    "title": "JJJJound x New Balance 990v3 Made in USA 'Olive'",
    "price": "$308",
    "image_url": "/shoes/shoe-128.png",
    "product_url": "https://www.goat.com/sneakers/jjjjound-x-990v3-made-in-usa-olive-m990jd3",
    "brand": "New Balance",
    "primary_color": "green",
    "primary_color_hex": "#8c9370"
  },
  {
    "title": "New Balance 990v6 Made in USA 'Triple Black'",
    "price": "$149",
    "image_url": "/shoes/shoe-129.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-triple-black-u990bb6",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#34343c"
  },
  {
    "title": "JJJJound x New Balance 990v3 Made in USA 'Brown'",
    "price": "$284",
    "image_url": "/shoes/shoe-130.png",
    "product_url": "https://www.goat.com/sneakers/jjjjound-x-990v3-made-in-usa-brown-m990jj3",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#544430"
  },
  {
    "title": "New Balance 990v6 Made in USA 'Salmon'",
    "price": "$77",
    "image_url": "/shoes/shoe-131.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-salmon-u990sr6",
    "brand": "New Balance",
    "primary_color": "red",
    "primary_color_hex": "#f88984"
  },
  {
    "title": "New Balance 990v3 Made In USA 'Navy'",
    "price": "$139",
    "image_url": "/shoes/shoe-132.png",
    "product_url": "https://www.goat.com/sneakers/990-made-in-usa-navy-denim-m990nb3",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#767a79"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Light Mushroom Moonrock'",
    "price": "$154",
    "image_url": "/shoes/shoe-133.png",
    "product_url": "https://www.goat.com/sneakers/990v6-made-in-usa-light-mushroom-moonrock-u990mm6",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#99826b"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Community Pack - Navy'",
    "price": "$174",
    "image_url": "/shoes/shoe-134.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v6-made-in-usa-community-pack-vintage-indigo-u990lt6",
    "brand": "New Balance",
    "primary_color": "blue",
    "primary_color_hex": "#5c6f88"
  },
  {
    "title": "Teddy Santis x New Balance 990v3 Made in USA 'Raw Amethyst'",
    "price": "$275",
    "image_url": "/shoes/shoe-135.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v3-made-in-usa-raw-amethyst-m990td3",
    "brand": "New Balance",
    "primary_color": "purple",
    "primary_color_hex": "#bcacc4"
  },
  {
    "title": "Teddy Santis x New Balance 990v3 Made in USA 'Green Purple'",
    "price": "$156",
    "image_url": "/shoes/shoe-136.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v3-made-in-usa-olive-burgundy-m990gp3",
    "brand": "New Balance",
    "primary_color": "red",
    "primary_color_hex": "#5e4a44"
  },
  {
    "title": "New Balance Teddy Santis x 990v3 Made in USA 'Scarlet Marblehead'",
    "price": "$234",
    "image_url": "/shoes/shoe-137.png",
    "product_url": "https://www.goat.com/sneakers/990v3-made-in-usa-scarlet-marblehead-m990tf3",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#7c7978"
  },
  {
    "title": "New Balance 990v5 Made in USA 'Castlerock'",
    "price": "$192",
    "image_url": "/shoes/shoe-138.png",
    "product_url": "https://www.goat.com/sneakers/990v5-grey-m990gl5",
    "brand": "New Balance",
    "primary_color": "light_gray",
    "primary_color_hex": "#c0bfbe"
  },
  {
    "title": "Teddy Santis x New Balance 990v3 Made in USA 'Black Tan'",
    "price": "$135",
    "image_url": "/shoes/shoe-139.png",
    "product_url": "https://www.goat.com/sneakers/990v3-made-in-usa-black-tan-m990bb3",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#524c3c"
  },
  {
    "title": "Kith x New Balance 990v3 Made in USA 'Steel Blue'",
    "price": "$239",
    "image_url": "/shoes/shoe-140.png",
    "product_url": "https://www.goat.com/sneakers/kith-x-990v3-made-in-usa-steel-blue-m990ks3",
    "brand": "New Balance",
    "primary_color": "dark_gray",
    "primary_color_hex": "#443c3c"
  },
  {
    "title": "New Balance 990v4 Made in USA 'Castlerock'",
    "price": "$223",
    "image_url": "/shoes/shoe-141.png",
    "product_url": "https://www.goat.com/sneakers/990v4-made-in-usa-castlerock-m990gl4",
    "brand": "New Balance",
    "primary_color": "gray",
    "primary_color_hex": "#808081"
  },
  {
    "title": "New Balance Aimé Leon Dore x 990v4 Made in USA 'True Camo'",
    "price": "$233",
    "image_url": "/shoes/shoe-142.png",
    "product_url": "https://www.goat.com/sneakers/aime-leon-dore-x-990v4-made-in-usa-true-camo-u990ct4",
    "brand": "New Balance",
    "primary_color": "orange",
    "primary_color_hex": "#50452c"
  },
  {
    "title": "Teddy Santis x New Balance 990v6 Made in USA 'Community Pack - Mint'",
    "price": "$118",
    "image_url": "/shoes/shoe-143.png",
    "product_url": "https://www.goat.com/sneakers/teddy-santis-x-990v6-made-in-usa-clay-ash-u990gt6",
    "brand": "New Balance",
    "primary_color": "teal",
    "primary_color_hex": "#245254"
  }
]
eslint.config.mjs
파일 저장

import { defineConfig, globalIgnores } from "eslint/config";
import nextVitals from "eslint-config-next/core-web-vitals";

const eslintConfig = defineConfig([
  ...nextVitals,
  // Override default ignores of eslint-config-next.
  globalIgnores([
    // Default ignores of eslint-config-next:
    ".next/**",
    "out/**",
    "build/**",
    "next-env.d.ts",
  ]),
]);

export default eslintConfig;
함께 쓰는 파일 27개 보기
next.config.mjs
파일 저장

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: false,
  devIndicators: false,
  webpack: (config) => {
    config.module.rules.push({
      test: /\.(glsl|vs|fs|vert|frag)$/,
      use: ['raw-loader', 'glslify-loader'],
    });
    return config;
  },
};

export default nextConfig;
postcss.config.mjs
파일 저장

const config = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

export default config;
src/components/CloseButton.jsx
파일 저장

import React, { useEffect, useState, useRef } from "react";
import { Html } from "@react-three/drei";
import { useThree } from "@react-three/fiber";

export function CloseButton({
  isActive,
  position,
  onClose,
}) {
  const { gl } = useThree();
  const [shouldShow, setShouldShow] = useState(false);
  const timerRef = useRef(null);
  const canvasRef = useRef(null);

  useEffect(() => {
    canvasRef.current = gl.domElement;
  }, [gl]);

  // 250ms delay before showing
  useEffect(() => {
    // Clear any existing timer
    if (timerRef.current) {
      clearTimeout(timerRef.current);
      timerRef.current = null;
    }

    if (!isActive) {
      // Use setTimeout(0) to avoid synchronous setState
      timerRef.current = setTimeout(() => {
        setShouldShow(false);
      }, 0);
      return () => {
        if (timerRef.current)
          clearTimeout(timerRef.current);
      };
    }

    timerRef.current = setTimeout(() => {
      setShouldShow(true);
    }, 250);

    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
        timerRef.current = null;
      }
    };
  }, [isActive]);

  if (!isActive) return null;

  // Convert 3D position to screen coordinates for Html
  // Html centers by default, so we need to adjust
  const [x, y, z] = position;

  return (
    <Html
      position={[x, y, z]}
      center
      style={{
        pointerEvents: "auto",
        transform: "translate(-50%, -150%)",
      }}
      occlude
    >
      <style>
        {`
          @keyframes breathe {
            0%, 100% { transform: scale(1); }
            50% { transform: scale(1.015); }
          }
        `}
      </style>
      <button
        onClick={(e) => {
          e.stopPropagation();
          onClose();
        }}
        onMouseEnter={() => {
          if (canvasRef.current) {
            canvasRef.current.style.cursor = "pointer";
          }
        }}
        onMouseLeave={() => {
          if (canvasRef.current) {
            canvasRef.current.style.cursor = "grab";
          }
        }}
        style={{
          width: "32px",
          height: "32px",
          border: "1px solid rgba(0, 0, 0, 1.2)",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          cursor: "pointer",
          padding: 0,
          opacity: shouldShow ? 0.6 : 0,
          animation: shouldShow
            ? "breathe 3.14s ease-in-out infinite"
            : "none",
          transform: shouldShow ? "scale(1)" : "scale(0.8)",
          transition: "opacity 0.2s ease",
          background: "transparent",
        }}
        onMouseOver={(e) => {
          e.currentTarget.style.opacity = "0.8";
        }}
        onMouseOut={(e) => {
          if (shouldShow) {
            e.currentTarget.style.opacity = "0.6";
          }
        }}
      >
        <svg
          width="14"
          height="14"
          viewBox="0 0 24 24"
          fill="none"
          stroke="rgba(0, 0, 0, 0.8)"
          strokeWidth="2"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <line x1="18" y1="6" x2="6" y2="18" />
          <line x1="6" y1="6" x2="18" y2="18" />
        </svg>
      </button>
    </Html>
  );
}
src/components/GridUI.jsx
파일 저장

import React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { CONFIG } from "./grid/gridConfig";
// 1. THE PHYSICS
// High stiffness, moderate damping = "Snappy but smooth" (Apple feel)
const islandTransition = {
  type: "spring",
  stiffness: 500,
  damping: 30,
  mass: 1,
};

export function UnifiedControlBar({
  currentCollection,
  onSwitch,
  setZoomTrigger,
  isZoomedIn,
  hasActiveSelection,
  nikeFilter,
  onFilterChange,
}) {
  const collections = [
    "Nike",
    "New Balance",
    "Under $150",
  ];

  const nikeFilters = [
    { id: "all", label: "All" },
    { id: "jordan", label: "Jordan" },
    { id: "dunk", label: "Dunk" },
  ];

  return (
    <div
      className="control-bar-container"
      style={{
        position: "fixed",
        bottom: "40px",
        left: "0",
        right: "0",
        display: "flex",
        justifyContent: "center",
        alignItems: "flex-end",
        zIndex: 100,
        pointerEvents: "none",
      }}
    >
      {/* 
        THE ISLAND CONTAINER
        - layout: Animates width/height changes
        - borderRadius: 32px is standard for pill shapes
      */}
      <motion.div
        className="control-bar-island"
        layout
        transition={islandTransition}
        style={{
          background:
            "linear-gradient(135deg, rgba(255, 240, 235, 0.4) 0%, rgba(255, 255, 255, 0.3) 50%, rgba(245, 235, 255, 0.4) 100%)", // Very subtle orange to purple gradient for glassy depth
          backdropFilter: "blur(40px) saturate(200%)", // Increased blur for more glassy feel
          WebkitBackdropFilter: "blur(40px) saturate(200%)", // Safari support
          borderRadius: "32px",
          border: "1px solid rgba(255, 255, 255, 0.3)", // Subtle white border for glass edge
          boxShadow:
            "0 8px 32px rgba(0, 0, 0, 0.1), inset 0 1px 0 rgba(255, 255, 255, 0.5)", // Soft shadow + inner highlight
          padding: "6px",
          display: "flex",
          alignItems: "center",
          pointerEvents: "auto",
          height: "56px",
          overflow: "hidden", // Crucial for clipping content during resize
        }}
      >
        {/* 
          mode="popLayout" is CRITICAL. 
          It lets the outgoing component "pop" out of the layout flow 
          so the container can instantly shrink/grow to the new content's size.
        */}
        <AnimatePresence mode="popLayout" initial={false}>
          {hasActiveSelection ? (
            /* ---------------- STATE 1: BUY NOW (Active Selection) ---------------- */
            <motion.button
              key="buy-now-mode"
              initial={{
                opacity: 0,
                scale: 0.9,
                filter: "blur(4px)",
              }}
              animate={{
                opacity: 1,
                scale: 1,
                filter: "blur(0px)",
              }}
              exit={{
                opacity: 0,
                scale: 0.9,
                filter: "blur(4px)",
              }}
              transition={{
                ...islandTransition,
                opacity: { duration: 0.2 },
              }}
              onClick={() => {
                // Handle buy now action
              }}
              style={{
                background: "#000",
                color: "#fff",
                border: "none",
                borderRadius: "24px",
                padding: "0 24px",
                height: "44px",
                fontSize: "14px",
                fontWeight: "600",
                cursor: "pointer",
                display: "flex",
                alignItems: "center",
                whiteSpace: "nowrap",
              }}
              whileTap={{ scale: 0.95 }}
            >
              Buy Now
            </motion.button>
          ) : isZoomedIn ? (
            /* ---------------- STATE 2: COMPACT (Zoomed In) ---------------- */
            <motion.div
              key="compact-mode"
              initial={{
                opacity: 0,
                scale: 0.5,
                filter: "blur(4px)",
              }}
              animate={{
                opacity: 1,
                scale: 1,
                filter: "blur(0px)",
              }}
              exit={{
                opacity: 0,
                scale: 0.5,
                filter: "blur(4px)",
              }}
              transition={{
                ...islandTransition,
                opacity: { duration: 0.2 },
              }}
              style={{ display: "flex" }}
            >
              <ControlButton
                icon="remove"
                onClick={() => setZoomTrigger("OUT")}
                label="Zoom Out"
              />
            </motion.div>
          ) : (
            /* ---------------- STATE 2: EXPANDED (Zoomed Out) ---------------- */
            <motion.div
              key="expanded-mode"
              initial={{
                opacity: 0,
                scale: 0.9,
                filter: "blur(4px)",
              }}
              animate={{
                opacity: 1,
                scale: 1,
                filter: "blur(0px)",
              }}
              exit={{
                opacity: 0,
                scale: 0.9,
                filter: "blur(4px)",
              }}
              transition={{
                ...islandTransition,
                opacity: { duration: 0.2 },
              }}
              style={{
                display: "flex",
                alignItems: "center",
                gap: "8px",
              }}
            >
              <ControlButton
                icon="add"
                onClick={() =>
                  setZoomTrigger(CONFIG.zoomIn)
                }
                label="Zoom In"
              />

              {/* Vertical Divider */}
              <motion.div
                layout
                initial={{ opacity: 0, height: 0 }}
                animate={{ opacity: 1, height: "24px" }}
                transition={{ delay: 0.1 }}
                style={{
                  width: "1px",
                  background: "rgba(0,0,0,0.08)",
                  margin: "0 2px",
                  boxShadow:
                    "0 0 1px rgba(255, 255, 255, 0.3)", // Subtle highlight for glassy feel
                }}
              />

              {/* Collection Tabs Group */}
              <div style={{ display: "flex", gap: "2px" }}>
                {collections.map((name, index) => {
                  const isActive =
                    currentCollection === index;
                  return (
                    <TabButton
                      key={name}
                      isActive={isActive}
                      onClick={() => onSwitch(index)}
                    >
                      {name}
                    </TabButton>
                  );
                })}
              </div>

              {/* Nike Filters - only show when Nike collection is active (desktop only) */}
              {currentCollection === 0 && (
                <div className="desktop-filters">
                  {/* Vertical Divider */}
                  <motion.div
                    layout
                    initial={{ opacity: 0, scaleX: 0 }}
                    animate={{ opacity: 1, scaleX: 1 }}
                    transition={islandTransition}
                    style={{
                      width: "1px",
                      height: "24px",
                      background: "rgba(0,0,0,0.08)",
                      margin: "0 6px",
                      transformOrigin: "center",
                    }}
                  />

                  {/* Filter Chips */}
                  <motion.div
                    layout
                    initial={{ opacity: 0 }}
                    animate={{ opacity: 1 }}
                    transition={islandTransition}
                    style={{ display: "flex", gap: "4px" }}
                  >
                    {nikeFilters.map((filter) => (
                      <FilterChip
                        key={filter.id}
                        isActive={nikeFilter === filter.id}
                        onClick={() => onFilterChange(filter.id)}
                        layoutGroup="desktop"
                      >
                        {filter.label}
                      </FilterChip>
                    ))}
                  </motion.div>
                </div>
              )}

            </motion.div>
          )}
        </AnimatePresence>
      </motion.div>

      {/* Mobile Nike Filters - appears above main bar */}
      <AnimatePresence>
        {currentCollection === 0 && !isZoomedIn && !hasActiveSelection && (
          <motion.div
            className="mobile-filters"
            initial={{ opacity: 0, y: 20 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: 20 }}
            transition={islandTransition}
            style={{
              position: "absolute",
              bottom: "70px",
              left: 0,
              right: 0,
              display: "none", // Hidden by default, shown on mobile via CSS
              justifyContent: "center",
              pointerEvents: "none",
            }}
          >
            <div
              style={{
                display: "flex",
                justifyContent: "center",
                background: "rgba(255, 255, 255, 0.85)",
                backdropFilter: "blur(40px) saturate(200%)",
                WebkitBackdropFilter: "blur(40px) saturate(200%)",
                borderRadius: "20px",
                border: "1px solid rgba(255, 255, 255, 0.3)",
                boxShadow: "0 4px 20px rgba(0, 0, 0, 0.08)",
                padding: "6px 8px",
                gap: "4px",
                pointerEvents: "auto",
              }}
            >
              {nikeFilters.map((filter) => (
                <FilterChip
                  key={`mobile-${filter.id}`}
                  isActive={nikeFilter === filter.id}
                  onClick={() => onFilterChange(filter.id)}
                  layoutGroup="mobile"
                >
                  {filter.label}
                </FilterChip>
              ))}
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Responsive scaling for shorter viewports (tablets) and mobile */}
      <style>{`
        .desktop-filters {
          display: flex;
          align-items: center;
        }
        .mobile-filters {
          display: none !important;
        }
        @media (max-height: 800px) {
          .control-bar-container {
            bottom: 24px !important;
          }
          .control-bar-island {
            height: 48px !important;
            border-radius: 24px !important;
            padding: 4px !important;
          }
        }
        @media (max-height: 650px) {
          .control-bar-container {
            bottom: 16px !important;
          }
          .control-bar-island {
            height: 44px !important;
          }
        }
        @media (max-width: 768px) {
          .control-bar-container {
            bottom: 20px !important;
          }
          .control-bar-island {
            height: 48px !important;
            padding: 4px !important;
          }
          .desktop-filters {
            display: none !important;
          }
          .mobile-filters {
            display: flex !important;
          }
        }
        @media (max-width: 480px) {
          .control-bar-container {
            bottom: 16px !important;
          }
          .control-bar-island {
            height: 44px !important;
          }
          .control-button {
            width: 36px !important;
            height: 36px !important;
          }
          .tab-button {
            padding: 6px 10px !important;
            font-size: 12px !important;
          }
          .filter-chip {
            padding: 4px 8px !important;
            font-size: 11px !important;
          }
          .mobile-filters {
            bottom: 60px !important;
          }
        }
      `}</style>
    </div>
  );
}

// --- Sub-components for cleaner code & isolated animations ---

function ControlButton({ onClick, icon, label }) {
  return (
    <motion.button
      layout="position" // Prevents icon distortion during resize
      onClick={onClick}
      className="control-button"
      whileHover={{
        scale: 1.05,
        backgroundColor: "rgba(0,0,0,0.05)",
      }}
      whileTap={{ scale: 0.9 }}
      transition={{ duration: 0.2 }}
      style={{
        width: "44px",
        height: "44px",
        borderRadius: "50%",
        border: "none",
        background: "transparent",
        color: "#111",
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        cursor: "pointer",
        outline: "none",
      }}
      aria-label={label}
    >
      {/* Simple SVG Icons instead of text for a sharper look */}
      {icon === "add" ? (
        <svg
          width="20"
          height="20"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <line x1="12" y1="5" x2="12" y2="19"></line>
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      ) : (
        <svg
          width="20"
          height="20"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          strokeWidth="2.5"
          strokeLinecap="round"
          strokeLinejoin="round"
        >
          <line x1="5" y1="12" x2="19" y2="12"></line>
        </svg>
      )}
    </motion.button>
  );
}

function TabButton({ children, isActive, onClick }) {
  return (
    <motion.button
      layout
      onClick={onClick}
      className="tab-button"
      style={{
        position: "relative",
        border: "none",
        background: "transparent",
        color: isActive ? "#000" : "#666",
        padding: "8px 16px",
        borderRadius: "20px",
        fontSize: "14px",
        fontWeight: "600",
        cursor: "pointer",
        whiteSpace: "nowrap", // Prevents text wrapping during resize
        zIndex: 1,
        transition: "color 0.2s ease",
      }}
    >
      {children}
      {isActive && (
        <motion.div
          layoutId="activeTabIndicator"
          transition={islandTransition}
          style={{
            position: "absolute",
            inset: 0,
            background: "rgba(255, 255, 255, 0.6)", // Solid background for active state
            backdropFilter: "blur(20px)",
            WebkitBackdropFilter: "blur(20px)",
            borderRadius: "20px",
            border: "1px solid rgba(255, 255, 255, 0.4)",
            boxShadow:
              "0 2px 8px rgba(0,0,0,0.06), inset 0 1px 0 rgba(255, 255, 255, 0.6)",
            zIndex: -1,
          }}
        />
      )}
    </motion.button>
  );
}

function FilterChip({ children, isActive, onClick, layoutGroup = "default" }) {
  return (
    <motion.button
      layout
      onClick={onClick}
      className="filter-chip"
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.95 }}
      transition={islandTransition}
      style={{
        position: "relative",
        border: "none",
        background: "transparent",
        color: isActive ? "#fff" : "#555",
        padding: "6px 12px",
        borderRadius: "14px",
        fontSize: "12px",
        fontWeight: "500",
        cursor: "pointer",
        whiteSpace: "nowrap",
        zIndex: 1,
      }}
    >
      {/* Animated background pill */}
      {isActive && (
        <motion.div
          layoutId={`activeFilterIndicator-${layoutGroup}`}
          transition={islandTransition}
          style={{
            position: "absolute",
            inset: 0,
            background: "rgba(0, 0, 0, 0.85)",
            borderRadius: "14px",
            zIndex: -1,
          }}
        />
      )}
      {!isActive && (
        <div
          style={{
            position: "absolute",
            inset: 0,
            background: "rgba(0, 0, 0, 0.05)",
            borderRadius: "14px",
            zIndex: -1,
          }}
        />
      )}
      {children}
    </motion.button>
  );
}
src/components/Header.jsx
파일 저장

import React from "react";

export default function Header() {
  return (
    <header
      className="site-header"
      style={{
        position: "fixed",
        top: 0,
        left: 0,
        right: 0,
        zIndex: 50,
        padding: "20px 32px",
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        pointerEvents: "none",
      }}
    >
      {/* Logo */}
      <div
        style={{
          display: "flex",
          alignItems: "center",
          gap: "12px",
          pointerEvents: "auto",
        }}
      >
        {/* Text logo */}
        <div
          style={{
            display: "flex",
            flexDirection: "column",
            gap: "0px",
          }}
        >
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: "6px",
            }}
          >
            <span
              style={{
                fontFamily:
                  "'Helvetica Neue', Helvetica, Arial, sans-serif",
                fontSize: "15px",
                fontWeight: "700",
                letterSpacing: "0.12em",
                color: "#000",
                textTransform: "uppercase",
              }}
            >
              PERFECT
            </span>
            <span
              style={{
                fontFamily:
                  "'Helvetica Neue', Helvetica, Arial, sans-serif",
                fontSize: "15px",
                fontWeight: "300",
                letterSpacing: "0.12em",
                color: "#000",
                textTransform: "uppercase",
              }}
            >
              PAIR
            </span>
          </div>
          <span
            style={{
              fontFamily:
                "'Helvetica Neue', Helvetica, Arial, sans-serif",
              fontSize: "8px",
              fontWeight: "400",
              letterSpacing: "0.2em",
              color: "#999",
              textTransform: "uppercase",
              marginTop: "-1px",
            }}
          >
            Est. 2026
          </span>
        </div>
      </div>

      {/* Right side - minimal nav hints */}
      <div
        className="header-nav"
        style={{
          display: "flex",
          alignItems: "center",
          gap: "24px",
          pointerEvents: "auto",
        }}
      >
        <NavItem label="BROWSE" number="01" />
        <NavItem label="COLLECT" number="02" />
        <Divider />
        <QuoteText text="FOOTWEAR" />
      </div>

      {/* Mobile responsive styles */}
      <style>{`
        @media (max-width: 600px) {
          .site-header {
            padding: 16px 20px !important;
          }
          .header-nav {
            display: none !important;
          }
        }
      `}</style>
    </header>
  );
}

function NavItem({ label, number }) {
  return (
    <div
      style={{
        display: "flex",
        alignItems: "baseline",
        gap: "6px",
        cursor: "pointer",
        opacity: 0.6,
        transition: "opacity 0.2s ease",
      }}
      onMouseEnter={(e) =>
        (e.currentTarget.style.opacity = 1)
      }
      onMouseLeave={(e) =>
        (e.currentTarget.style.opacity = 0.6)
      }
    >
      <span
        style={{
          fontFamily:
            "'Helvetica Neue', Helvetica, Arial, sans-serif",
          fontSize: "9px",
          fontWeight: "400",
          color: "#999",
        }}
      >
        {number}
      </span>
      <span
        style={{
          fontFamily:
            "'Helvetica Neue', Helvetica, Arial, sans-serif",
          fontSize: "11px",
          fontWeight: "500",
          letterSpacing: "0.08em",
          color: "#000",
        }}
      >
        {label}
      </span>
    </div>
  );
}

function Divider() {
  return (
    <div
      style={{
        width: "1px",
        height: "12px",
        background: "rgba(0,0,0,0.15)",
      }}
    />
  );
}

function QuoteText({ text }) {
  return (
    <span
      style={{
        fontFamily:
          "'Helvetica Neue', Helvetica, Arial, sans-serif",
        fontSize: "11px",
        fontWeight: "400",
        letterSpacing: "0.05em",
        color: "#000",
        opacity: 0.4,
      }}
    >
      "{text}"
    </span>
  );
}
src/components/HoloCardMaterial.js
파일 저장

import * as THREE from "three";
import { shaderMaterial } from "@react-three/drei";
import { extend } from "@react-three/fiber";
import vertexShader from "@/shaders/holocard.vert";
import fragmentShader from "@/shaders/holocard.frag";

// --- HOLOGRAPHIC CARD SHADER MATERIAL ---
const HoloCardMaterial = shaderMaterial(
  {
    uTime: 0,
    uTexture: new THREE.Texture(),
    uOpacity: 1,
    uActive: 0, // 0 = normal, 1 = active/selected
  },
  vertexShader,
  fragmentShader
);

// Extend so R3F recognizes <holoCardMaterial />
extend({ HoloCardMaterial });

export default HoloCardMaterial;
src/components/MiniMap.jsx
파일 저장

import React, { useRef, useEffect, useState } from "react";

export default function MiniMap({
  gridDims,
  rigState,
  config,
  totalItems,
  isZoomedIn,
}) {
  const containerRef = useRef();
  const canvasRef = useRef();
  const zoomRef = useRef(1);
  const centerRef = useRef({ x: 0.5, y: 0.5 });
  const opacityRef = useRef(0);

  // Use relative sizing (percentage of viewport) - larger on mobile
  const [mapWidthPercent, setMapWidthPercent] = useState(8);
  const [dimensions, setDimensions] = useState({ width: 0, height: 0 });

  const aspectRatio = gridDims.width / gridDims.height;
  const dpr =
    typeof window !== "undefined"
      ? window.devicePixelRatio || 1
      : 1;

  // Calculate actual pixel dimensions from percentage, responsive sizing
  useEffect(() => {
    const updateDimensions = () => {
      // Larger minimap on mobile
      let widthPercent = 8; // Desktop default
      if (window.innerWidth < 480) {
        widthPercent = 20; // Phone
      } else if (window.innerWidth < 768) {
        widthPercent = 15; // Tablet
      }
      setMapWidthPercent(widthPercent);

      const width = (window.innerWidth * widthPercent) / 100;
      const height = width / aspectRatio;
      setDimensions({ width, height });
    };
    updateDimensions();
    window.addEventListener("resize", updateDimensions);
    return () => window.removeEventListener("resize", updateDimensions);
  }, [aspectRatio]);

  const cols = config.gridCols;
  const rows = Math.ceil(totalItems / cols);

  useEffect(() => {
    let rafId;

    const draw = () => {
      if (!containerRef.current || !canvasRef.current) {
        rafId = requestAnimationFrame(draw);
        return;
      }

      // Show when dragging OR when an item is active
      // On mobile, only show when zoomed in
      const isMobile = window.innerWidth < 768;
      const isActive =
        rigState.isDragging || rigState.activeId !== null;
      const shouldShow = isMobile ? (isActive && isZoomedIn) : isActive;
      const targetOp = shouldShow ? 1 : 0;
      opacityRef.current +=
        (targetOp - opacityRef.current) * 0.1;
      containerRef.current.style.opacity =
        opacityRef.current;

      if (opacityRef.current < 0.02) {
        rafId = requestAnimationFrame(draw);
        return;
      }

      // Calculate zoom and center targets
      const isFocused = rigState.activeId !== null;
      const targetZoom = isFocused ? 2.5 : 1;

      let targetCenterX = 0.5;
      let targetCenterY = 0.5;

      if (isFocused) {
        const col = rigState.activeId % cols;
        const row = Math.floor(rigState.activeId / cols);
        targetCenterX = (col + 0.5) / cols;
        targetCenterY = (row + 0.5) / rows;
      }

      // Smooth interpolation
      zoomRef.current +=
        (targetZoom - zoomRef.current) * 0.08;
      centerRef.current.x +=
        (targetCenterX - centerRef.current.x) * 0.08;
      centerRef.current.y +=
        (targetCenterY - centerRef.current.y) * 0.08;

      const canvas = canvasRef.current;
      const ctx = canvas.getContext("2d");
      const w = canvas.width;
      const h = canvas.height;

      ctx.clearRect(0, 0, w, h);

      // Apply zoom transform
      ctx.save();
      const zoom = zoomRef.current;
      const cx = centerRef.current.x * w;
      const cy = centerRef.current.y * h;

      ctx.translate(w / 2, h / 2);
      ctx.scale(zoom, zoom);
      ctx.translate(-cx, -cy);

      // Draw dots - size relative to canvas
      const baseDotSize = Math.max(w, h) * 0.015; // 1.5% of larger dimension
      for (let i = 0; i < totalItems; i++) {
        const c = i % cols;
        const r = Math.floor(i / cols);
        const nX = (c + 0.5) / cols;
        const nY = (r + 0.5) / rows;

        const isSelected = rigState.activeId === i;
        const dotSize = isSelected ? baseDotSize * 2 : baseDotSize;

        ctx.beginPath();
        ctx.arc(nX * w, nY * h, dotSize, 0, Math.PI * 2);
        ctx.fillStyle = isSelected
          ? "#FFB000"
          : "rgba(255, 255, 255, 0.4)";
        ctx.fill();
      }

      // Viewport Rect (only show when not focused)
      if (!isFocused) {
        const offPctX =
          -rigState.current.x / gridDims.width;
        const offPctY =
          rigState.current.y / gridDims.height;

        const vFov = (45 * Math.PI) / 180;
        const viewHeight = 2 * Math.tan(vFov / 2) * 10;
        const viewWidth =
          viewHeight *
          (window.innerWidth / window.innerHeight);

        const rectW =
          Math.min(viewWidth / gridDims.width, 1) * w;
        const rectH =
          Math.min(viewHeight / gridDims.height, 1) * h;
        const rectX = (0.5 + offPctX) * w - rectW / 2;
        const rectY = (0.5 + offPctY) * h - rectH / 2;

        ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
        ctx.lineWidth = 1.5 / zoom;
        ctx.strokeRect(rectX, rectY, rectW, rectH);
      }

      ctx.restore();
      rafId = requestAnimationFrame(draw);
    };

    rafId = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(rafId);
  }, [gridDims, cols, rows, rigState, config, totalItems, isZoomedIn]);

  return (
    <div
      ref={containerRef}
      style={{
        position: "fixed",
        bottom: "2vh",
        right: "2vw",
        width: `${mapWidthPercent}vw`,
        aspectRatio: aspectRatio,
        background: "rgba(0,0,0,0.4)",
        backdropFilter: "blur(10px)",
        borderRadius: 0,
        opacity: 0,
        border: "1px solid rgba(255,255,255,0.2)",
        overflow: "hidden",
        pointerEvents: "none",
        zIndex: 100,
      }}
    >
      <canvas
        ref={canvasRef}
        width={dimensions.width * dpr}
        height={dimensions.height * dpr}
        style={{ width: "100%", height: "100%" }}
      />
    </div>
  );
}
src/components/TopologyBackground.jsx
파일 저장

import React, { useRef } from "react";
import { useFrame, extend } from "@react-three/fiber";
import { shaderMaterial, Plane } from "@react-three/drei";
import * as THREE from "three";
import { easing } from "maath";
import vertexShader from "@/shaders/topography.vert";
import fragmentShader from "@/shaders/topography.frag";

// --- 1. SHADER: FLUID TOPOGRAPHY ---
const TopographyMaterial = shaderMaterial(
  {
    uTime: 0,
    uColor: new THREE.Color("#e0e0e0"),
    uResolution: new THREE.Vector2(1, 1),
    uOpacity: 1.0,
    uLineOpacity: 0.4,
    uScale: 3.0,
    uLineThickness: 0.03,
  },
  vertexShader,
  fragmentShader
);

extend({ TopographyMaterial });

export function TopologyBackground({
  isZoomedIn = false,
  quality = 1,
  color = "#e0e0e0",
  opacity = 0.4,
  speed = 0.05,
  scale = 3.0,
  lineThickness = 0.03,
}) {
  const materialRef = useRef();

  // Fixed world-space dimensions (doesn't change with camera zoom)
  const planeWidth = 90;
  const planeHeight = 40;

  useFrame((_, delta) => {
    if (!materialRef.current) return;

    materialRef.current.uTime += delta * (speed / 0.05); // Normalize speed
    materialRef.current.uResolution.set(planeWidth, planeHeight);
    materialRef.current.uColor.set(color);
    materialRef.current.uLineOpacity = opacity;
    materialRef.current.uScale = scale;
    materialRef.current.uLineThickness = lineThickness;

    // Tween opacity based on zoom state
    const targetOpacity = isZoomedIn ? 0.25 : 1.0;
    easing.damp(materialRef.current, "uOpacity", targetOpacity, 0.3, delta);
  });

  // Low quality mode: render simple solid color plane (no shader)
  if (quality < 0.5) {
    return (
      <Plane args={[planeWidth, planeHeight]} position={[0, 0, -15]} renderOrder={-1}>
        <meshBasicMaterial color="#e8e8e8" />
      </Plane>
    );
  }

  return (
    // Pass the calculated Width/Height to the geometry
    <Plane args={[planeWidth, planeHeight]} position={[0, 0, -15]} renderOrder={-1}>
      <topographyMaterial
        ref={materialRef}
        transparent
        depthWrite={false}
      />
    </Plane>
  );
}
src/components/grid/GridCanvas.jsx
파일 저장

import { useMemo, useState } from "react";
import { useFrame } from "@react-three/fiber";
import { CONFIG } from "./gridConfig";
import { EMPTY_COLORS, matchesFilter, calculateGridDimensions } from "./gridState";
import { ShoeTile } from "./ShoeTile";

// --- OPTIMIZED COMPONENT: GRID CANVAS ---
// Renders a single set of items with Time-Sliced mounting
export function GridCanvas({
    items,
    gridVisible,
    transitionStartTime,
    interactive,
    filter = "all",
    colorFilter = EMPTY_COLORS,
}) {
    // Calculate filtered items and their new positions
    const { mappedItems, filteredGridDims } = useMemo(() => {
        const spacing = CONFIG.itemSize + CONFIG.gap;
        const filteredItems = items.filter((item) =>
            matchesFilter(item, filter, colorFilter)
        );
        const filteredCount = filteredItems.length;
        const filteredDims =
            calculateGridDimensions(filteredCount);
        const maxDelay = gridVisible
            ? CONFIG.enterStaggerDelay
            : CONFIG.exitStaggerDelay;
        let filteredIdx = 0;
        const mapped = items.map((shoe, i) => {
            const matches = matchesFilter(shoe, filter, colorFilter);
            let targetPos;
            if (matches) {
                const col = filteredIdx % CONFIG.gridCols;
                const row = Math.floor(
                    filteredIdx / CONFIG.gridCols
                );
                targetPos = {
                    x:
                        col * spacing -
                        filteredDims.width / 2 +
                        spacing / 2,
                    y:
                        -(row * spacing) +
                        filteredDims.height / 2 -
                        spacing / 2,
                };
                filteredIdx++;
            } else {
                const col = i % CONFIG.gridCols;
                const row = Math.floor(i / CONFIG.gridCols);
                const originalDims = calculateGridDimensions(
                    items.length
                );
                targetPos = {
                    x:
                        col * spacing -
                        originalDims.width / 2 +
                        spacing / 2,
                    y:
                        -(row * spacing) +
                        originalDims.height / 2 -
                        spacing / 2,
                };
            }
            return {
                ...shoe,
                index: i,
                randomDelay: Math.random() * maxDelay,
                basePos: targetPos,
                matchesFilter: matches,
            };
        });
        return {
            mappedItems: mapped,
            filteredGridDims: filteredDims,
        };
    }, [items, filter, colorFilter, gridVisible]);
    // --- TIME-SLICED MOUNTING ---
    // Start with 0 items rendered for entering grids.
    // Every frame, add more items to prevent GPU texture upload spike.
    // If EXITING, render everything immediately (no need to stagger out).
    const [mountedCount, setMountedCount] = useState(
        gridVisible ? 0 : items.length
    );
    useFrame(() => {
        // 5 items per frame @ 60fps = ~12 frames (200ms) to load 60 items.
        // Fast enough to be invisible, slow enough to fix the lag.
        if (mountedCount < mappedItems.length) {
            setMountedCount((prev) =>
                Math.min(prev + 5, mappedItems.length)
            );
        }
    });
    return (
        <>
            {mappedItems.map((item, i) => {
                // Only render if within the mounted count
                if (i > mountedCount) return null;
                return (
                    <ShoeTile
                        key={item.product_url || item.index}
                        data={item}
                        index={item.index}
                        basePos={item.basePos}
                        gridVisible={gridVisible}
                        transitionStartTime={transitionStartTime}
                        interactive={interactive && item.matchesFilter}
                        matchesFilter={item.matchesFilter}
                        gridHeight={filteredGridDims.height}
                    />
                );
            })}
        </>
    );
}
src/components/grid/Rig.jsx
파일 저장

import { useRef, useEffect } from "react";
import { useFrame, useThree } from "@react-three/fiber";
import * as THREE from "three";
import { easing } from "maath";
import { CONFIG } from "./gridConfig";
import { rigState } from "./gridState";

// --- COMPONENT: RIG ---
// Controls the camera. Moved OUT of GridCanvas so it is persistent.
export function Rig({ gridW, gridH }) {
    const { camera, gl } = useThree();
    const prevPos = useRef(new THREE.Vector3());
    const hasSetInitialZoom = useRef(false);
    useEffect(() => {
        if (!hasSetInitialZoom.current && rigState.zoom) {
            camera.position.z = rigState.zoom;
            hasSetInitialZoom.current = true;
        }
    }, [camera]);
    const getBounds = () => {
        const dist = camera.position.z;
        const vFov = (camera.fov * Math.PI) / 180;
        const visibleHeight = 2 * Math.tan(vFov / 2) * dist;
        const visibleWidth = visibleHeight * camera.aspect;
        const xLimit = Math.max(
            0,
            (gridW - visibleWidth) / 2 + 2
        );
        const yLimit = Math.max(
            0,
            (gridH - visibleHeight) / 2 + 2
        );
        return { x: xLimit, y: yLimit, visibleHeight };
    };

    useEffect(() => {
        const canvas = gl.domElement;
        let isDown = false;
        let startX = 0;
        let startY = 0;
        let initialRigX = 0;
        let initialRigY = 0;
        let maxDragDistance = 0; // Track max distance for click detection
        const onDown = (e) => {
            isDown = true;
            startX = e.clientX;
            startY = e.clientY;
            initialRigX = rigState.target.x;
            initialRigY = rigState.target.y;
            maxDragDistance = 0;
            rigState.isDragging = false; // Reset on new gesture
            canvas.style.cursor = "grabbing";
        };
        const onMove = (e) => {
            if (!isDown) return;
            const dx = e.clientX - startX;
            const dy = e.clientY - startY;
            const distance = Math.sqrt(dx * dx + dy * dy);
            maxDragDistance = Math.max(maxDragDistance, distance);
            // Use higher threshold for mobile (touch is less precise)
            const threshold =
                "ontouchstart" in window
                    ? 15
                    : CONFIG.clickThreshold;
            if (maxDragDistance > threshold) {
                rigState.isDragging = true;
                rigState.activeId = null;
            }
            const { x: bx, y: by, visibleHeight } = getBounds();
            const sensitivity =
                (visibleHeight / window.innerHeight) *
                CONFIG.dragSpeed;
            let rawTargetX = initialRigX + dx * sensitivity;
            let rawTargetY = initialRigY - dy * sensitivity;
            // Apply resistance when dragging past bounds
            if (rawTargetX > bx)
                rawTargetX =
                    bx + (rawTargetX - bx) * CONFIG.dragResistance;
            if (rawTargetX < -bx)
                rawTargetX =
                    -bx + (rawTargetX + bx) * CONFIG.dragResistance;
            if (rawTargetY > by)
                rawTargetY =
                    by + (rawTargetY - by) * CONFIG.dragResistance;
            if (rawTargetY < -by)
                rawTargetY =
                    -by + (rawTargetY + by) * CONFIG.dragResistance;
            // Hard limit on overshoot for snappier mobile feel
            const maxOvershoot = 3;
            rawTargetX = Math.max(
                -bx - maxOvershoot,
                Math.min(bx + maxOvershoot, rawTargetX)
            );
            rawTargetY = Math.max(
                -by - maxOvershoot,
                Math.min(by + maxOvershoot, rawTargetY)
            );
            rigState.target.set(rawTargetX, rawTargetY, 0);
        };
        const onUp = () => {
            if (!isDown) return; // Ignore spurious events
            isDown = false;
            rigState.isDragging = false;
            canvas.style.cursor = "grab";
            if (rigState.activeId !== null) return;
            const { x: bx, y: by } = getBounds();
            // When zoomed out, snap to center. When zoomed in, allow panning.
            const isZoomedOut =
                camera.position.z > CONFIG.zoomIn + 2;
            let snapX = isZoomedOut
                ? 0
                : Math.max(-bx, Math.min(bx, rigState.target.x));
            let snapY = isZoomedOut
                ? 2
                : Math.max(-by, Math.min(by, rigState.target.y));
            rigState.target.set(snapX, snapY, 0);
        };
        canvas.addEventListener("pointerdown", onDown);
        window.addEventListener("pointermove", onMove);
        window.addEventListener("pointerup", onUp);
        window.addEventListener("pointercancel", onUp); // Handle interrupted touch
        return () => {
            canvas.removeEventListener("pointerdown", onDown);
            window.removeEventListener("pointermove", onMove);
            window.removeEventListener("pointerup", onUp);
            window.removeEventListener("pointercancel", onUp);
        };
    }, [gl, camera, gridW, gridH]);

    useFrame((state, delta) => {
        easing.damp3(
            rigState.current,
            rigState.target,
            CONFIG.dampFactor,
            delta
        );
        easing.damp(
            camera.position,
            "z",
            rigState.zoom,
            CONFIG.zoomDamp,
            delta
        );
        rigState.velocity
            .copy(rigState.current)
            .sub(prevPos.current);
        prevPos.current.copy(rigState.current);
        const zoomFactor = Math.min(
            1,
            CONFIG.zoomIn / rigState.zoom
        );
        const tiltX =
            rigState.velocity.y * CONFIG.tiltFactor * zoomFactor;
        const tiltY =
            -rigState.velocity.x * CONFIG.tiltFactor * zoomFactor;
        easing.damp(camera.rotation, "x", tiltX, 0.2, delta);
        easing.damp(camera.rotation, "y", tiltY, 0.2, delta);
    });
    return null;
}
src/components/grid/ShoeGrid.jsx
파일 저장

import React, {
    useMemo,
    useState,
    useEffect,
    Suspense,
} from "react";
import { Canvas } from "@react-three/fiber";
import { useTexture } from "@react-three/drei";
import * as THREE from "three";
import { Leva } from "leva";
// --- REAL DATA IMPORT ---
import shoes from "../../../backend/shoes.json";
import MiniMap from "../MiniMap";
import { DEFAULT_CONFIG, CONFIG } from "./gridConfig";
import { rigState, calculateGridDimensions, EMPTY_COLORS, matchesFilter } from "./gridState";
import { useGridConfig } from "./useGridConfig";
import { Rig } from "./Rig";
import { GridCanvas } from "./GridCanvas";
import { UnifiedControlBar } from "../GridUI";
import Header from "../Header";
import { TopologyBackground } from "../TopologyBackground";
import "../HoloCardMaterial"; // Registers <holoCardMaterial /> with R3F

// --- PRELOAD ALL TEXTURES ---
// This ensures all shoe images are cached before switching collections
shoes.forEach((shoe) => {
    useTexture.preload(shoe.image_url);
});

// --- MAIN EXPORT ---
export default function ShoeGrid() {
    const [zoomTarget, setZoomTarget] = useState(null);
    const [initialZoom] = useState(DEFAULT_CONFIG.zoomOut);
    const [currentZoom, setCurrentZoom] = useState(
        rigState.zoom
    );
    const controls = useGridConfig();
    // Track zoom state for UI components
    useEffect(() => {
        const interval = setInterval(() => {
            setCurrentZoom(rigState.zoom);
        }, 50); // Update every 50ms
        return () => clearInterval(interval);
    }, []);
    // Track active selection state
    const [hasActiveSelection, setHasActiveSelection] =
        useState(false);
    useEffect(() => {
        const interval = setInterval(() => {
            setHasActiveSelection(rigState.activeId !== null);
        }, 16); // Update every frame (60fps) for smoother updates
        return () => clearInterval(interval);
    }, []);
    const isZoomedIn = currentZoom <= CONFIG.zoomIn + 0.5;

    // Responsive zoom for mobile viewports
    useEffect(() => {
        const updateResponsiveZoom = () => {
            const width = window.innerWidth;
            let newZoomOut;
            if (width < 480) {
                newZoomOut = 48; // Phone
            } else if (width < 768) {
                newZoomOut = 38; // Tablet portrait
            } else {
                newZoomOut = DEFAULT_CONFIG.zoomOut; // Desktop default (31)
            }
            CONFIG.zoomOut = newZoomOut;
            // Only update current zoom if we're in zoomed-out state
            if (rigState.zoom > CONFIG.zoomIn + 2) {
                rigState.zoom = newZoomOut;
                setCurrentZoom(newZoomOut);
            }
        };
        updateResponsiveZoom();
        window.addEventListener("resize", updateResponsiveZoom);
        return () => window.removeEventListener("resize", updateResponsiveZoom);
    }, []);

    // Filter state for Nike collection
    const [nikeFilter, setNikeFilter] = useState("all"); // 'all' | 'jordan' | 'dunk'
    const [colorFilter, setColorFilter] = useState(EMPTY_COLORS); // [] = all, ['blue','green'] = blue OR green

    // Collections - Nike (all, unfiltered), New Balance, Under $150
    const collectionsData = useMemo(() => {
        // All Nike shoes (filtering happens in GridCanvas)
        const nike = shoes.filter((s) => s.brand === "Nike");
        // New Balance shoes - take half and double to make 30+ items
        const newBalanceFull = shoes.filter(
            (s) => s.brand === "New Balance"
        );
        const newBalanceHalf = newBalanceFull.slice(0, Math.ceil(newBalanceFull.length / 2));
        const newBalance = [
            ...newBalanceHalf,
            ...newBalanceHalf.map((s, i) => ({
                ...s,
                product_url: `${s.product_url}-dup-${i}`,
            })),
        ];
        // Under $150 (all brands)
        const budget = shoes.filter((s) => {
            const price = parseInt(
                s.price?.replace(/[$,]/g, "") || "999"
            );
            return price < 150;
        });
        return [nike, newBalance, budget];
    }, []);
    // --- Grid Stack State ---
    // Instead of one list of items, we keep a stack of "Rendered Layers".
    // This allows us to have one layer exiting and one layer entering simultaneously.
    // Initial grid uses Nike collection (index 0)
    const [gridLayers, setGridLayers] = useState(() => [
        {
            id: "init",
            items: shoes.filter((s) => s.brand === "Nike"),
            mode: "enter", // 'enter' | 'exit'
            startTime: 0,
        },
    ]);
    const [activeCollectionIdx, setActiveCollectionIdx] =
        useState(0);
    const handleCollectionSwitch = (index) => {
        if (index === activeCollectionIdx) return;
        const now = Date.now();
        setGridLayers((prev) => {
            // 1. Mark existing 'enter' layers as 'exit'
            const exitingLayers = prev.map((layer) =>
                layer.mode === "enter"
                    ? { ...layer, mode: "exit", startTime: now }
                    : layer
            );
            // 2. Add new 'enter' layer
            const newLayer = {
                id: `grid-${index}-${now}`, // Unique ID for key
                items: collectionsData[index],
                mode: "enter",
                startTime: now,
            };
            return [...exitingLayers, newLayer];
        });
        setActiveCollectionIdx(index);
        // Clear Nike filters when leaving Nike collection
        setNikeFilter("all");
        setColorFilter(EMPTY_COLORS);
        rigState.target.set(0, 2, 0);
        rigState.activeId = null;
        // 3. Cleanup old layers after transition time
        setTimeout(() => {
            setGridLayers((prev) =>
                prev.filter((layer) => layer.mode === "enter")
            );
        }, CONFIG.cleanupTimeout);
    };
    // Handle filter change (for Nike collection) - just update filter state
    // The grid will animate items in place
    const handleFilterChange = (filter) => {
        if (filter === nikeFilter) return;
        setNikeFilter(filter);
        rigState.activeId = null;
    };

    // Handle color filter change (accepts array of colors)
    const handleColorFilterChange = (colors) => {
        setColorFilter(colors.length > 0 ? colors : EMPTY_COLORS);
        rigState.activeId = null;
    };

    useEffect(() => {
        if (zoomTarget === "OUT") {
            rigState.zoom = CONFIG.zoomOut;
            setCurrentZoom(CONFIG.zoomOut);
            rigState.target.set(0, 2, 0);
        } else if (typeof zoomTarget === "number") {
            rigState.zoom = zoomTarget;
            setCurrentZoom(zoomTarget);
        }
        setZoomTarget(null);
    }, [zoomTarget]);
    // Determine active grid dimensions for the Rig
    // We use the dimensions of the LAST layer (the incoming one)
    const activeLayer = gridLayers[gridLayers.length - 1];
    // Calculate filtered item count for Nike collection
    const filteredItemCount = useMemo(() => {
        if (activeCollectionIdx !== 0)
            return activeLayer.items.length;
        return activeLayer.items.filter((item) =>
            matchesFilter(item, nikeFilter, colorFilter)
        ).length;
    }, [activeLayer.items, activeCollectionIdx, nikeFilter, colorFilter]);

    const activeDims = calculateGridDimensions(
        filteredItemCount
    );

    return (
        <div
            style={{
                width: "100vw",
                height: "100vh",
                backgroundColor: "#f0f0f0",
                position: "relative",
                overflow: "hidden",
                touchAction: "none", // Prevent mobile browser touch gestures
            }}
        >
            <Leva collapsed={true} hidden={false} />
            <Header />
            <Canvas
                camera={{ position: [0, 0, initialZoom], fov: 45 }}
                dpr={[1, 2]}
                gl={{
                    antialias: true,
                    toneMapping: THREE.NoToneMapping,
                }}
            >
                {/* Rig is now shared, based on the dimensions of the active grid */}
                <Rig
                    gridW={activeDims.width}
                    gridH={activeDims.height}
                />
                {/* Tech Background - geometric lines and crosshairs for CAD/architectural feel */}
                <TopologyBackground
                    isZoomedIn={isZoomedIn}
                    color={CONFIG.bgColor}
                    opacity={CONFIG.bgOpacity}
                    speed={CONFIG.bgSpeed}
                    scale={CONFIG.bgScale}
                    lineThickness={CONFIG.bgLineThickness}
                />
                <fog
                    attach="fog"
                    args={[
                        "#f0f0f0",
                        controls?.fogNear ?? DEFAULT_CONFIG.fogNear,
                        controls?.fogFar ?? DEFAULT_CONFIG.fogFar,
                    ]}
                />
                {/* Suspense boundary for texture loading */}
                <Suspense fallback={null}>
                    {/* Render all active layers (Entering + Exiting) */}
                    {gridLayers.map((layer, layerIdx) => (
                        <GridCanvas
                            key={layer.id} // Essential for React to treat them as different trees
                            items={layer.items}
                            gridVisible={layer.mode === "enter"}
                            transitionStartTime={layer.startTime}
                            interactive={layer.mode === "enter"} // Only entering grid is clickable
                            filter={
                                activeCollectionIdx === 0
                                    ? nikeFilter
                                    : "all"
                            }
                            colorFilter={
                                activeCollectionIdx === 0
                                    ? colorFilter
                                    : EMPTY_COLORS
                            }
                        />
                    ))}
                </Suspense>
            </Canvas>
            <MiniMap
                gridDims={activeDims}
                rigState={rigState}
                config={CONFIG}
                totalItems={filteredItemCount}
                isZoomedIn={isZoomedIn}
            />
            <UnifiedControlBar
                currentCollection={activeCollectionIdx}
                onSwitch={handleCollectionSwitch}
                setZoomTrigger={setZoomTarget}
                isZoomedIn={isZoomedIn}
                hasActiveSelection={hasActiveSelection}
                nikeFilter={nikeFilter}
                onFilterChange={handleFilterChange}
            />
        </div>
    );
}
src/components/grid/ShoeTile.jsx
파일 저장

import React, {
    useRef,
    useMemo,
    useState,
    useLayoutEffect,
} from "react";
import { useFrame } from "@react-three/fiber";
import { useTexture, Text } from "@react-three/drei";
import * as THREE from "three";
import { easing } from "maath";
import { CONFIG } from "./gridConfig";
import { rigState } from "./gridState";
import { CloseButton } from "../CloseButton";

// --- OPTIMIZED COMPONENT: SHOE TILE ---
export function ShoeTile({
    data,
    index,
    basePos,
    gridVisible,
    transitionStartTime,
    interactive,
    matchesFilter = true,
    gridHeight,
}) {
    const ref = useRef();
    const imageRef = useRef();
    const titleRef = useRef();
    const priceRef = useRef();
    const [hovered, setHovered] = useState(false);
    const texture = useTexture(data.image_url);
    // Animation Refs
    const focusZ = useRef(0);
    const rotationX = useRef(0);
    const rotationY = useRef(0);
    const curveZ = useRef(0);
    const transitionZ = useRef(0);
    const transitionY = useRef(0);
    const breathScale = useRef(1);
    // Animated position for filter transitions
    const animatedPos = useRef({
        x: basePos.x,
        y: basePos.y,
    });
    const filterOpacity = useRef(1);
    const filterScale = useRef(1);
    // State to track if we should stop processing entirely (optimization)
    const isSleep = useRef(false);
    // Track if this item was dimmed due to focus mode (for fast recovery)
    const wasDimmedByFocus = useRef(false);
    // RESET ANIMATION STATE ON MOUNT

    useLayoutEffect(() => {
        const normalizedY =
            gridHeight > 0 ? basePos.y / (gridHeight / 2) : 0;
        if (gridVisible) {
            transitionZ.current = CONFIG.enterStartZ;
            transitionY.current =
                normalizedY * CONFIG.enterSpreadY;
            if (imageRef.current)
                imageRef.current.material.uOpacity =
                    CONFIG.enterStartOpacity;
            isSleep.current = false;
        } else {
            transitionZ.current = 0;
            transitionY.current = 0;
            if (imageRef.current)
                imageRef.current.material.uOpacity = 1;
        }
    }, []);
    const imageDims = useMemo(() => {
        const maxSize = CONFIG.itemSize * 0.9;
        if (!texture.image)
            return { width: maxSize, height: maxSize };
        const imgAspect =
            texture.image.width / texture.image.height;
        return imgAspect > 1
            ? { width: maxSize, height: maxSize / imgAspect }
            : { width: maxSize * imgAspect, height: maxSize };
    }, [texture]);
    useFrame((state, delta) => {
        // OPTIMIZATION 1: If sleeping or ref missing, stop immediately.
        if (!ref.current || isSleep.current) return;
        // --- 0. Filter Animation ---
        easing.damp(
            animatedPos.current,
            "x",
            basePos.x,
            0.2,
            delta
        );
        easing.damp(
            animatedPos.current,
            "y",
            basePos.y,
            0.2,
            delta
        );
        const targetFilterOpacity = matchesFilter ? 1 : 0;
        const targetFilterScale = matchesFilter ? 1 : CONFIG.filterScaleTarget;
        easing.damp(
            filterOpacity,
            "current",
            targetFilterOpacity,
            CONFIG.filterOpacityDamp,
            delta
        );
        easing.damp(
            filterScale,
            "current",
            targetFilterScale,
            CONFIG.filterOpacityDamp,
            delta
        );
        // Sleep check: If filtered out and visually invisible, stop processing
        // Check actual material opacity, not filterOpacity ref, to avoid popping
        const actualOpacity = imageRef.current?.material?.uOpacity ?? 1;
        if (actualOpacity < 0.01 && !matchesFilter) {
            ref.current.visible = false;
            return;
        }
        // --- 1. Stagger Logic ---
        const now = Date.now();
        const timeSinceTrigger = now - transitionStartTime;
        const staggerDelay = data.randomDelay || 0;
        const canTransition = timeSinceTrigger > staggerDelay;

        // --- 2. Calculate Targets ---
        let targetTransitionOpacity = 1.0;
        let targetTransitionZ = 0;
        const normalizedY =
            gridHeight > 0 ? basePos.y / (gridHeight / 2) : 0;
        let targetTransitionY = 0;
        if (gridVisible) {
            // ENTERING
            if (canTransition) {
                targetTransitionOpacity = 1.0;
                targetTransitionZ = 0;
                targetTransitionY = 0;
            } else {
                targetTransitionOpacity = CONFIG.enterStartOpacity;
                targetTransitionZ = CONFIG.enterStartZ;
                targetTransitionY =
                    normalizedY * CONFIG.enterSpreadY;
            }
        } else {
            // EXITING
            if (canTransition) {
                targetTransitionOpacity = 0.0;
                targetTransitionZ = CONFIG.exitEndZ;
                targetTransitionY =
                    normalizedY * CONFIG.exitSpreadY;
            } else {
                targetTransitionOpacity = 1.0;
                targetTransitionZ = 0;
                targetTransitionY = 0;
            }
        }
        // --- 3. Base Position ---
        const x = animatedPos.current.x + rigState.current.x;
        const y = animatedPos.current.y + rigState.current.y;
        // --- 4. Dynamic Culling ---
        const currentCull =
            CONFIG.cullDistance * (rigState.zoom / 8);
        const isPositionVisible =
            Math.abs(x) < currentCull &&
            Math.abs(y) < currentCull;
        // OPTIMIZATION 2: Strict Visibility Culling
        // If exiting and invisible, stop running this loop forever
        if (
            !gridVisible &&
            targetTransitionOpacity < 0.01 &&
            filterOpacity.current < 0.01
        ) {
            ref.current.visible = false;
            isSleep.current = true;
            return;
        }
        // Standard view culling
        if (
            !isPositionVisible &&
            !(!gridVisible && canTransition)
        ) {
            ref.current.visible = false;
            return;
        }
        // If opacity is effectively 0, hide mesh to save GPU rasterization
        if (
            imageRef.current?.material.uOpacity < 0.01 &&
            targetTransitionOpacity < 0.01
        ) {
            ref.current.visible = false;
            return;
        }
        ref.current.visible = true;
        // --- 5. Curvature & Zoom ---
        const isZoomedIn = rigState.zoom <= CONFIG.zoomIn + 0.5;
        const maxZoom = CONFIG.zoomOut || 50;
        const zoomRatio = isZoomedIn
            ? 0
            : THREE.MathUtils.clamp(
                (rigState.zoom - CONFIG.zoomIn) /
                (maxZoom - CONFIG.zoomIn),
                0,
                1
            );
        const smoothRatio = easing.cubic.inOut(zoomRatio);
        const distSq = x * x + y * y;
        const dist = Math.sqrt(distSq);
        const targetCurveZ =
            -distSq * CONFIG.curvatureStrength * smoothRatio;
        // Optimization: Skip complex rotation math if fading out
        let rotX = 0,
            rotY = 0;
        if (targetTransitionOpacity > 0.1) {
            const rotationIntensity =
                Math.min(dist * 0.4, 2.0) * smoothRatio;
            rotX =
                y *
                CONFIG.curvatureStrength *
                CONFIG.rotationStrength *
                rotationIntensity;
            rotY =
                -x *
                CONFIG.curvatureStrength *
                CONFIG.rotationStrength *
                rotationIntensity;
        }
        // --- 6. Interaction State ---
        const isFocusMode = rigState.activeId !== null;
        const isActive = rigState.activeId === index;
        const isHovered = hovered && interactive;
        let interactionScale = 1.0;
        let interactionOpacity = 1.0;
        let targetTextOpacity = 0;
        let targetFocusZ = 0;
        if (isFocusMode) {
            if (isActive) {
                interactionScale = CONFIG.focusScale;
                interactionOpacity = 1.0;
                targetTextOpacity = 1.0;
                targetFocusZ = 2;
            } else {
                interactionScale = CONFIG.dimScale;
                interactionOpacity = CONFIG.dimOpacity;
                targetTextOpacity = 0;
                targetFocusZ = -0.5;
                // Track that this item was dimmed
                wasDimmedByFocus.current = true;
            }
        } else {
            interactionScale =
                isHovered && !rigState.isDragging ? 1.05 : 1.0;
            targetFocusZ =
                isHovered && !rigState.isDragging ? 0.5 : 0;
        }
        const finalOpacity =
            interactionOpacity *
            targetTransitionOpacity *
            filterOpacity.current;
        const combinedScale =
            interactionScale * filterScale.current;
        // --- 7. Apply Animations ---
        easing.damp(
            ref.current.scale,
            "x",
            combinedScale,
            0.15,
            delta
        );
        easing.damp(
            ref.current.scale,
            "y",
            combinedScale,
            0.15,
            delta
        );
        easing.damp(
            focusZ,
            "current",
            targetFocusZ,
            0.2,
            delta
        );
        easing.damp(
            curveZ,
            "current",
            targetCurveZ,
            0.2,
            delta
        );
        easing.damp(
            transitionZ,
            "current",
            targetTransitionZ,
            CONFIG.transitionZDamp,
            delta
        );
        easing.damp(
            transitionY,
            "current",
            targetTransitionY,
            CONFIG.transitionYDamp,
            delta
        );
        ref.current.position.set(
            x,
            y + transitionY.current,
            curveZ.current + focusZ.current + transitionZ.current
        );
        easing.damp(rotationX, "current", rotX, 0.2, delta);
        easing.damp(rotationY, "current", rotY, 0.2, delta);
        ref.current.rotation.set(
            rotationX.current,
            rotationY.current,
            0
        );
        if (imageRef.current) {
            // Update shader uniforms
            imageRef.current.material.uTime =
                state.clock.elapsedTime;
            // Smoothly animate active state for shader effects
            const isActive = rigState.activeId === index;
            const activeDamp = isActive ? 0.6 : 0.15; // Slow open, fast close
            easing.damp(
                imageRef.current.material,
                "uActive",
                isActive ? 1 : 0,
                activeDamp,
                delta
            );
            // Animate opacity via shader uniform
            // Use faster damp for filter transitions and focus recovery, slower for grid enter/exit
            let opacityDamp;
            const isFilterTransition = !matchesFilter || filterOpacity.current < 0.99;
            // Focus recovery: item was dimmed and is now recovering
            const isFocusRecovery = !isFocusMode && wasDimmedByFocus.current;
            if (isFilterTransition && gridVisible) {
                // Filtering in or out - use filter damp for faster fade
                opacityDamp = CONFIG.filterOpacityDamp;
            } else if (isFocusRecovery && gridVisible) {
                // Recovering from dimmed state after deselection - use faster damp
                opacityDamp = CONFIG.filterOpacityDamp;
                // Reset flag once opacity is recovered
                if (imageRef.current.material.uOpacity > 0.95) {
                    wasDimmedByFocus.current = false;
                }
            } else if (gridVisible) {
                opacityDamp = CONFIG.enterOpacityDamp;
            } else {
                opacityDamp = CONFIG.exitOpacityDamp;
            }
            easing.damp(
                imageRef.current.material,
                "uOpacity",
                finalOpacity,
                opacityDamp,
                delta
            );
        }
        // Only update text opacity if text is actually rendered
        if (gridVisible) {
            const textTarget =
                targetTransitionOpacity < 0.8
                    ? 0
                    : targetTextOpacity;
            if (titleRef.current)
                easing.damp(
                    titleRef.current,
                    "fillOpacity",
                    textTarget,
                    0.1,
                    delta
                );
            if (priceRef.current)
                easing.damp(
                    priceRef.current,
                    "fillOpacity",
                    textTarget,
                    0.1,
                    delta
                );
            // Breathing animation for text when active
            const isActiveItem = rigState.activeId === index;
            const targetBreath = isActiveItem
                ? 1 +
                Math.sin(state.clock.elapsedTime * 2.0) * 0.035
                : 1;
            easing.damp(
                breathScale,
                "current",
                targetBreath,
                0.1,
                delta
            );
            // Apply breathing scale to text
            if (titleRef.current) {
                titleRef.current.scale.setScalar(
                    breathScale.current
                );
            }
            if (priceRef.current) {
                priceRef.current.scale.setScalar(
                    breathScale.current
                );
            }
        }
    });

    const handleClick = (e) => {
        if (!interactive) return;
        if (rigState.isDragging) {
            e.stopPropagation();
            return;
        }
        e.stopPropagation();
        if (rigState.activeId === index) {
            rigState.activeId = null;
        } else {
            const isZoomedOut = rigState.zoom > CONFIG.zoomIn + 2;
            rigState.target.set(-basePos.x, -basePos.y, 0);
            if (isZoomedOut) {
                rigState.activeId = index;
                rigState.zoom = CONFIG.zoomIn;
            } else {
                rigState.activeId = index;
            }
        }
    };
    const textY = -(imageDims.height / 2) - 0.25;
    const isActive = rigState.activeId === index;
    return (
        <group ref={ref}>
            <mesh
                onPointerOver={() => setHovered(true)}
                onPointerOut={() => setHovered(false)}
                onClick={handleClick}
            >
                <planeGeometry
                    args={[
                        imageDims.width * 1.1,
                        imageDims.height * 1.1,
                    ]}
                />
                <meshBasicMaterial visible={false} />
            </mesh>
            <mesh ref={imageRef}>
                <planeGeometry
                    args={[imageDims.width, imageDims.height, 16, 16]}
                />
                <holoCardMaterial
                    transparent={true}
                    uTexture={texture}
                />
            </mesh>
            {/* OPTIMIZATION 3: CONDITIONAL TEXT RENDERING */}
            {/* Do NOT render text if the grid is exiting. Saves massive CPU overhead. */}
            {gridVisible && (
                <>
                    <Text
                        ref={titleRef}
                        position={[0, textY, 0.01]}
                        fontSize={0.1}
                        color="#000"
                        anchorY="top"
                        anchorX="center"
                        maxWidth={2.5}
                        fillOpacity={0}
                    >
                        {data.title}
                    </Text>
                    {data.price && (
                        <Text
                            ref={priceRef}
                            position={[0, textY - 0.22, 0.01]}
                            fontSize={0.09}
                            color="#555"
                            anchorY="top"
                            anchorX="center"
                            fillOpacity={0}
                        >
                            {data.price}
                        </Text>
                    )}
                </>
            )}
            <CloseButton
                isActive={isActive}
                position={[
                    imageDims.width / 2 - 0.15,
                    imageDims.height / 2 - 0.15,
                    0.02,
                ]}
                onClose={() => {
                    rigState.activeId = null;
                }}
            />
        </group>
    );
}
src/components/grid/gridConfig.js
파일 저장

// --- CONFIGURATION ---
// Default values - will be overridden by controls
export const DEFAULT_CONFIG = {
  gridCols: 8,
  itemSize: 2.5,
  gap: 0.4,

  // Physics
  dragSpeed: 2.2,
  dampFactor: 0.2,
  tiltFactor: 0.08,
  clickThreshold: 5,
  dragResistance: 0.25,

  // Camera / Zoom
  zoomIn: 12,
  zoomOut: 31, // Starting zoom level (configurable via Leva)
  zoomDamp: 0.25,

  // Visuals
  focusScale: 1.5,
  dimScale: 0.5,
  dimOpacity: 0.15,

  // 3D Curvature Effect
  curvatureStrength: 0.06, // How much it curves back
  rotationStrength: 0, // How much tiles rotate to face center

  // Culling
  cullDistance: 14,

  // Minimap
  mapWidth: 120,
  mapDotSize: 2,

  // Fog
  fogNear: 19,
  fogFar: 100,

  // Animation
  enterStartOpacity: 0.0,
  enterStartZ: -50,
  exitEndZ: 20,
  transitionZDamp: 0.25,
  enterOpacityDamp: 0.85,
  exitOpacityDamp: 0.15,
  enterStaggerDelay: 400,
  exitStaggerDelay: 300,
  cleanupTimeout: 700,
  exitSpreadY: 0.5, // How far top/bottom items spread vertically when exiting
  enterSpreadY: 1, // How far items start spread when entering (then settle to 0)
  transitionYDamp: 0.08, // Y spread animates faster than Z (lower = faster)
  filterOpacityDamp: 0.06, // Filter fade out (lower = faster, used when filtering within collection)
  filterScaleTarget: 0.5, // Scale target when filtered out (smaller = more dramatic shrink)

  // Tech Background
  bgColor: "#e0e0e0",
  bgOpacity: 0.4,
  bgSpeed: 0.05,
  bgScale: 3.0,
  bgLineThickness: 0.03,
};

// Create a ref to hold the current config so it can be updated
export let CONFIG = { ...DEFAULT_CONFIG };

// Initialize CONFIG with defaults
Object.assign(CONFIG, DEFAULT_CONFIG);
src/components/grid/gridState.js
파일 저장

import * as THREE from "three";
import { CONFIG } from "./gridConfig";

// --- GLOBAL STATE ---
export const rigState = {
    target: new THREE.Vector3(0, 2, 0),
    current: new THREE.Vector3(0, 2, 0),
    velocity: new THREE.Vector3(0, 0, 0),
    zoom: CONFIG.zoomOut,
    isDragging: false,
    activeId: null,
};

// --- HELPER: Grid Dimensions ---
export const calculateGridDimensions = (count) => {
    const rows = Math.ceil(count / CONFIG.gridCols);
    const spacing = CONFIG.itemSize + CONFIG.gap;
    return {
        width: CONFIG.gridCols * spacing,
        height: rows * spacing,
    };
};

// Stable empty array to avoid unnecessary re-renders
export const EMPTY_COLORS = [];

// --- HELPER: Check if item matches filter ---
export const matchesFilter = (item, filter, colorFilter = EMPTY_COLORS) => {
    // Check type filter (jordan/dunk)
    let matchesType = true;
    if (filter !== "all") {
        const title = item.title.toLowerCase();
        if (filter === "jordan") matchesType = title.includes("jordan");
        else if (filter === "dunk") matchesType = title.includes("dunk");
    }

    // Check color filter (array - OR logic across colors)
    let matchesColor = true;
    if (colorFilter.length > 0) {
        const shoeColor = item.primary_color || "";
        matchesColor = colorFilter.some((c) => {
            // Match gray variants (gray, dark_gray, light_gray) when "gray" is selected
            if (c === "gray") return shoeColor.includes("gray");
            return shoeColor === c;
        });
    }

    return matchesType && matchesColor;
};
src/components/grid/useGridConfig.js
파일 저장

import { useEffect } from "react";
import { useControls } from "leva";
import { DEFAULT_CONFIG, CONFIG } from "./gridConfig";

// Hook to set up Leva controls and sync them to CONFIG
export function useGridConfig() {
  // Controls for main config values
  const controls = useControls(
    "Shoe Grid",
    {
      curvatureStrength: {
        value: DEFAULT_CONFIG.curvatureStrength,
        min: 0,
        max: 0.2,
        step: 0.001,
        label: "Curvature Strength",
      },
      rotationStrength: {
        value: DEFAULT_CONFIG.rotationStrength,
        min: 0,
        max: 5,
        step: 0.1,
        label: "Rotation Strength",
      },
      focusScale: {
        value: DEFAULT_CONFIG.focusScale,
        min: 1,
        max: 3,
        step: 0.1,
        label: "Focus Scale",
      },
      dimScale: {
        value: DEFAULT_CONFIG.dimScale,
        min: 0,
        max: 1,
        step: 0.05,
        label: "Dim Scale",
      },
      dimOpacity: {
        value: DEFAULT_CONFIG.dimOpacity,
        min: 0,
        max: 1,
        step: 0.05,
        label: "Dim Opacity",
      },
      dragSpeed: {
        value: DEFAULT_CONFIG.dragSpeed,
        min: 0.1,
        max: 3,
        step: 0.1,
        label: "Drag Speed",
      },
      dampFactor: {
        value: DEFAULT_CONFIG.dampFactor,
        min: 0.05,
        max: 0.5,
        step: 0.05,
        label: "Damp Factor",
      },
      tiltFactor: {
        value: DEFAULT_CONFIG.tiltFactor,
        min: 0,
        max: 0.2,
        step: 0.01,
        label: "Tilt Factor",
      },
      zoomIn: {
        value: DEFAULT_CONFIG.zoomIn,
        min: 5,
        max: 30,
        step: 1,
        label: "Zoom In",
      },
      zoomDamp: {
        value: DEFAULT_CONFIG.zoomDamp,
        min: 0.05,
        max: 0.5,
        step: 0.05,
        label: "Zoom Damp",
      },
      zoomOut: {
        value: DEFAULT_CONFIG.zoomOut,
        min: 10,
        max: 100,
        step: 1,
        label: "Zoom Out",
      },
    },
    { collapsed: true, order: 0 }
  );
  // Animation controls

  const animationControls = useControls(
    "Transition",
    {
      enterStartOpacity: {
        value: DEFAULT_CONFIG.enterStartOpacity,
        min: 0,
        max: 1,
        step: 0.05,
        label: "Enter Start Opacity",
      },
      enterStartZ: {
        value: DEFAULT_CONFIG.enterStartZ,
        min: -50,
        max: 0,
        step: 1,
        label: "Enter Start Z",
      },
      exitEndZ: {
        value: DEFAULT_CONFIG.exitEndZ,
        min: 0,
        max: 50,
        step: 1,
        label: "Exit End Z",
      },
      transitionZDamp: {
        value: DEFAULT_CONFIG.transitionZDamp,
        min: 0.05,
        max: 1,
        step: 0.05,
        label: "Transition Z Damp",
      },
      enterOpacityDamp: {
        value: DEFAULT_CONFIG.enterOpacityDamp,
        min: 0.05,
        max: 1,
        step: 0.05,
        label: "Enter Opacity Damp",
      },
      exitOpacityDamp: {
        value: DEFAULT_CONFIG.exitOpacityDamp,
        min: 0.05,
        max: 1,
        step: 0.05,
        label: "Exit Opacity Damp",
      },
      enterStaggerDelay: {
        value: DEFAULT_CONFIG.enterStaggerDelay,
        min: 0,
        max: 2000,
        step: 50,
        label: "Enter Stagger Delay (ms)",
      },
      exitStaggerDelay: {
        value: DEFAULT_CONFIG.exitStaggerDelay,
        min: 0,
        max: 1000,
        step: 50,
        label: "Exit Stagger Delay (ms)",
      },
      cleanupTimeout: {
        value: DEFAULT_CONFIG.cleanupTimeout,
        min: 500,
        max: 3000,
        step: 100,
        label: "Cleanup Timeout (ms)",
      },
      exitSpreadY: {
        value: DEFAULT_CONFIG.exitSpreadY,
        min: 0,
        max: 10,
        step: 0.5,
        label: "Exit Spread Y",
      },
      enterSpreadY: {
        value: DEFAULT_CONFIG.enterSpreadY,
        min: 0,
        max: 10,
        step: 0.5,
        label: "Enter Spread Y",
      },
      transitionYDamp: {
        value: DEFAULT_CONFIG.transitionYDamp,
        min: 0.01,
        max: 0.5,
        step: 0.01,
        label: "Y Damp (lower=faster)",
      },
      filterOpacityDamp: {
        value: DEFAULT_CONFIG.filterOpacityDamp,
        min: 0.01,
        max: 0.3,
        step: 0.01,
        label: "Filter Opacity Damp",
      },
    },
    { collapsed: true, order: 1 }
  );

  // TopologyBackground controls
  const techBgControls = useControls(
    "BG Shader",
    {
      bgColor: {
        value: "#e0e0e0",
        label: "Color",
      },
      bgOpacity: {
        value: 0.4,
        min: 0,
        max: 1,
        step: 0.05,
        label: "Opacity",
      },
      bgSpeed: {
        value: 0.05,
        min: 0,
        max: 0.2,
        step: 0.01,
        label: "Speed",
      },
      bgScale: {
        value: 3.0,
        min: 1,
        max: 10,
        step: 0.5,
        label: "Scale",
      },
      bgLineThickness: {
        value: 0.03,
        min: 0.01,
        max: 0.1,
        step: 0.01,
        label: "Thickness",
      },
    },
    { collapsed: true, order: 2 }
  );

  // Update CONFIG object when controls change
  useEffect(() => {
    if (controls) {
      CONFIG.curvatureStrength =
        controls.curvatureStrength ??
        DEFAULT_CONFIG.curvatureStrength;
      CONFIG.rotationStrength =
        controls.rotationStrength ??
        DEFAULT_CONFIG.rotationStrength;
      CONFIG.fogNear =
        controls.fogNear ?? DEFAULT_CONFIG.fogNear;
      CONFIG.fogFar =
        controls.fogFar ?? DEFAULT_CONFIG.fogFar;
      CONFIG.focusScale =
        controls.focusScale ?? DEFAULT_CONFIG.focusScale;
      CONFIG.dimScale =
        controls.dimScale ?? DEFAULT_CONFIG.dimScale;
      CONFIG.dimOpacity =
        controls.dimOpacity ?? DEFAULT_CONFIG.dimOpacity;
      CONFIG.dragSpeed =
        controls.dragSpeed ?? DEFAULT_CONFIG.dragSpeed;
      CONFIG.dampFactor =
        controls.dampFactor ?? DEFAULT_CONFIG.dampFactor;
      CONFIG.tiltFactor =
        controls.tiltFactor ?? DEFAULT_CONFIG.tiltFactor;
      CONFIG.zoomIn =
        controls.zoomIn ?? DEFAULT_CONFIG.zoomIn;
      CONFIG.zoomDamp =
        controls.zoomDamp ?? DEFAULT_CONFIG.zoomDamp;
      CONFIG.zoomOut =
        controls.zoomOut ?? DEFAULT_CONFIG.zoomOut;
    }
    if (animationControls) {
      CONFIG.enterStartOpacity =
        animationControls.enterStartOpacity ??
        DEFAULT_CONFIG.enterStartOpacity;
      CONFIG.enterStartZ =
        animationControls.enterStartZ ??
        DEFAULT_CONFIG.enterStartZ;
      CONFIG.exitEndZ =
        animationControls.exitEndZ ??
        DEFAULT_CONFIG.exitEndZ;
      CONFIG.transitionZDamp =
        animationControls.transitionZDamp ??
        DEFAULT_CONFIG.transitionZDamp;
      CONFIG.enterOpacityDamp =
        animationControls.enterOpacityDamp ??
        DEFAULT_CONFIG.enterOpacityDamp;
      CONFIG.exitOpacityDamp =
        animationControls.exitOpacityDamp ??
        DEFAULT_CONFIG.exitOpacityDamp;
      CONFIG.enterStaggerDelay =
        animationControls.enterStaggerDelay ??
        DEFAULT_CONFIG.enterStaggerDelay;
      CONFIG.exitStaggerDelay =
        animationControls.exitStaggerDelay ??
        DEFAULT_CONFIG.exitStaggerDelay;
      CONFIG.cleanupTimeout =
        animationControls.cleanupTimeout ??
        DEFAULT_CONFIG.cleanupTimeout;
      CONFIG.exitSpreadY =
        animationControls.exitSpreadY ??
        DEFAULT_CONFIG.exitSpreadY;
      CONFIG.enterSpreadY =
        animationControls.enterSpreadY ??
        DEFAULT_CONFIG.enterSpreadY;
      CONFIG.transitionYDamp =
        animationControls.transitionYDamp ??
        DEFAULT_CONFIG.transitionYDamp;
      CONFIG.filterOpacityDamp =
        animationControls.filterOpacityDamp ??
        DEFAULT_CONFIG.filterOpacityDamp;
    }
    if (techBgControls) {
      CONFIG.bgColor = techBgControls.bgColor ?? "#e0e0e0";
      CONFIG.bgOpacity = techBgControls.bgOpacity ?? 0.4;
      CONFIG.bgSpeed = techBgControls.bgSpeed ?? 0.05;
      CONFIG.bgScale = techBgControls.bgScale ?? 3.0;
      CONFIG.bgLineThickness =
        techBgControls.bgLineThickness ?? 0.03;
    }
  }, [controls, animationControls, techBgControls]);

  return controls;
}
src/pages/_app.js
파일 저장

import "@/styles/globals.css";
import { Inter } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });

export default function App({ Component, pageProps }) {
  return (
    <main className={inter.className}>
      <Component {...pageProps} />
    </main>
  );
}
src/pages/_document.js
파일 저장

import { Html, Head, Main, NextScript } from "next/document";

export default function Document() {
  return (
    <Html lang="en">
      <Head />
      <body>
        <Main />
        <NextScript />
      </body>
    </Html>
  );
}
src/pages/index.js
파일 저장

import Head from "next/head";
import dynamic from "next/dynamic";

// Dynamic import to avoid SSR issues with R3F
const ShoeGrid = dynamic(() => import("@/components/grid/ShoeGrid"), {
  ssr: false,
});

export default function HomePage() {
  return (
    <>
      <Head>
        <title>Shoe Finder</title>
        <meta name="description" content="Explore our endless collection of sneakers" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="icon" href="/favicon.ico" />
      </Head>
      <ShoeGrid />
    </>
  );
}
src/shaders/holocard.frag
파일 저장

uniform sampler2D uTexture;
uniform float uOpacity;
uniform float uActive;
varying vec2 vUv;

void main() {
  vec4 texColor = texture2D(uTexture, vUv);
  vec3 baseColor = texColor.rgb;

  if (uActive < 0.01) {
    gl_FragColor = vec4(baseColor, texColor.a * uOpacity);
    return;
  }

  // Light band sweep - driven by uActive transition (0->1)
  float diagonal = (vUv.x * 0.8) + vUv.y;
  float sheenPos = uActive * 2.5;
  float sheenWidth = 0.5;

  float dist = abs(diagonal - sheenPos);
  float intensity = 1.0 - smoothstep(0.0, sheenWidth, dist);
  intensity = pow(intensity, 3.0);

  // Fade out the sheen as uActive approaches 1 (sweep complete)
  float sheenFade = 1.0 - smoothstep(0.7, 1.0, uActive);

  vec3 sheenColor = vec3(0.85, 0.92, 1.0) * intensity * 0.9 * sheenFade;
  vec3 finalColor = baseColor + sheenColor * texColor.a;

  gl_FragColor = vec4(finalColor, texColor.a * uOpacity);
}
src/shaders/holocard.vert
파일 저장

varying vec2 vUv;
uniform float uTime;
uniform float uActive;

void main() {
  vUv = uv;
  vec3 pos = position;

  // "Breathing" - only when active
  float breath = sin(uTime * 2.0) * 0.015 * uActive;
  float scale = 1.0 + breath;

  gl_Position = projectionMatrix * modelViewMatrix * vec4(pos * scale, 1.0);
}
src/shaders/topography.frag
파일 저장

#pragma glslify: snoise = require('glsl-noise/simplex/2d')

uniform float uTime;
uniform vec3 uColor;
uniform vec2 uResolution;
uniform float uOpacity;
uniform float uLineOpacity;
uniform float uScale;
uniform float uLineThickness;
varying vec2 vUv;

void main() {
  vec2 uv = vUv;

  // Correct aspect ratio based on the plane dimensions
  float aspect = uResolution.x / uResolution.y;
  vec2 noiseUv = uv;
  noiseUv.x *= aspect;

  // Circle mask (aspect-corrected to stay perfectly round)
  vec2 centeredUv = uv - 0.5;
  centeredUv.x *= aspect;
  float dist = length(centeredUv);
  float radius = 0.6;
  float mask = 1.0 - smoothstep(radius - 0.01, radius + 0.01, dist);

  // Noise Generation
  float n = snoise(noiseUv * uScale + uTime * 0.05);

  // Isolines
  float lines = fract(n * 5.0);
  float pattern = smoothstep(0.5 - uLineThickness, 0.5, lines) - smoothstep(0.5, 0.5 + uLineThickness, lines);

  // Opacity
  float opacity = uLineOpacity;

  // Grain
  float grain = (fract(sin(dot(vUv, vec2(12.9898, 78.233) * 2.0)) * 43758.5453) - 0.5) * 0.15;

  vec3 finalColor = uColor + grain;

  gl_FragColor = vec4(finalColor, pattern * opacity * mask * uOpacity);
}
src/shaders/topography.vert
파일 저장

varying vec2 vUv;

void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
src/styles/globals.css
파일 저장

@import "tailwindcss";

@theme {
  --animate-accordion-down: accordion-down 0.2s ease-out;
  --animate-accordion-up: accordion-up 0.2s ease-out;
}

@keyframes accordion-down {
  from {
    height: 0;
  }
  to {
    height: var(--radix-accordion-content-height);
  }
}

@keyframes accordion-up {
  from {
    height: var(--radix-accordion-content-height);
  }
  to {
    height: 0;
  }
}

/* Dialog animations */
@keyframes dialog-overlay-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes dialog-content-in {
  from {
    opacity: 0;
    transform:  scale(0.95);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

.dialog-overlay {
  animation: dialog-overlay-in 0.15s ease-out;
}

.dialog-content {
  animation: dialog-content-in 0.15s ease-out;
}

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

html,
body {
  max-width: 100vw;
  overflow-x: hidden;
}

body {
  color: var(--foreground);
  background: var(--background);
  font-family: Arial, Helvetica, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

a {
  color: inherit;
  text-decoration: none;
}

@media (prefers-color-scheme: dark) {
  html {
    color-scheme: dark;
  }
}
Original author attribution실행 안내·자료
파일 저장

From Flat to Spatial: Creating a 3D Product Grid with React Three Fiber
Original author: Matt Greenberg
Published by Codrops. Copyright (c) 2026 Codrops.
License: MIT, pursuant to the publisher's downloadable-demo grant.
Keep CODROPS-MIT-2026.txt and the original project notices with copies.
Third-party media exceptions and credits are recorded separately in the source inventory.
external/troika-default/sans-serif.normal.400.woff — OFL-1.1실행 안내·자료

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

external/troika-default/OFL.txt — OFL-1.1실행 안내·자료

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

Bundled dependency licenses실행 안내·자료
파일 저장

react@19.2.3 — LICENSE
MIT License

Copyright (c) Meta Platforms, Inc. and affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


scheduler@0.27.0 — LICENSE
MIT License

Copyright (c) Meta Platforms, Inc. and affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


react-dom@19.2.3 — LICENSE
MIT License

Copyright (c) Meta Platforms, Inc. and affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


three@0.182.0 — LICENSE
The MIT License

Copyright © 2010-2025 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


use-sync-external-store@1.7.0 — LICENSE
MIT License

Copyright (c) Meta Platforms, Inc. and affiliates.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


zustand@5.0.15 — LICENSE
MIT License

Copyright (c) 2019 Paul Henschel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


suspend-react@0.1.3 — LICENSE
MIT License

Copyright (c) 2021 Paul Henschel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


its-fine@2.0.0 — LICENSE
MIT License

Copyright (c) 2022-2025 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


react-use-measure@2.1.7 — LICENSE
MIT License

Copyright (c) 2019-2025 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@babel/runtime@7.29.7 — LICENSE
MIT License

Copyright (c) 2014-present Sebastian McKenzie and other contributors

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


@react-three/drei@10.7.7 — LICENSE
MIT License

Copyright (c) 2020 react-spring

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@use-gesture/core@10.3.1 — LICENSE
Copyright (c) 2018-present Paul Henschel <drcmda@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@use-gesture/react@10.3.1 — LICENSE
Copyright (c) 2018-present Paul Henschel <drcmda@gmail.com>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

three-stdlib@2.36.1 — LICENSE
MIT License

Copyright (c) 2021-2023 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


potpack@1.0.2 — LICENSE
ISC License

Copyright (c) 2018, Mapbox

Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.


fflate@0.6.11 — LICENSE
MIT License

Copyright (c) 2020 Arjun Barrett

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

troika-worker-utils@0.52.0 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


webgl-sdf-generator@1.1.1 — LICENSE.txt
Copyright (c) 2021 Jason Johnston

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


bidi-js@1.1.0 — LICENSE.txt
Copyright (c) 2021 Jason Johnston

MIT License

Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:

The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


troika-three-utils@0.52.5 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


troika-three-text@0.52.5 — LICENSE
MIT License

Copyright (c) 2019 ProtectWise
Copyright (c) 2021 Jason Johnston

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


meshline@3.3.1 — LICENSE
MIT License

Copyright (c) 2016 Jaume Sanchez

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


camera-controls@3.1.2 — LICENSE
MIT License

Copyright (c) 2017 @yomotsu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


hls.js@1.7.3 — LICENSE
Copyright (c) 2017 Dailymotion (http://www.dailymotion.com)

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

src/remux/mp4-generator.js and src/demux/exp-golomb.ts implementation in this project
are derived from the HLS library for video.js (https://github.com/videojs/videojs-contrib-hls)

That work is also covered by the Apache 2 License, following copyright:
Copyright (c) 2013-2015 Brightcove


THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


stats.js@0.17.0 — LICENSE
The MIT License

Copyright (c) 2009-2016 stats.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


detect-gpu@5.0.70 — LICENSE
MIT License

Copyright (c) 2020 Tim van Scherpenzeel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


three-mesh-bvh@0.8.3 — LICENSE
MIT License

Copyright (c) 2018 Garrett Johnson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@monogrid/gainmap-js@3.4.0 — LICENSE
MIT License

Copyright (c) 2023 MONOGRID

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


zustand@4.5.7 — LICENSE
MIT License

Copyright (c) 2019 Paul Henschel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


tunnel-rat@0.1.2 — LICENSE
MIT License

Copyright (c) 2022 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-compose-refs@1.1.5 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-slot@1.3.3 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-primitive@2.1.10 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-use-layout-effect@1.1.4 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-portal@1.1.17 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


dequal@2.0.3 — license
The MIT License (MIT)

Copyright (c) Luke Edwards <luke.edwards05@gmail.com> (lukeed.com)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


zustand@3.7.2 — LICENSE
MIT License

Copyright (c) 2019 Paul Henschel

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


v8n@1.5.1 — LICENSE
MIT License

Copyright (c) 2018 Bruno C. Couto

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/primitive@1.1.7 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-context@1.2.2 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-use-callback-ref@1.1.4 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-dismissable-layer@1.1.19 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-id@1.1.4 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@floating-ui/utils@0.2.12 — LICENSE
MIT License

Copyright (c) 2021-present Floating UI contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


@floating-ui/core@1.8.0 — LICENSE
MIT License

Copyright (c) 2021-present Floating UI contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


@floating-ui/dom@1.8.0 — LICENSE
MIT License

Copyright (c) 2021-present Floating UI contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


@floating-ui/react-dom@2.1.9 — LICENSE
MIT License

Copyright (c) 2021-present Floating UI contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


@radix-ui/react-arrow@1.1.15 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-use-size@1.1.4 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-popper@1.3.7 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-presence@1.1.10 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-use-effect-event@0.0.5 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-use-controllable-state@1.2.6 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-visually-hidden@1.2.11 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


@radix-ui/react-tooltip@1.2.16 — LICENSE
MIT License

Copyright (c) 2022 WorkOS

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


leva@0.10.1 — LICENSE
MIT License

Copyright (c) 2021 Poimandres

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


colord@2.10.0 — LICENSE.md
MIT License

Copyright (c) 2020 Vlad Shilov omgovich@ya.ru

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


react-colorful@5.8.1 — LICENSE
MIT License

Copyright (c) 2020-present Vlad Shilov <omgovich@ya.ru>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


prop-types@15.8.1 — LICENSE
MIT License

Copyright (c) 2013-present, Facebook, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


tslib@2.8.1 — LICENSE.txt
Copyright (c) Microsoft Corporation.

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.

file-selector@0.5.0 — LICENSE
MIT License

Copyright (c) 2020 Roland Groza

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


attr-accept@2.2.5 — LICENSE
The MIT License (MIT)

Copyright (c) 2015 Andrey Okonetchnikov

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



react-dropzone@12.1.0 — LICENSE
The MIT License (MIT)

Copyright (c) 2018 Param Aggarwal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.



isobject@3.0.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

is-plain-object@2.0.4 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


is-extendable@1.0.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2015-2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

for-in@1.0.2 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2017, Jon Schlinkert

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


mixin-deep@1.3.2 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2015, 2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


get-value@2.0.6 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2016, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


assign-symbols@1.0.0 — LICENSE
The MIT License (MIT)

Copyright (c) 2015, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


extend-shallow@3.0.2 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2015, 2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


split-string@3.1.0 — LICENSE
The MIT License (MIT)

Copyright (c) 2015-2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


is-extendable@0.1.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2015, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


extend-shallow@2.0.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2015, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


is-extendable@0.1.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2015, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


set-value@2.0.1 — LICENSE
The MIT License (MIT)

Copyright (c) 2014-2017, Jon Schlinkert

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


merge-value@1.0.0 — LICENSE
The MIT License (MIT)

Copyright (c) 2015-2017, Jon Schlinkert.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.


framer-motion@12.25.0 — LICENSE.md
The MIT License (MIT)

Copyright (c) 2018 Framer B.V.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


motion-utils@12.24.10 — LICENSE.md
The MIT License (MIT)

Copyright (c) 2024 [Motion](https://motion.dev) B.V.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


motion-dom@12.24.11 — LICENSE.md
The MIT License (MIT)

Copyright (c) 2024 [Motion](https://motion.dev) B.V.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.