import html2canvas from 'html2canvas'
import { motion } from 'framer-motion'
import { useEffect, useRef, useState } from 'react'
import * as THREE from 'three'
import type { CSSProperties, RefObject } from 'react'
import { StudioLogo } from './StudioLogo'
import type { IntroPhase } from '../types'

type PercentPoint = {
  x: number
  y: number
}

type CrackSegment = {
  d: string
  cls: string
  delay: string
}

type ShardSpec = {
  delayMs: number
  points: PercentPoint[]
  centroid: PercentPoint
  rotationDeg: number
  spinXDeg: number
  spinYDeg: number
  spinZDeg: number
  tiltXDeg: number
  tiltYDeg: number
  translateXPct: number
  translateYPct: number
}

type ClosingStage = 'crack' | 'shatter'

const SHATTER_SNAPSHOT_SCALE = 1.25
const SHATTER_RENDERER_PIXEL_RATIO = 1.25
const SHARD_DURATION_MS = 1720
const CRACK_GROWTH_DURATION_MS = 600
const SNAPSHOT_CAPTURE_LEAD_MS = 10
const pt = (x: number, y: number): PercentPoint => ({ x, y })

function IntroBlocks() {
  const blocks = [
    { w: '18vw', h: '2px', x: '-20vw', y: '28%', delay: '0s', dur: '3.2s', dir: 'h' },
    { w: '2px', h: '22vh', x: '62%', y: '-25vh', delay: '0.2s', dur: '2.8s', dir: 'v' },
    { w: '12vw', h: '2px', x: '105vw', y: '54%', delay: '0.4s', dur: '3.5s', dir: 'h-r' },
    { w: '2px', h: '16vh', x: '38%', y: '110vh', delay: '0.6s', dur: '3.0s', dir: 'v-b' },
    { w: '8vw', h: '1px', x: '-10vw', y: '72%', delay: '0.8s', dur: '2.6s', dir: 'h' },
    { w: '1px', h: '30vh', x: '78%', y: '-35vh', delay: '1.0s', dur: '3.8s', dir: 'v' },
    { w: '22vw', h: '1px', x: '105vw', y: '18%', delay: '0.3s', dur: '2.9s', dir: 'h-r' },
    { w: '1px', h: '12vh', x: '22%', y: '110vh', delay: '0.7s', dur: '3.3s', dir: 'v-b' },
    { w: '6vw', h: '2px', x: '-8vw', y: '88%', delay: '1.2s', dur: '2.4s', dir: 'h' },
    { w: '2px', h: '8vh', x: '88%', y: '-12vh', delay: '0.5s', dur: '3.6s', dir: 'v' },
  ]

  return (
    <div className="intro-blocks" aria-hidden="true">
      {blocks.map((b, i) => (
        <span
          key={i}
          className={`intro-block intro-block-${b.dir}`}
          style={({
            width: b.w,
            height: b.h,
            left: b.x,
            top: b.y,
            animationDelay: b.delay,
            animationDuration: b.dur,
          }) as CSSProperties}
        />
      ))}
    </div>
  )
}

function IntroSvgLines() {
  return (
    <svg
      className="intro-svg-lines"
      viewBox="0 0 1000 600"
      preserveAspectRatio="xMidYMid slice"
      aria-hidden="true"
    >
      {[0, 22, 45, 67, 90, 112, 135, 157, 180, 202, 225, 247, 270, 292, 315, 337].map((angle, i) => {
        const rad = (angle * Math.PI) / 180
        const x2 = 500 + Math.cos(rad) * 600
        const y2 = 300 + Math.sin(rad) * 600
        return (
          <line
            key={i}
            x1="500"
            y1="300"
            x2={x2}
            y2={y2}
            className="svg-radial-ray"
            style={{ '--ray-i': i, '--ray-delay': `${i * 0.08}s` } as CSSProperties}
          />
        )
      })}
      <ellipse cx="500" cy="300" rx="280" ry="80" className="svg-orbit svg-orbit-1" />
      <ellipse cx="500" cy="300" rx="200" ry="55" className="svg-orbit svg-orbit-2" />
      <ellipse cx="500" cy="300" rx="380" ry="110" className="svg-orbit svg-orbit-3" />
      <defs>
        <linearGradient id="scanBeam" x1="0" y1="0" x2="1" y2="0">
          <stop offset="0%" stopColor="rgba(77,124,255,0)" />
          <stop offset="50%" stopColor="rgba(77,124,255,0.18)" />
          <stop offset="100%" stopColor="rgba(77,124,255,0)" />
        </linearGradient>
        <path id="orbit-path-1" d="M 780 300 A 280 80 0 1 0 779.9 300" />
        <path id="orbit-path-2" d="M 700 300 A 200 55 0 1 1 699.9 300" />
      </defs>
      <>
        <rect x="0" y="0" width="1000" height="3" fill="url(#scanBeam)" className="svg-h-scan" />
        <rect x="0" y="0" width="1000" height="1" fill="rgba(232,234,240,0.25)" className="svg-h-scan svg-h-scan-sharp" />
      </>
      <path d="M 0 600 Q 500 0 1000 600" className="svg-flow-path svg-flow-1" />
      <path d="M 0 0 Q 500 600 1000 0" className="svg-flow-path svg-flow-2" />
      <line x1="500" y1="200" x2="500" y2="400" className="svg-cross-v" />
      <line x1="400" y1="300" x2="600" y2="300" className="svg-cross-h" />
      <>
        <circle r="3" className="svg-orbit-dot svg-orbit-dot-1">
          <animateMotion dur="3s" repeatCount="indefinite">
            <mpath href="#orbit-path-1" />
          </animateMotion>
        </circle>
        <circle r="2" className="svg-orbit-dot svg-orbit-dot-2">
          <animateMotion dur="4.5s" repeatCount="indefinite" begin="-1.5s">
            <mpath href="#orbit-path-2" />
          </animateMotion>
        </circle>
      </>
    </svg>
  )
}

const crackCenter = pt(50, 52)
const SLASH_DIRECTION = (() => {
  const raw = { x: 0.82, y: 0.57 }
  const length = Math.hypot(raw.x, raw.y)
  return { x: raw.x / length, y: raw.y / length }
})()
const SLASH_NORMAL = {
  x: -SLASH_DIRECTION.y,
  y: SLASH_DIRECTION.x,
}

function averagePoint(points: PercentPoint[]) {
  const total = points.reduce(
    (acc, point) => ({ x: acc.x + point.x, y: acc.y + point.y }),
    { x: 0, y: 0 },
  )
  return {
    x: total.x / points.length,
    y: total.y / points.length,
  }
}

const crackViewport = {
  width: 1000,
  height: 600,
} as const

function clamp(value: number, min: number, max: number) {
  return Math.max(min, Math.min(max, value))
}

function fract(value: number) {
  return value - Math.floor(value)
}

function pseudoNoise(seed: number) {
  return fract(Math.sin(seed * 127.1 + seed * 0.13) * 43758.5453123)
}

function normalizePercentPoint(point: PercentPoint) {
  const x = clamp(point.x, 0, 100)
  const y = clamp(point.y, 0, 100)
  return {
    x: sameValue(x, 0) ? 0 : sameValue(x, 100) ? 100 : x,
    y: sameValue(y, 0) ? 0 : sameValue(y, 100) ? 100 : y,
  }
}

function sameValue(a: number, b: number) {
  return Math.abs(a - b) < 0.001
}

function isBorderEdge(a: PercentPoint, b: PercentPoint) {
  return (
    (sameValue(a.x, 0) && sameValue(b.x, 0)) ||
    (sameValue(a.x, 100) && sameValue(b.x, 100)) ||
    (sameValue(a.y, 0) && sameValue(b.y, 0)) ||
    (sameValue(a.y, 100) && sameValue(b.y, 100))
  )
}

function distance(a: PercentPoint, b: PercentPoint) {
  return Math.hypot(a.x - b.x, a.y - b.y)
}

function dot(a: PercentPoint, b: PercentPoint) {
  return a.x * b.x + a.y * b.y
}

function polygonArea(points: PercentPoint[]) {
  let area = 0
  for (let i = 0; i < points.length; i += 1) {
    const current = points[i]
    const next = points[(i + 1) % points.length]
    area += current.x * next.y - next.x * current.y
  }
  return Math.abs(area) * 0.5
}

function polygonCentroid(points: PercentPoint[]) {
  let signedArea = 0
  let centroidX = 0
  let centroidY = 0

  for (let i = 0; i < points.length; i += 1) {
    const current = points[i]
    const next = points[(i + 1) % points.length]
    const cross = current.x * next.y - next.x * current.y
    signedArea += cross
    centroidX += (current.x + next.x) * cross
    centroidY += (current.y + next.y) * cross
  }

  if (Math.abs(signedArea) < 0.001) {
    return averagePoint(points)
  }

  return normalizePercentPoint({
    x: centroidX / (3 * signedArea),
    y: centroidY / (3 * signedArea),
  })
}

function collapseClosePoints(points: PercentPoint[], epsilon = 0.08) {
  const cleaned: PercentPoint[] = []

  points.forEach((point) => {
    const normalized = normalizePercentPoint(point)
    if (cleaned.length === 0 || distance(cleaned[cleaned.length - 1], normalized) > epsilon) {
      cleaned.push(normalized)
    }
  })

  if (cleaned.length > 1 && distance(cleaned[0], cleaned[cleaned.length - 1]) <= epsilon) {
    cleaned.pop()
  }

  return cleaned
}

function simplifyPolygon(points: PercentPoint[]) {
  let simplified = collapseClosePoints(points)
  let changed = true

  while (changed && simplified.length > 3) {
    changed = false
    const nextPass: PercentPoint[] = []

    for (let i = 0; i < simplified.length; i += 1) {
      const prev = simplified[(i - 1 + simplified.length) % simplified.length]
      const current = simplified[i]
      const next = simplified[(i + 1) % simplified.length]
      const cross = Math.abs(
        (current.x - prev.x) * (next.y - current.y) -
        (current.y - prev.y) * (next.x - current.x),
      )

      if (cross < 0.06 && distance(prev, next) > 0.24) {
        changed = true
        continue
      }

      nextPass.push(current)
    }

    simplified = collapseClosePoints(nextPass)
  }

  return simplified
}

function fromSlashSpace(along: number, across: number) {
  return normalizePercentPoint({
    x: crackCenter.x + SLASH_DIRECTION.x * along + SLASH_NORMAL.x * across,
    y: crackCenter.y + SLASH_DIRECTION.y * along + SLASH_NORMAL.y * across,
  })
}

function buildGlassSeedPoints() {
  const seeds: PercentPoint[] = [crackCenter]

  const lanes = [-28, -16, -6, 6, 16, 28]
  const steps = [-58, -38, -18, 2, 22, 42, 62]

  lanes.forEach((across, laneIndex) => {
    steps.forEach((along, stepIndex) => {
      if (laneIndex === 2 && stepIndex === 3) return

      const alongJitter = (pseudoNoise(100 + laneIndex * 20 + stepIndex) - 0.5) * 7.2
      const acrossJitter = (pseudoNoise(300 + laneIndex * 20 + stepIndex) - 0.5) * 5.6
      const taper = 1 - Math.min(0.35, Math.abs(across) / 120)

      seeds.push(fromSlashSpace(
        along + alongJitter * taper,
        across + acrossJitter * (0.72 + Math.abs(across) / 48),
      ))
    })
  })

  const flankLanes = [-38, 38]
  const flankSteps = [-44, -14, 16, 46]

  flankLanes.forEach((across, laneIndex) => {
    flankSteps.forEach((along, stepIndex) => {
      seeds.push(fromSlashSpace(
        along + (pseudoNoise(500 + laneIndex * 20 + stepIndex) - 0.5) * 6.2,
        across + (pseudoNoise(600 + laneIndex * 20 + stepIndex) - 0.5) * 6.6,
      ))
    })
  })

  const pushEdgeSeed = (x: number, y: number, seed: number, xJitter: number, yJitter: number) => {
    seeds.push(normalizePercentPoint({
      x: x + (pseudoNoise(seed) - 0.5) * xJitter,
      y: y + (pseudoNoise(seed + 17) - 0.5) * yJitter,
    }))
  }

  ;[12, 34, 58, 84].forEach((x, index) => pushEdgeSeed(x, 7.4, 400 + index, 3.4, 1.2))
  ;[16, 40, 64, 88].forEach((x, index) => pushEdgeSeed(x, 92.6, 500 + index, 3.4, 1.2))
  ;[18, 44, 72].forEach((y, index) => pushEdgeSeed(7.3, y, 600 + index, 1.2, 4))
  ;[22, 50, 78].forEach((y, index) => pushEdgeSeed(92.7, y, 700 + index, 1.2, 4))

  return seeds
}

function clipPolygonWithBisector(polygon: PercentPoint[], seed: PercentPoint, other: PercentPoint) {
  const clipped: PercentPoint[] = []
  const direction = {
    x: other.x - seed.x,
    y: other.y - seed.y,
  }
  const midpoint = {
    x: (seed.x + other.x) * 0.5,
    y: (seed.y + other.y) * 0.5,
  }

  const signedDistance = (point: PercentPoint) =>
    (point.x - midpoint.x) * direction.x + (point.y - midpoint.y) * direction.y

  for (let i = 0; i < polygon.length; i += 1) {
    const current = polygon[i]
    const next = polygon[(i + 1) % polygon.length]
    const currentDistance = signedDistance(current)
    const nextDistance = signedDistance(next)
    const currentInside = currentDistance <= 0.0001
    const nextInside = nextDistance <= 0.0001

    if (currentInside && nextInside) {
      clipped.push(next)
      continue
    }

    if (currentInside !== nextInside) {
      const t = currentDistance / (currentDistance - nextDistance)
      clipped.push(normalizePercentPoint({
        x: current.x + (next.x - current.x) * t,
        y: current.y + (next.y - current.y) * t,
      }))
    }

    if (!currentInside && nextInside) {
      clipped.push(next)
    }
  }

  return simplifyPolygon(clipped)
}

function buildGlassVoronoiCells() {
  const seeds = buildGlassSeedPoints()
  const bounds = [pt(0, 0), pt(100, 0), pt(100, 100), pt(0, 100)]

  return seeds.map((seed, index) => {
    let polygon = [...bounds]

    seeds.forEach((other, otherIndex) => {
      if (otherIndex === index || polygon.length < 3) return
      polygon = clipPolygonWithBisector(polygon, seed, other)
    })

    return simplifyPolygon(polygon)
  })
}

function edgeKey(a: PercentPoint, b: PercentPoint) {
  const first = normalizePercentPoint(a)
  const second = normalizePercentPoint(b)
  const ordered =
    first.x < second.x || (sameValue(first.x, second.x) && first.y <= second.y)
      ? [first, second]
      : [second, first]

  return `${ordered[0].x.toFixed(2)},${ordered[0].y.toFixed(2)}|${ordered[1].x.toFixed(2)},${ordered[1].y.toFixed(2)}`
}

function toSvgPoint(point: PercentPoint) {
  return {
    x: (point.x / 100) * crackViewport.width,
    y: (point.y / 100) * crackViewport.height,
  }
}

function buildJaggedCrackPath(
  start: PercentPoint,
  end: PercentPoint,
  jitterSeed: number,
  roughness = 1,
) {
  const dx = end.x - start.x
  const dy = end.y - start.y
  const length = Math.hypot(dx, dy)
  const nx = length === 0 ? 0 : -dy / length
  const ny = length === 0 ? 0 : dx / length
  const points = [start]
  const kinkSteps =
    roughness < 0.34
      ? [0.5]
      : length < 16
        ? [0.52]
        : [0.32, 0.68]
  const amplitude = (Math.min(1.8, 0.45 + length * 0.02)) * roughness

  kinkSteps.forEach((t, index) => {
    const base = {
      x: start.x + dx * t,
      y: start.y + dy * t,
    }
    const wave = (jitterSeed + index) % 2 === 0 ? 1 : -1
    const taper = index === 0 ? 1 : 0.72
    points.push({
      x: base.x + nx * amplitude * wave * taper,
      y: base.y + ny * amplitude * wave * taper,
    })
  })

  points.push(end)
  return points
    .map((point, index) => {
      const svgPoint = toSvgPoint(point)
      return `${index === 0 ? 'M' : 'L'}${svgPoint.x.toFixed(1)} ${svgPoint.y.toFixed(1)}`
    })
    .join(' ')
}

function createCrackSegment(start: PercentPoint, end: PercentPoint, order: number): CrackSegment | null {
  if (distance(start, end) < 4) return null

  const mid = {
    x: (start.x + end.x) / 2,
    y: (start.y + end.y) / 2,
  }

  const segment = { x: end.x - start.x, y: end.y - start.y }
  const segmentLength = Math.hypot(segment.x, segment.y)
  const segmentUnit =
    segmentLength === 0
      ? { x: 0, y: 0 }
      : { x: segment.x / segmentLength, y: segment.y / segmentLength }
  const centerOffset = { x: mid.x - crackCenter.x, y: mid.y - crackCenter.y }
  const slashDistance = Math.abs(dot(centerOffset, SLASH_NORMAL))
  const slashTravel = Math.abs(dot(centerOffset, SLASH_DIRECTION))
  const slashAlignment = Math.abs(dot(segmentUnit, SLASH_DIRECTION))
  const centerDistance = distance(mid, crackCenter)
  const isSlashCore = slashAlignment > 0.84 && slashDistance < 6.5
  const isSlashSupport = slashAlignment > 0.7 && slashDistance < 15
  const band = isSlashCore
    ? 'intro-crack-path-slash'
    : isSlashSupport
      ? 'intro-crack-path-main'
      : centerDistance < 26
        ? 'intro-crack-path-branch'
        : 'intro-crack-path-fray'
  const secondary = !isSlashCore && (centerDistance > 20 || slashAlignment < 0.52) ? ' intro-crack-path-secondary' : ''
  const roughness = isSlashCore ? 0.24 : isSlashSupport ? 0.42 : centerDistance < 26 ? 0.62 : 0.82
  const delay = 0.12 + Math.min((slashDistance * 0.7 + slashTravel * 0.18) / 60, 1) * 0.38 + order * 0.003
  return {
    d: buildJaggedCrackPath(start, end, order, roughness),
    cls: `${band}${secondary}`,
    delay: `${delay.toFixed(2)}s`,
  }
}

const shardPolygons = buildGlassVoronoiCells()

const crackSegments = (() => {
  const internalEdges = new Map<string, { a: PercentPoint; b: PercentPoint; count: number }>()

  shardPolygons.forEach((polygon) => {
    polygon.forEach((point, index) => {
      const next = polygon[(index + 1) % polygon.length]
      if (isBorderEdge(point, next)) return

      const key = edgeKey(point, next)
      const existing = internalEdges.get(key)
      if (existing) {
        existing.count += 1
      } else {
        internalEdges.set(key, { a: normalizePercentPoint(point), b: normalizePercentPoint(next), count: 1 })
      }
    })
  })

  return Array.from(internalEdges.values())
    .filter((edge) => edge.count > 1 && distance(edge.a, edge.b) > 2.8)
    .map((edge, index) => createCrackSegment(edge.a, edge.b, index))
    .filter((segment): segment is CrackSegment => segment !== null)
})()

const shardSpecs: ShardSpec[] = shardPolygons.map((points, index) => {
  const centroid = polygonCentroid(points)
  const area = polygonArea(points)
  const dx = (centroid.x - crackCenter.x) / 50
  const dy = (centroid.y - crackCenter.y) / 50
  const radial = Math.hypot(dx, dy)
  const spreadNoise = pseudoNoise(900 + index) - 0.5
  const delay = Math.max(0, radial * 0.068 - 0.015) + (area < 88 ? 0.026 : 0)
  const slashOffset = dot({ x: centroid.x - crackCenter.x, y: centroid.y - crackCenter.y }, SLASH_NORMAL)
  const slashAlong = dot({ x: centroid.x - crackCenter.x, y: centroid.y - crackCenter.y }, SLASH_DIRECTION)
  const slashPull = slashOffset / 18
  const directionalBurst = slashAlong > 0 ? 1.2 : 0.72
  const outwardScatter = (dx === 0 ? Math.sign(spreadNoise || 1) : Math.sign(dx)) * (6 + radial * 10)
  const lateral = slashPull * 12.5 + SLASH_DIRECTION.x * directionalBurst * 16 + outwardScatter + spreadNoise * 4.8
  const vertical = 102 + radial * 28 + directionalBurst * 10 + Math.max(0, 120 - area) * 0.14
  const slashDrift = SLASH_DIRECTION.y * directionalBurst * 28
  const diagonalLift = Math.abs(slashPull) * 10

  return {
    delayMs: delay * 1000,
    centroid,
    points,
    rotationDeg: slashPull * 1.6 + spreadNoise * 1.4,
    spinXDeg: spreadNoise * 46 + dx * 18,
    spinYDeg: slashPull * 44 + spreadNoise * 30,
    spinZDeg: slashPull * 22 + dx * 14,
    tiltXDeg: -dy * 9 + spreadNoise * 4,
    tiltYDeg: slashPull * 13 + dx * 7 + spreadNoise * 7,
    translateXPct: lateral,
    translateYPct: vertical + slashDrift - diagonalLift,
  }
})

function clamp01(value: number) {
  return Math.max(0, Math.min(1, value))
}

function lerp(from: number, to: number, t: number) {
  return from + (to - from) * t
}

function easeOutCubic(t: number) {
  return 1 - Math.pow(1 - t, 3)
}

function easeInCubic(t: number) {
  return t * t * t
}

function toScenePoint(point: PercentPoint, width: number, height: number) {
  return new THREE.Vector2(
    (point.x / 100) * width - width / 2,
    height / 2 - (point.y / 100) * height,
  )
}

function createShardMeshData(spec: ShardSpec, width: number, height: number) {
  const center = toScenePoint(spec.centroid, width, height)
  const shapePoints = spec.points.map((point) => {
    const worldPoint = toScenePoint(point, width, height)
    return new THREE.Vector2(worldPoint.x - center.x, worldPoint.y - center.y)
  })
  const shape = new THREE.Shape(shapePoints)
  const geometry = new THREE.ShapeGeometry(shape)
  const position = geometry.getAttribute('position')
  const uv = new Float32Array(position.count * 2)

  for (let i = 0; i < position.count; i += 1) {
    const x = position.getX(i) + center.x
    const y = position.getY(i) + center.y
    uv[i * 2] = (x + width / 2) / width
    uv[i * 2 + 1] = (y + height / 2) / height
  }

  geometry.setAttribute('uv', new THREE.BufferAttribute(uv, 2))
  const outlinePoints = spec.points.map((point) => {
    const worldPoint = toScenePoint(point, width, height)
    return new THREE.Vector3(worldPoint.x - center.x, worldPoint.y - center.y, 0.5)
  })

  return { center, geometry, outlinePoints }
}

function IntroCrackGrowth() {
  return (
    <div className="intro-crack-growth-wrap" aria-hidden="true">
      <svg className="intro-crack-growth" viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice">
        {crackSegments.map((segment, i) => (
          <path
            key={i}
            d={segment.d}
            pathLength={100}
            className={`intro-crack-path ${segment.cls}`}
            style={{ '--crack-delay': segment.delay } as CSSProperties}
          />
        ))}
      </svg>
    </div>
  )
}

function WordmarkLine({
  delay,
  word,
}: {
  delay: number
  word: string
}) {
  const trailLayers = [1, 2, 3, 4, 5, 6]

  return (
    <span
      className="intro-wordmark-line"
      style={{ '--wordmark-delay': `${delay}s` } as CSSProperties}
    >
      <span className="intro-wordmark-trail" aria-hidden="true">
        {trailLayers.map((layer) => (
          <span key={layer} className={`intro-wordmark-trail-layer intro-wordmark-trail-layer-${layer}`}>
            {word}
          </span>
        ))}
      </span>
      <span className="intro-wordmark-core">{word}</span>
    </span>
  )
}

function IntroSource({
  sourceRef,
  closing,
}: {
  sourceRef?: RefObject<HTMLDivElement | null>
  closing: boolean
}) {
  const words = ['No', 'Stay', 'Night', 'Studio'] as const

  return (
    <div ref={sourceRef} className={`intro-snapshot-source${closing ? ' is-closing-source' : ''}`}>
      <div className="intro-overlay-backdrop" />
      <div className={`intro-main-content${closing ? ' is-closing-live' : ''}`}>
        <div className="intro-hero-wordmark" aria-hidden="true">
          {words.map((word, i) => (
            <WordmarkLine
              key={word}
              word={word}
              delay={0.06 + i * 0.04}
            />
          ))}
        </div>
        <motion.div
          className="intro-hero-logo"
          initial={{ x: '110vw' }}
          animate={{ x: 0, y: 0, opacity: 1, scale: 1 }}
          transition={{ duration: 0.88, delay: 0.18, ease: [0.22, 1, 0.36, 1] }}
          aria-hidden="true"
        >
          <StudioLogo />
        </motion.div>
        <div className="intro-line-cloud" />
        <IntroSvgLines />
        <div className="intro-grid-layer" />
        <IntroBlocks />
      </div>
    </div>
  )
}

function ClosingOverlay({
  sourceRef,
  onComplete,
  onShatterStart,
}: {
  sourceRef: RefObject<HTMLDivElement | null>
  onComplete: () => void
  onShatterStart: (hidden: boolean) => void
}) {
  const shatterHostRef = useRef<HTMLDivElement | null>(null)
  const animationFrameRef = useRef<number | null>(null)
  const [snapshotCanvas, setSnapshotCanvas] = useState<HTMLCanvasElement | null>(null)
  const [crackFinished, setCrackFinished] = useState(false)
  const [freezeLiveFrame, setFreezeLiveFrame] = useState(false)
  const stage: ClosingStage = crackFinished && snapshotCanvas ? 'shatter' : 'crack'

  useEffect(() => {
    onShatterStart(stage === 'shatter')
  }, [onShatterStart, stage])

  useEffect(() => {
    let cancelled = false
    let crackTimer = 0
    let holdTimer = 0

    const pauseAnimations = () => {
      const source = sourceRef.current
      if (!source) return
      source.querySelectorAll<HTMLElement>('*').forEach((node) => {
        const animations = node.getAnimations?.() ?? []
        animations.forEach((animation) => {
          try {
            animation.pause()
          } catch {
            // Ignore non-pausable animations.
          }
        })
      })
    }

    const captureSnapshot = async () => {
      await new Promise<void>((resolve) => {
        holdTimer = window.setTimeout(
          resolve,
          Math.max(0, CRACK_GROWTH_DURATION_MS - SNAPSHOT_CAPTURE_LEAD_MS),
        )
      })
      if (cancelled) return
      await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
      if (cancelled) return
      setFreezeLiveFrame(true)
      await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
      if (cancelled) return
      pauseAnimations()
      await new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()))
      if (cancelled) return

      const source = sourceRef.current
      if (cancelled || !source) return

      const bounds = source.getBoundingClientRect()
      const canvas = await html2canvas(source, {
        backgroundColor: null,
        scale: Math.min(window.devicePixelRatio || 1, SHATTER_SNAPSHOT_SCALE),
        logging: false,
        useCORS: true,
        scrollX: 0,
        scrollY: 0,
        width: Math.max(1, Math.round(bounds.width)),
        height: Math.max(1, Math.round(bounds.height)),
        windowWidth: Math.max(1, Math.round(bounds.width)),
        windowHeight: Math.max(1, Math.round(bounds.height)),
      })

      if (!cancelled) {
        setSnapshotCanvas(canvas)
      }
    }

    crackTimer = window.setTimeout(() => {
      if (!cancelled) setCrackFinished(true)
    }, CRACK_GROWTH_DURATION_MS)
    void captureSnapshot()

    return () => {
      cancelled = true
      setFreezeLiveFrame(false)
      window.clearTimeout(holdTimer)
      window.clearTimeout(crackTimer)
      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current)
        animationFrameRef.current = null
      }
    }
  }, [sourceRef])

  useEffect(() => {
    if (stage !== 'shatter' || !snapshotCanvas || !shatterHostRef.current) return

    const host = shatterHostRef.current
    const dpr = Math.min(window.devicePixelRatio || 1, SHATTER_RENDERER_PIXEL_RATIO)
    const width = host.clientWidth || window.innerWidth
    const height = host.clientHeight || window.innerHeight

    const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: false, powerPreference: 'high-performance' })
    renderer.setPixelRatio(dpr)
    renderer.setSize(width, height, false)
    renderer.setClearColor(0x000000, 0)
    renderer.sortObjects = false
    host.innerHTML = ''
    host.appendChild(renderer.domElement)

    const scene = new THREE.Scene()
    const camera = new THREE.OrthographicCamera(-width / 2, width / 2, height / 2, -height / 2, 0.1, 2000)
    camera.position.z = 10

    const texture = new THREE.CanvasTexture(snapshotCanvas)
    texture.flipY = true
    texture.colorSpace = THREE.SRGBColorSpace
    texture.minFilter = THREE.LinearFilter
    texture.magFilter = THREE.LinearFilter
    texture.generateMipmaps = false

    const baseMaterial = new THREE.MeshBasicMaterial({
      map: texture,
      transparent: true,
      depthTest: false,
      depthWrite: false,
      side: THREE.FrontSide,
    })

    const outlineMaterial = new THREE.LineBasicMaterial({
      color: 0xffffff,
      transparent: true,
      opacity: 0.08,
      depthTest: false,
      depthWrite: false,
    })

    const maxShardDelayMs = Math.max(...shardSpecs.map((shard) => shard.delayMs))
    const totalDurationMs = maxShardDelayMs + SHARD_DURATION_MS + 40
    let animationStart: number | null = null

    const shardEntries: Array<{
      mesh: THREE.Mesh
      outline: THREE.LineLoop
      spec: ShardSpec
      center: THREE.Vector2
    }> = []

    shardSpecs.forEach((spec) => {
      const { geometry, center, outlinePoints } = createShardMeshData(spec, width, height)
      const material = baseMaterial.clone()
      material.opacity = 1

      const mesh = new THREE.Mesh(geometry, material)
      mesh.position.set(center.x, center.y, 0)
      mesh.frustumCulled = false
      scene.add(mesh)

      const lineMaterial = outlineMaterial.clone()
      const outlineGeometry = new THREE.BufferGeometry().setFromPoints(outlinePoints)
      const outline = new THREE.LineLoop(outlineGeometry, lineMaterial)
      outline.position.set(center.x, center.y, 0)
      outline.frustumCulled = false
      scene.add(outline)

      shardEntries.push({ mesh, outline, spec, center })
    })

    const animate = (timestamp: number) => {
      if (animationStart === null) animationStart = timestamp
      const elapsed = timestamp - animationStart

      shardEntries.forEach(({ mesh, outline, spec, center }) => {
        const localElapsed = elapsed - spec.delayMs
        const progress = clamp01(localElapsed / SHARD_DURATION_MS)
        if (progress <= 0) {
          mesh.visible = false
          outline.visible = false
          return
        }

        mesh.visible = true
        outline.visible = true

        const drift = easeOutCubic(progress)
        const drop = easeInCubic(progress)
        const x = center.x + (spec.translateXPct / 100) * width * drift
        const y = center.y - (spec.translateYPct / 100) * height * drop
        const rotationZ = THREE.MathUtils.degToRad(spec.rotationDeg * progress + spec.spinZDeg * progress * progress)
        const rotationX = THREE.MathUtils.degToRad(spec.tiltXDeg * progress + spec.spinXDeg * progress * progress)
        const rotationY = THREE.MathUtils.degToRad(spec.tiltYDeg * progress + spec.spinYDeg * progress * progress)

        mesh.position.set(x, y, 0)
        mesh.rotation.set(rotationX, rotationY, rotationZ)
        ;(mesh.material as THREE.MeshBasicMaterial).opacity = 1

        outline.position.set(x, y, 0)
        outline.rotation.set(rotationX, rotationY, rotationZ)
        ;(outline.material as THREE.LineBasicMaterial).opacity =
          progress < 0.14 ? lerp(0, 0.09, progress / 0.14) : 0.09
      })

      renderer.render(scene, camera)
      if (elapsed < totalDurationMs) {
        animationFrameRef.current = window.requestAnimationFrame(animate)
      } else {
        host.innerHTML = ''
        onComplete()
      }
    }

    animationFrameRef.current = window.requestAnimationFrame(animate)

    return () => {
      if (animationFrameRef.current !== null) {
        window.cancelAnimationFrame(animationFrameRef.current)
        animationFrameRef.current = null
      }
      shardEntries.forEach(({ mesh, outline }) => {
        mesh.geometry.dispose()
        ;(mesh.material as THREE.MeshBasicMaterial).dispose()
        outline.geometry.dispose()
        ;(outline.material as THREE.LineBasicMaterial).dispose()
      })
      texture.dispose()
      renderer.dispose()
      host.innerHTML = ''
    }
  }, [onComplete, snapshotCanvas, stage])

  return (
    <>
      {stage === 'crack' ? (
        <div
          className={`intro-main-content is-clacking intro-crack-layer${freezeLiveFrame ? ' is-frozen-live' : ''}`}
          aria-hidden="true"
        >
          <IntroCrackGrowth />
        </div>
      ) : null}
      <div className={`intro-shatter${stage === 'shatter' ? ' is-active' : ''}`} aria-hidden="true">
        <div ref={shatterHostRef} className="intro-shatter-webgl" />
      </div>
    </>
  )
}

export function IntroOverlay({
  phase,
  onSkip,
  onComplete,
}: {
  phase: IntroPhase
  onSkip: () => void
  onComplete: () => void
}) {
  const sourceRef = useRef<HTMLDivElement | null>(null)
  const [sourceHidden, setSourceHidden] = useState(false)
  const isSourceHidden = phase === 'closing' && sourceHidden

  if (phase === 'hidden') return null

  return (
    <motion.div
      className={`intro-overlay intro-overlay-${phase}`}
      initial={false}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      <div className={`intro-source-shell${isSourceHidden ? ' is-hidden' : ''}`}>
        <IntroSource sourceRef={sourceRef} closing={phase === 'closing'} />
      </div>
      {phase === 'closing' ? (
        <ClosingOverlay sourceRef={sourceRef} onComplete={onComplete} onShatterStart={setSourceHidden} />
      ) : null}
      <button type="button" className="intro-skip" onClick={onSkip}>Skip</button>
    </motion.div>
  )
}
