{
	"$schema": "https://ui.shadcn.com/schema/registry-item.json",
	"name": "PaperCrumple-JS-TW",
	"title": "PaperCrumple",
	"description": "An image that crumples into a textured 3D sheet while held and follows the grabbed point as you drag. Release it as a crumpled ball, unfold it flat, or leave the paper creased, with customizable folds, paper grain, lighting and shadows.",
	"type": "registry:component",
	"files": [
		{
			"type": "registry:component",
			"path": "PaperCrumple/PaperCrumple.jsx",
			"content": "'use client';\n\nimport { useEffect, useRef, useState } from 'react';\nimport * as THREE from 'three';\nconst spring = (value = 0) => ({ value, target: value, velocity: 0 });\nconst clamp = (value, min, max) => Math.min(max, Math.max(min, value));\nconst finite = (value, fallback) => (Number.isFinite(value) ? value : fallback);\nfunction advance(s, dt, duration, instant) {\n  if (instant || duration <= 0) {\n    s.value = s.target;\n    s.velocity = 0;\n    return false;\n  }\n  const omega = 8 / Math.max(0.06, duration);\n  const offset = s.value - s.target;\n  const term = s.velocity + omega * offset;\n  const decay = Math.exp(-omega * dt);\n  s.value = s.target + (offset + term * dt) * decay;\n  s.velocity = (s.velocity - omega * term * dt) * decay;\n  if (Math.abs(s.value - s.target) < 0.0001 && Math.abs(s.velocity) < 0.001) {\n    s.value = s.target;\n    s.velocity = 0;\n    return false;\n  }\n  return true;\n}\nfunction randomSource(seed) {\n  let value = seed | 0;\n  return () => {\n    value |= 0;\n    value = (value + 0x6d2b79f5) | 0;\n    let n = Math.imul(value ^ (value >>> 15), 1 | value);\n    n = (n + Math.imul(n ^ (n >>> 7), 61 | n)) ^ n;\n    return ((n ^ (n >>> 14)) >>> 0) / 4294967296;\n  };\n}\nfunction createPaperPath(rest, triangles, shortSide, density, sharpness, depth, seed) {\n  const count = rest.length / 3;\n  const points = Float64Array.from(rest);\n  const previous = Float64Array.from(rest);\n  const before = Float64Array.from(rest);\n  const edges = [];\n  const hinges = [];\n  const adjacency = new Map();\n  const random = randomSource(seed);\n  const guides = Array.from({ length: density }, () => {\n    const angle = random() * Math.PI * 2;\n    return { x: Math.cos(angle), y: Math.sin(angle), phase: random() * Math.PI * 2, weight: random() * 0.6 + 0.4 };\n  });\n  for (let t = 0; t < triangles.length; t += 3) {\n    for (let k = 0; k < 3; k++) {\n      const a = triangles[t + k],\n        b = triangles[t + ((k + 1) % 3)],\n        opposite = triangles[t + ((k + 2) % 3)];\n      const key = Math.min(a, b) * count + Math.max(a, b);\n      const other = adjacency.get(key);\n      if (!other) {\n        adjacency.set(key, { a, b, opposite });\n        const length = Math.hypot(rest[a * 3] - rest[b * 3], rest[a * 3 + 1] - rest[b * 3 + 1]);\n        edges.push(a * 3, b * 3, length);\n      } else {\n        const c = other.opposite * 3,\n          d = opposite * 3;\n        const length = Math.hypot(rest[c] - rest[d], rest[c + 1] - rest[d + 1]);\n        const mx = (rest[c] + rest[d]) * 0.5,\n          my = (rest[c + 1] + rest[d + 1]) * 0.5;\n        let weakness = 0;\n        for (const guide of guides) {\n          const distance = Math.abs(Math.sin(((mx * guide.x + my * guide.y) / shortSide) * 4 + guide.phase));\n          weakness = Math.max(weakness, Math.exp(-distance * distance * 80) * guide.weight);\n        }\n        hinges.push(c, d, length, 0.12 + (1 - weakness) * 0.75);\n      }\n    }\n  }\n  const spacing = Math.sqrt((shortSide * shortSide) / count);\n  const thickness = shortSide * 0.008;\n  const samples = [rest.slice()];\n  const frameCount = 64;\n  const stepsPerFrame = 3;\n  const totalSteps = frameCount * stepsPerFrame;\n  let initialRadius = 0;\n  for (let i = 0; i < rest.length; i += 3)\n    initialRadius = Math.max(initialRadius, Math.hypot(rest[i] / 0.94, rest[i + 1] / 1.02));\n  initialRadius *= 1.02;\n  function constrain(list, stride, stiffness, reverse) {\n    for (let n = 0; n < list.length; n += stride) {\n      const edge = reverse ? list.length - stride - n : n;\n      const a = list[edge],\n        b = list[edge + 1];\n      const dx = points[b] - points[a],\n        dy = points[b + 1] - points[a + 1],\n        dz = points[b + 2] - points[a + 2];\n      const length = Math.sqrt(dx * dx + dy * dy + dz * dz);\n      if (length < 0.000001) continue;\n      const weight = stride === 4 ? list[edge + 3] : 1;\n      const amount = (1 - list[edge + 2] / length) * 0.5 * stiffness * weight;\n      points[a] += dx * amount;\n      points[b] -= dx * amount;\n      points[a + 1] += dy * amount;\n      points[b + 1] -= dy * amount;\n      points[a + 2] += dz * amount;\n      points[b + 2] -= dz * amount;\n    }\n  }\n  function separateLayers() {\n    const margin = thickness * 2;\n    for (let t = 0; t < triangles.length; t += 3) {\n      const a = triangles[t] * 3,\n        b = triangles[t + 1] * 3,\n        c = triangles[t + 2] * 3;\n      const ax = points[a],\n        ay = points[a + 1],\n        az = points[a + 2];\n      const bx = points[b] - ax,\n        by = points[b + 1] - ay,\n        bz = points[b + 2] - az;\n      const cx = points[c] - ax,\n        cy = points[c + 1] - ay,\n        cz = points[c + 2] - az;\n      let nx = by * cz - bz * cy,\n        ny = bz * cx - bx * cz,\n        nz = bx * cy - by * cx;\n      const length = Math.hypot(nx, ny, nz);\n      if (length < 0.0000001) continue;\n      nx /= length;\n      ny /= length;\n      nz /= length;\n      const minX = Math.min(ax, points[b], points[c]) - margin;\n      const maxX = Math.max(ax, points[b], points[c]) + margin;\n      const minY = Math.min(ay, points[b + 1], points[c + 1]) - margin;\n      const maxY = Math.max(ay, points[b + 1], points[c + 1]) + margin;\n      const minZ = Math.min(az, points[b + 2], points[c + 2]) - margin;\n      const maxZ = Math.max(az, points[b + 2], points[c + 2]) + margin;\n      const bb = bx * bx + by * by + bz * bz,\n        cc = cx * cx + cy * cy + cz * cz;\n      const bc = bx * cx + by * cy + bz * cz;\n      const determinant = bb * cc - bc * bc;\n      if (determinant < 0.0000000001) continue;\n      for (let p = 0; p < points.length; p += 3) {\n        if (p === a || p === b || p === c) continue;\n        if (\n          points[p] < minX ||\n          points[p] > maxX ||\n          points[p + 1] < minY ||\n          points[p + 1] > maxY ||\n          points[p + 2] < minZ ||\n          points[p + 2] > maxZ\n        )\n          continue;\n        const rx = rest[p] - (rest[a] + rest[b] + rest[c]) / 3;\n        const ry = rest[p + 1] - (rest[a + 1] + rest[b + 1] + rest[c + 1]) / 3;\n        if (rx * rx + ry * ry < spacing * spacing * 6) continue;\n        const dx = points[p] - ax,\n          dy = points[p + 1] - ay,\n          dz = points[p + 2] - az;\n        const distance = dx * nx + dy * ny + dz * nz;\n        const previousDistance =\n          (before[p] - before[a]) * nx + (before[p + 1] - before[a + 1]) * ny + (before[p + 2] - before[a + 2]) * nz;\n        const side = previousDistance >= 0 ? 1 : -1;\n        if (distance * side >= thickness || Math.abs(distance) > margin) continue;\n        const pb = dx * bx + dy * by + dz * bz,\n          pc = dx * cx + dy * cy + dz * cz;\n        const u = (cc * pb - bc * pc) / determinant;\n        const v = (bb * pc - bc * pb) / determinant;\n        if (u < 0 || v < 0 || u + v > 1) continue;\n        const w = 1 - u - v;\n        const correction = (thickness * side - distance) / (1 + w * w + u * u + v * v);\n        for (let axis = 0; axis < 3; axis++) {\n          const normal = axis === 0 ? nx : axis === 1 ? ny : nz;\n          const movement = normal * correction;\n          points[p + axis] += movement;\n          points[a + axis] -= movement * w;\n          points[b + axis] -= movement * u;\n          points[c + axis] -= movement * v;\n        }\n      }\n    }\n  }\n  for (let step = 1; step <= totalSteps; step++) {\n    const progress = step / totalSteps;\n    const compression = progress * progress * (3 - 2 * progress);\n    const radius = initialRadius * (1 - compression) + shortSide * (0.19 - depth * 0.025) * compression;\n    before.set(points);\n    for (let i = 0; i < points.length; i += 3) {\n      const x = rest[i] / shortSide,\n        y = rest[i + 1] / shortSide;\n      let buckle = 0;\n      for (const guide of guides) buckle += Math.sin((x * guide.x + y * guide.y) * 5 + guide.phase) * guide.weight;\n      for (let axis = 0; axis < 3; axis++) {\n        const velocity = (points[i + axis] - previous[i + axis]) * 0.55;\n        previous[i + axis] = points[i + axis];\n        points[i + axis] += clamp(velocity, -spacing * 0.15, spacing * 0.15);\n      }\n      points[i + 2] += (buckle / density) * shortSide * 0.0007 * Math.sin(progress * Math.PI);\n    }\n    for (let pass = 0; pass < 18; pass++) {\n      constrain(hinges, 4, 0.45 * (1 - sharpness * 0.4), pass % 2 === 0);\n      for (let i = 0; i < points.length; i += 3) {\n        const x = points[i] / 0.94,\n          y = points[i + 1] / 1.02,\n          z = points[i + 2] / 0.86;\n        const distance = Math.hypot(x, y, z);\n        if (distance > radius) {\n          const push = (1 - radius / distance) * 0.55;\n          points[i] -= points[i] * push;\n          points[i + 1] -= points[i + 1] * push;\n          points[i + 2] -= points[i + 2] * push;\n        }\n      }\n      constrain(edges, 3, 1, pass % 2 !== 0);\n      if (pass === 8 || pass === 17) separateLayers();\n    }\n    for (let h = 0; h < hinges.length; h += 4) {\n      const a = hinges[h],\n        b = hinges[h + 1];\n      const length = Math.hypot(points[a] - points[b], points[a + 1] - points[b + 1], points[a + 2] - points[b + 2]);\n      if (length < hinges[h + 2] * 0.86) hinges[h + 2] += (length - hinges[h + 2]) * 0.12;\n    }\n    if (step % stepsPerFrame === 0) samples.push(Float32Array.from(points));\n  }\n  const folded = Float64Array.from(points);\n  for (let step = 1; step <= 80; step++) {\n    const t = step / 80;\n    const unfold = t * t * (3 - 2 * t);\n    for (let pass = 0; pass < 12; pass++) {\n      for (let i = 0; i < points.length; i++) {\n        const target = folded[i] + (rest[i] - folded[i]) * unfold;\n        points[i] += (target - points[i]) * (i % 3 === 2 ? 0.04 : 0.22);\n      }\n      constrain(hinges, 4, 0.7, pass % 2 === 0);\n      constrain(edges, 3, 1, pass % 2 !== 0);\n    }\n  }\n  const creased = Float32Array.from(points);\n  return { samples, creased };\n}\nconst PaperCrumple = ({\n  src,\n  alt = 'Crumplable image',\n  backSrc = '',\n  width = 320,\n  height = 400,\n  sceneHeight = 560,\n  imageFit = 'cover',\n  releaseBehavior = 'restore',\n  crumpleAmount = 0.85,\n  crumpleDuration = 0.55,\n  releaseDuration = 0.4,\n  foldCount = 6,\n  foldSharpness = 0.6,\n  wrinkleDepth = 0.65,\n  creaseStrength = 0.18,\n  paperColor = '#f4f0e8',\n  roughness = 0.92,\n  paperTexture = 0.08,\n  lightIntensity = 1.8,\n  lightAngle = -35,\n  shadow = true,\n  shadowOpacity = 0.08,\n  draggable = true,\n  dragRotation = 10,\n  dragRadius = 180,\n  returnToOrigin = true,\n  rotation = 0,\n  seed = 7,\n  detail = 64,\n  disabled = false,\n  resetKey = 0,\n  onStateChange,\n  onError,\n  className = '',\n  style\n}) => {\n  const pathCache = useRef(null);\n  const rootRef = useRef(null);\n  const canvasRef = useRef(null);\n  const hitRef = useRef(null);\n  const resetRef = useRef(null);\n  const cancelRef = useRef(null);\n  const [status, setStatus] = useState('loading');\n  const options = useRef({\n    releaseBehavior,\n    crumpleAmount,\n    crumpleDuration,\n    releaseDuration,\n    creaseStrength,\n    draggable,\n    dragRotation,\n    dragRadius,\n    returnToOrigin,\n    disabled,\n    onStateChange,\n    onError\n  });\n  options.current = {\n    releaseBehavior,\n    crumpleAmount,\n    crumpleDuration,\n    releaseDuration,\n    creaseStrength,\n    draggable,\n    dragRotation,\n    dragRadius,\n    returnToOrigin,\n    disabled,\n    onStateChange,\n    onError\n  };\n  useEffect(() => {\n    const root = rootRef.current,\n      canvas = canvasRef.current,\n      hit = hitRef.current;\n    if (!root || !canvas || !hit) return;\n    setStatus('loading');\n    hit.disabled = true;\n    hit.dataset.held = 'false';\n    hit.setAttribute('aria-pressed', 'false');\n    const initialRect = root.getBoundingClientRect();\n    const initialWidth = Math.max(1, finite(width, 320));\n    const initialHeight = Math.max(1, finite(height, 400));\n    const initialScale = Math.min(\n      1,\n      Math.max(1, initialRect.width - 48) / initialWidth,\n      Math.max(1, initialRect.height - 48) / initialHeight\n    );\n    root.style.setProperty('--pc-image-width', `${initialWidth * initialScale}px`);\n    root.style.setProperty('--pc-image-height', `${initialHeight * initialScale}px`);\n    let renderer;\n    try {\n      renderer = new THREE.WebGLRenderer({ canvas, alpha: true, antialias: true, powerPreference: 'low-power' });\n    } catch (error) {\n      setStatus('error');\n      options.current.onError?.(error instanceof Error ? error : new Error('WebGL is unavailable.'));\n      return;\n    }\n    const paperWidth = Math.max(1, finite(width, 320));\n    const paperHeight = Math.max(1, finite(height, 400));\n    const aspect = paperHeight / paperWidth;\n    const shortSide = Math.min(1, aspect);\n    const resolution = Math.round(clamp(finite(detail, 64) / 4, 8, 24));\n    const columns = Math.max(8, Math.round(resolution / Math.max(1, aspect)));\n    const rows = Math.max(8, Math.round(resolution * Math.min(1, aspect)));\n    const rng = randomSource(finite(seed, 7));\n    const sharpness = clamp(finite(foldSharpness, 0.6), 0, 1);\n    const foldTotal = Math.round(clamp(finite(foldCount, 6), 3, 16));\n    const depth = clamp(finite(wrinkleDepth, 0.65), 0, 2);\n    const count = (columns + 1) * (rows + 1);\n    const original = new Float32Array(count * 3);\n    const positions = new Float32Array(count * 3);\n    const uvs = new Float32Array(count * 2);\n    const indices = [];\n    for (let row = 0; row <= rows; row++) {\n      for (let col = 0; col <= columns; col++) {\n        const index = row * (columns + 1) + col;\n        const u = (col + (col > 0 && col < columns ? (rng() - 0.5) * 0.5 : 0)) / columns;\n        const v = (row + (row > 0 && row < rows ? (rng() - 0.5) * 0.5 : 0)) / rows;\n        const x = u - 0.5,\n          y = (v - 0.5) * aspect;\n        original[index * 3] = x;\n        original[index * 3 + 1] = y;\n        uvs[index * 2] = u;\n        uvs[index * 2 + 1] = v;\n        if (col < columns && row < rows) {\n          const a = index,\n            b = index + 1,\n            c = index + columns + 1,\n            d = c + 1;\n          if (rng() > 0.5) indices.push(a, b, d, a, d, c);\n          else indices.push(a, b, c, b, d, c);\n        }\n      }\n    }\n    const pathKey = JSON.stringify([paperWidth, paperHeight, resolution, foldTotal, sharpness, depth, seed]);\n    if (pathCache.current?.key !== pathKey) {\n      pathCache.current = {\n        key: pathKey,\n        value: createPaperPath(original, indices, shortSide, foldTotal, sharpness, depth, finite(seed, 7))\n      };\n    }\n    const paperPath = pathCache.current.value;\n    const renderPositions = new Float32Array(indices.length * 3);\n    const renderNormals = new Float32Array(indices.length * 3);\n    const renderUvs = new Float32Array(indices.length * 2);\n    const faceNormals = new Float32Array(indices.length);\n    const incidentFaces = Array.from({ length: count }, () => []);\n    for (let i = 0; i < indices.length; i++) {\n      renderUvs[i * 2] = uvs[indices[i] * 2];\n      renderUvs[i * 2 + 1] = uvs[indices[i] * 2 + 1];\n      incidentFaces[indices[i]].push(Math.floor(i / 3) * 3);\n    }\n    const geometry = new THREE.BufferGeometry();\n    geometry.setAttribute('position', new THREE.BufferAttribute(renderPositions, 3).setUsage(THREE.DynamicDrawUsage));\n    geometry.setAttribute('normal', new THREE.BufferAttribute(renderNormals, 3).setUsage(THREE.DynamicDrawUsage));\n    geometry.setAttribute('uv', new THREE.BufferAttribute(renderUvs, 2));\n    const grainData = new Uint8Array(128 * 128 * 4);\n    for (let i = 0; i < grainData.length; i += 4) {\n      const value = 100 + Math.floor(rng() * 155);\n      grainData[i] = grainData[i + 1] = grainData[i + 2] = value;\n      grainData[i + 3] = 255;\n    }\n    const grain = new THREE.DataTexture(grainData, 128, 128);\n    grain.wrapS = grain.wrapT = THREE.RepeatWrapping;\n    grain.repeat.set(5, 5 * aspect);\n    grain.magFilter = THREE.LinearFilter;\n    grain.minFilter = THREE.LinearFilter;\n    grain.needsUpdate = true;\n    const materialOptions = {\n      roughness: clamp(finite(roughness, 0.92), 0, 1),\n      metalness: 0,\n      bumpMap: grain,\n      bumpScale: clamp(finite(paperTexture, 0.08), 0, 1) * 0.32,\n      alphaTest: 0.04,\n      alphaToCoverage: true,\n      flatShading: false\n    };\n    const frontMaterial = new THREE.MeshStandardMaterial({ ...materialOptions, side: THREE.FrontSide });\n    const backMaterial = new THREE.MeshStandardMaterial({\n      ...materialOptions,\n      side: THREE.BackSide,\n      color: backSrc ? '#ffffff' : paperColor\n    });\n    const lighting = { value: 0 };\n    for (const material of [frontMaterial, backMaterial]) {\n      material.onBeforeCompile = shader => {\n        shader.uniforms.paperLighting = lighting;\n        shader.fragmentShader = 'uniform float paperLighting;\\n' + shader.fragmentShader;\n        shader.fragmentShader = shader.fragmentShader.replace(\n          '#include <map_fragment>',\n          `\n          #include <map_fragment>\n          #ifdef USE_MAP\n            if (vMapUv.x < 0.0 || vMapUv.x > 1.0 || vMapUv.y < 0.0 || vMapUv.y > 1.0) discard;\n            ${material === backMaterial && !backSrc ? 'diffuseColor.rgb = diffuse;' : ''}\n          #endif\n        `\n        );\n        shader.fragmentShader = shader.fragmentShader.replace(\n          '#include <opaque_fragment>',\n          `\n          outgoingLight = mix(diffuseColor.rgb, outgoingLight, paperLighting);\n          #include <opaque_fragment>\n        `\n        );\n      };\n      material.customProgramCacheKey = () => `paper-${material === backMaterial && !backSrc ? 'stock' : 'print'}`;\n    }\n    const depthMaterial = new THREE.MeshDepthMaterial({\n      depthPacking: THREE.RGBADepthPacking,\n      alphaTest: 0.04,\n      side: THREE.DoubleSide\n    });\n    depthMaterial.onBeforeCompile = shader => {\n      shader.fragmentShader = shader.fragmentShader.replace(\n        '#include <map_fragment>',\n        `\n        #include <map_fragment>\n        #ifdef USE_MAP\n          if (vMapUv.x < 0.0 || vMapUv.x > 1.0 || vMapUv.y < 0.0 || vMapUv.y > 1.0) discard;\n        #endif\n      `\n      );\n    };\n    const sheet = new THREE.Group();\n    const front = new THREE.Mesh(geometry, frontMaterial);\n    const back = new THREE.Mesh(geometry, backMaterial);\n    front.castShadow = shadow;\n    front.receiveShadow = shadow;\n    back.receiveShadow = shadow;\n    front.customDepthMaterial = depthMaterial;\n    sheet.add(front, back);\n    const scene = new THREE.Scene();\n    scene.add(sheet);\n    const camera = new THREE.PerspectiveCamera(35, 1, 0.1, 10000);\n    const ambient = new THREE.HemisphereLight(0xffffff, 0xa4a0b0, 1.35);\n    scene.add(ambient);\n    const light = new THREE.DirectionalLight(0xfffaf0, Math.max(0, finite(lightIntensity, 1.8)));\n    light.castShadow = shadow;\n    light.shadow.mapSize.set(1024, 1024);\n    light.shadow.bias = -0.0002;\n    light.shadow.normalBias = 0.6;\n    light.shadow.radius = 3;\n    scene.add(light, light.target);\n    const floorGeometry = new THREE.PlaneGeometry(1, 1);\n    const floorMaterial = new THREE.ShadowMaterial({\n      opacity: clamp(finite(shadowOpacity, 0.08), 0, 1),\n      depthWrite: false\n    });\n    const floor = new THREE.Mesh(floorGeometry, floorMaterial);\n    floor.receiveShadow = true;\n    floor.visible = shadow;\n    scene.add(floor);\n    renderer.setClearColor(0x000000, 0);\n    renderer.outputColorSpace = THREE.SRGBColorSpace;\n    renderer.shadowMap.enabled = shadow;\n    renderer.shadowMap.type = THREE.PCFSoftShadowMap;\n    const amount = spring(),\n      memory = spring(),\n      posX = spring(),\n      posY = spring(),\n      tiltX = spring(),\n      tiltY = spring();\n    const springs = [amount, memory, posX, posY, tiltX, tiltY];\n    const raycaster = new THREE.Raycaster();\n    const pointer = new THREE.Vector2();\n    const plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);\n    const anchor = new THREE.Vector3(),\n      world = new THREE.Vector3(),\n      corner = new THREE.Vector3();\n    const a = new THREE.Vector3(),\n      b = new THREE.Vector3(),\n      c = new THREE.Vector3();\n    const weights = new THREE.Vector3(1, 0, 0);\n    let gripIndices = [Math.floor(count / 2), 0, 0];\n    let viewportWidth = 1,\n      viewportHeight = 1,\n      scale = paperWidth;\n    let pointerX = 0,\n      pointerY = 0,\n      lastX = 0,\n      lastY = 0,\n      lastMove = 0;\n    let speedX = 0,\n      speedY = 0;\n    let held = false,\n      keyboard = false,\n      pointerId = null;\n    let peak = 0,\n      disposed = false,\n      ready = false,\n      inView = true,\n      contextLost = false;\n    let frame = 0,\n      lastTime = 0,\n      state = 'flat';\n    const textures = new Set();\n    const media = window.matchMedia('(prefers-reduced-motion: reduce)');\n    let reduceMotion = media.matches;\n    let previousAmount = -1,\n      previousMemory = -1;\n    const baseRotation = THREE.MathUtils.degToRad(finite(rotation, 0));\n    function publish(next) {\n      if (next === state) return;\n      state = next;\n      options.current.onStateChange?.(next);\n    }\n    function deform() {\n      if (amount.value === previousAmount && memory.value === previousMemory) return;\n      previousAmount = amount.value;\n      previousMemory = memory.value;\n      const fold = clamp(amount.value, 0, 1);\n      const frame = fold * (paperPath.samples.length - 1);\n      const lower = Math.floor(frame);\n      const upper = Math.min(lower + 1, paperPath.samples.length - 1);\n      const mix = frame - lower;\n      const from = paperPath.samples[lower];\n      const to = paperPath.samples[upper];\n      for (let i = 0; i < positions.length; i++) {\n        positions[i] = from[i] + (to[i] - from[i]) * mix;\n        positions[i] += (paperPath.creased[i] - original[i]) * memory.value * (1 - fold);\n      }\n      for (let face = 0; face < indices.length; face += 3) {\n        const a = indices[face] * 3,\n          b = indices[face + 1] * 3,\n          c = indices[face + 2] * 3;\n        const bx = positions[b] - positions[a],\n          by = positions[b + 1] - positions[a + 1],\n          bz = positions[b + 2] - positions[a + 2];\n        const cx = positions[c] - positions[a],\n          cy = positions[c + 1] - positions[a + 1],\n          cz = positions[c + 2] - positions[a + 2];\n        const nx = by * cz - bz * cy,\n          ny = bz * cx - bx * cz,\n          nz = bx * cy - by * cx;\n        const length = Math.hypot(nx, ny, nz) || 1;\n        faceNormals[face] = nx / length;\n        faceNormals[face + 1] = ny / length;\n        faceNormals[face + 2] = nz / length;\n      }\n      for (let i = 0; i < indices.length; i++) {\n        const source = indices[i] * 3;\n        const face = Math.floor(i / 3) * 3;\n        let nx = 0,\n          ny = 0,\n          nz = 0;\n        for (const neighbor of incidentFaces[indices[i]]) {\n          const dot =\n            faceNormals[face] * faceNormals[neighbor] +\n            faceNormals[face + 1] * faceNormals[neighbor + 1] +\n            faceNormals[face + 2] * faceNormals[neighbor + 2];\n          const weight = THREE.MathUtils.smoothstep(dot, 0.88 - (1 - sharpness) * 0.18, 0.98);\n          nx += faceNormals[neighbor] * weight;\n          ny += faceNormals[neighbor + 1] * weight;\n          nz += faceNormals[neighbor + 2] * weight;\n        }\n        const length = Math.hypot(nx, ny, nz) || 1;\n        renderPositions[i * 3] = positions[source];\n        renderPositions[i * 3 + 1] = positions[source + 1];\n        renderPositions[i * 3 + 2] = positions[source + 2];\n        renderNormals[i * 3] = nx / length;\n        renderNormals[i * 3 + 1] = ny / length;\n        renderNormals[i * 3 + 2] = nz / length;\n      }\n      geometry.attributes.position.needsUpdate = true;\n      geometry.attributes.normal.needsUpdate = true;\n      geometry.computeBoundingSphere();\n      geometry.computeBoundingBox();\n      lighting.value = THREE.MathUtils.smoothstep(fold + memory.value, 0, 0.4);\n    }\n    function placeHitTarget() {\n      const bounds = geometry.boundingBox;\n      if (!bounds) return;\n      let minX = Infinity,\n        minY = Infinity,\n        maxX = -Infinity,\n        maxY = -Infinity;\n      for (let i = 0; i < 8; i++) {\n        corner.set(\n          i & 1 ? bounds.max.x : bounds.min.x,\n          i & 2 ? bounds.max.y : bounds.min.y,\n          i & 4 ? bounds.max.z : bounds.min.z\n        );\n        corner.applyMatrix4(sheet.matrixWorld).project(camera);\n        const x = ((corner.x + 1) * viewportWidth) / 2,\n          y = ((1 - corner.y) * viewportHeight) / 2;\n        minX = Math.min(minX, x);\n        maxX = Math.max(maxX, x);\n        minY = Math.min(minY, y);\n        maxY = Math.max(maxY, y);\n      }\n      hit.style.transform = `translate3d(${minX}px, ${minY}px, 0)`;\n      hit.style.width = `${Math.max(24, maxX - minX)}px`;\n      hit.style.height = `${Math.max(24, maxY - minY)}px`;\n    }\n    function setPointer(x, y) {\n      pointer.set((x / viewportWidth) * 2 - 1, 1 - (y / viewportHeight) * 2);\n      raycaster.setFromCamera(pointer, camera);\n    }\n    function render(time) {\n      frame = 0;\n      if (disposed || !ready || contextLost || !inView || document.hidden) return;\n      const dt = lastTime ? Math.min(0.04, (time - lastTime) / 1000) : 1 / 60;\n      lastTime = time;\n      const opts = options.current;\n      let moving = false;\n      const duration = held ? finite(opts.crumpleDuration, 0.55) : finite(opts.releaseDuration, 0.4);\n      for (const s of springs)\n        moving = advance(s, dt, s === amount || s === memory ? duration : 0.42, reduceMotion || keyboard) || moving;\n      peak = Math.max(peak, amount.value);\n      deform();\n      sheet.rotation.set(tiltX.value, tiltY.value, baseRotation);\n      if (held && !keyboard && opts.draggable) {\n        const attribute = geometry.attributes.position;\n        anchor.set(0, 0, 0);\n        for (let i = 0; i < 3; i++) {\n          a.fromBufferAttribute(attribute, gripIndices[i]);\n          anchor.addScaledVector(a, weights.getComponent(i));\n        }\n        anchor.multiplyScalar(scale).applyEuler(sheet.rotation);\n        plane.constant = -anchor.z;\n        setPointer(pointerX, pointerY);\n        if (raycaster.ray.intersectPlane(plane, world)) {\n          posX.value = posX.target = world.x - anchor.x;\n          posY.value = posY.target = world.y - anchor.y;\n          posX.velocity = posY.velocity = 0;\n        }\n      }\n      sheet.position.set(posX.value, posY.value, 0);\n      sheet.updateMatrixWorld(true);\n      const shadowBounds = geometry.boundingBox;\n      if (shadowBounds) {\n        let backZ = Infinity;\n        for (let i = 0; i < 8; i++) {\n          corner.set(\n            i & 1 ? shadowBounds.max.x : shadowBounds.min.x,\n            i & 2 ? shadowBounds.max.y : shadowBounds.min.y,\n            i & 4 ? shadowBounds.max.z : shadowBounds.min.z\n          );\n          corner.applyMatrix4(sheet.matrixWorld);\n          backZ = Math.min(backZ, corner.z);\n        }\n        floor.position.z = backZ - scale * shortSide * 0.08;\n      }\n      renderer.render(scene, camera);\n      placeHitTarget();\n      if (moving) wake();\n    }\n    function wake() {\n      if (!frame && !disposed && ready && inView && !document.hidden && !contextLost)\n        frame = requestAnimationFrame(render);\n    }\n    function resize() {\n      const rect = root.getBoundingClientRect();\n      viewportWidth = Math.max(1, rect.width);\n      viewportHeight = Math.max(1, rect.height);\n      scale =\n        paperWidth *\n        Math.min(1, Math.max(1, viewportWidth - 48) / paperWidth, Math.max(1, viewportHeight - 48) / paperHeight);\n      sheet.scale.setScalar(scale);\n      camera.aspect = viewportWidth / viewportHeight;\n      camera.position.z = viewportHeight / (2 * Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)));\n      camera.updateProjectionMatrix();\n      camera.updateMatrixWorld();\n      renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));\n      renderer.setSize(viewportWidth, viewportHeight, false);\n      const reach = Math.max(viewportWidth, viewportHeight);\n      const angle = THREE.MathUtils.degToRad(finite(lightAngle, -35));\n      light.position.set(Math.sin(angle) * reach, Math.cos(angle) * reach, reach * 4);\n      light.shadow.camera.left = light.shadow.camera.bottom = -reach;\n      light.shadow.camera.right = light.shadow.camera.top = reach;\n      light.shadow.camera.near = 1;\n      light.shadow.camera.far = reach * 6;\n      light.shadow.camera.updateProjectionMatrix();\n      floor.position.z = -scale * shortSide * 0.12;\n      floor.scale.set(viewportWidth * 4, viewportHeight * 4, 1);\n      root.style.setProperty('--pc-image-width', `${scale}px`);\n      root.style.setProperty('--pc-image-height', `${scale * aspect}px`);\n      if (!held) {\n        const limitX = Math.max(0, (viewportWidth - scale) / 2 - 16);\n        const limitY = Math.max(0, (viewportHeight - scale * aspect) / 2 - 16);\n        posX.value = posX.target = clamp(posX.value, -limitX, limitX);\n        posY.value = posY.target = clamp(posY.value, -limitY, limitY);\n      }\n      wake();\n    }\n    function finish(instant = false) {\n      if (!held) return;\n      const opts = options.current;\n      held = false;\n      hit.setAttribute('aria-pressed', 'false');\n      hit.dataset.held = 'false';\n      const captured = pointerId;\n      pointerId = null;\n      if (captured !== null && hit.hasPointerCapture(captured)) hit.releasePointerCapture(captured);\n      if (opts.releaseBehavior === 'stay') {\n        amount.target = amount.value;\n        amount.velocity = 0;\n      } else {\n        amount.target = 0;\n        memory.target =\n          opts.releaseBehavior === 'creased'\n            ? Math.max(memory.value, peak * clamp(finite(opts.creaseStrength, 0.18), 0, 1))\n            : 0;\n      }\n      tiltX.target = tiltY.target = 0;\n      if (opts.returnToOrigin && opts.releaseBehavior !== 'stay') {\n        posX.target = posY.target = 0;\n      } else {\n        const bounds = geometry.boundingBox;\n        const spanX = opts.releaseBehavior === 'stay' && bounds ? bounds.max.x - bounds.min.x : 1;\n        const spanY = opts.releaseBehavior === 'stay' && bounds ? bounds.max.y - bounds.min.y : aspect;\n        const halfWidth =\n          ((Math.abs(Math.cos(baseRotation)) * spanX + Math.abs(Math.sin(baseRotation)) * spanY) * scale) / 2;\n        const halfHeight =\n          ((Math.abs(Math.sin(baseRotation)) * spanX + Math.abs(Math.cos(baseRotation)) * spanY) * scale) / 2;\n        const limitX = Math.min(\n          Math.max(0, finite(opts.dragRadius, 180)),\n          Math.max(0, viewportWidth / 2 - halfWidth - 16)\n        );\n        const limitY = Math.min(\n          Math.max(0, finite(opts.dragRadius, 180)),\n          Math.max(0, viewportHeight / 2 - halfHeight - 16)\n        );\n        const coast = !reduceMotion && !keyboard && performance.now() - lastMove < 90 ? 0.06 : 0;\n        posX.target = clamp(posX.value + speedX * coast, -limitX, limitX);\n        posY.target = clamp(posY.value - speedY * coast, -limitY, limitY);\n      }\n      if (instant || keyboard || reduceMotion) for (const s of springs) advance(s, 0, 0, true);\n      keyboard = false;\n      publish(\n        opts.releaseBehavior === 'stay' && amount.target > 0.001\n          ? 'crumpled'\n          : memory.target > 0.001\n            ? 'creased'\n            : 'flat'\n      );\n      wake();\n    }\n    function reset() {\n      finish(true);\n      for (const s of springs) s.target = s.value = s.velocity = 0;\n      peak = 0;\n      publish('flat');\n      wake();\n    }\n    resetRef.current = reset;\n    cancelRef.current = () => finish(true);\n    function start() {\n      held = true;\n      peak = amount.value;\n      amount.target = clamp(finite(options.current.crumpleAmount, 0.85), 0, 1);\n      hit.setAttribute('aria-pressed', 'true');\n      hit.dataset.held = 'true';\n      publish('holding');\n      wake();\n    }\n    function pointerDown(event) {\n      if (!ready || options.current.disabled || held || event.button !== 0 || !event.isPrimary) return;\n      const rect = root.getBoundingClientRect();\n      pointerX = lastX = event.clientX - rect.left;\n      pointerY = lastY = event.clientY - rect.top;\n      setPointer(pointerX, pointerY);\n      sheet.updateMatrixWorld(true);\n      const intersection = raycaster.intersectObjects([front, back], false)[0];\n      if (!intersection?.face) return;\n      event.preventDefault();\n      hit.dataset.pointer = 'true';\n      hit.focus({ preventScroll: true });\n      const { face } = intersection;\n      gripIndices = [face.a, face.b, face.c];\n      const attribute = geometry.attributes.position;\n      a.fromBufferAttribute(attribute, face.a);\n      b.fromBufferAttribute(attribute, face.b);\n      c.fromBufferAttribute(attribute, face.c);\n      THREE.Triangle.getBarycoord(sheet.worldToLocal(intersection.point.clone()), a, b, c, weights);\n      keyboard = false;\n      pointerId = event.pointerId;\n      speedX = speedY = 0;\n      lastMove = performance.now();\n      hit.setPointerCapture(event.pointerId);\n      start();\n    }\n    function pointerMove(event) {\n      if (!held || event.pointerId !== pointerId || !options.current.draggable) return;\n      const rect = root.getBoundingClientRect();\n      const now = performance.now();\n      const x = event.clientX - rect.left,\n        y = event.clientY - rect.top;\n      const dt = Math.max(0.008, (now - lastMove) / 1000);\n      speedX = (x - lastX) / dt;\n      speedY = (y - lastY) / dt;\n      lastX = x;\n      lastY = y;\n      lastMove = now;\n      pointerX = clamp(x, 12, viewportWidth - 12);\n      pointerY = clamp(y, 12, viewportHeight - 12);\n      const maxTilt = reduceMotion\n        ? 0\n        : THREE.MathUtils.degToRad(clamp(finite(options.current.dragRotation, 10), 0, 60));\n      tiltX.target = clamp(speedY / 1800, -1, 1) * maxTilt;\n      tiltY.target = clamp(speedX / 1800, -1, 1) * maxTilt;\n      wake();\n    }\n    function pointerUp(event) {\n      if (event.pointerId === pointerId) finish();\n    }\n    function pointerCancel(event) {\n      if (event.pointerId === pointerId) finish(true);\n    }\n    function cancel() {\n      finish(true);\n    }\n    function blur() {\n      hit.dataset.pointer = 'false';\n      cancel();\n    }\n    function keyDown(event) {\n      if (options.current.disabled || !ready) return;\n      hit.dataset.pointer = 'false';\n      if (event.key === 'Escape') {\n        event.preventDefault();\n        reset();\n        return;\n      }\n      if (event.key === ' ' || event.key === 'Enter') {\n        event.preventDefault();\n        if (event.repeat || held) return;\n        keyboard = true;\n        start();\n        advance(amount, 0, 0, true);\n        peak = Math.max(peak, amount.value);\n      } else if (\n        held &&\n        keyboard &&\n        options.current.draggable &&\n        ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(event.key)\n      ) {\n        event.preventDefault();\n        const step = event.shiftKey ? 30 : 12;\n        const limit = Math.max(0, finite(options.current.dragRadius, 180));\n        posX.target = clamp(\n          posX.target + (event.key === 'ArrowRight' ? step : event.key === 'ArrowLeft' ? -step : 0),\n          -Math.min(limit, viewportWidth / 3),\n          Math.min(limit, viewportWidth / 3)\n        );\n        posY.target = clamp(\n          posY.target + (event.key === 'ArrowUp' ? step : event.key === 'ArrowDown' ? -step : 0),\n          -Math.min(limit, viewportHeight / 3),\n          Math.min(limit, viewportHeight / 3)\n        );\n        wake();\n      }\n    }\n    function keyUp(event) {\n      if (keyboard && (event.key === ' ' || event.key === 'Enter')) {\n        event.preventDefault();\n        finish(true);\n      }\n    }\n    function visibility() {\n      lastTime = 0;\n      if (document.hidden) cancel();\n      else wake();\n    }\n    function motionChange() {\n      reduceMotion = media.matches;\n      if (reduceMotion) tiltX.target = tiltY.target = 0;\n      wake();\n    }\n    function loseContext(event) {\n      event.preventDefault();\n      contextLost = true;\n      cancel();\n      hit.disabled = true;\n      setStatus('error');\n      options.current.onError?.(new Error('The WebGL context was lost.'));\n    }\n    function restoreContext() {\n      contextLost = false;\n      if (ready) {\n        setStatus('ready');\n        hit.disabled = options.current.disabled;\n        wake();\n      }\n    }\n    hit.addEventListener('pointerdown', pointerDown);\n    hit.addEventListener('pointermove', pointerMove);\n    hit.addEventListener('pointerup', pointerUp);\n    hit.addEventListener('pointercancel', pointerCancel);\n    hit.addEventListener('lostpointercapture', pointerUp);\n    hit.addEventListener('keydown', keyDown);\n    hit.addEventListener('keyup', keyUp);\n    hit.addEventListener('blur', blur);\n    window.addEventListener('blur', cancel);\n    document.addEventListener('visibilitychange', visibility);\n    media.addEventListener('change', motionChange);\n    canvas.addEventListener('webglcontextlost', loseContext);\n    canvas.addEventListener('webglcontextrestored', restoreContext);\n    const observer = new ResizeObserver(resize);\n    observer.observe(root);\n    const intersectionObserver = new IntersectionObserver(entries => {\n      inView = entries[0].isIntersecting;\n      lastTime = 0;\n      if (!inView) cancel();\n      else wake();\n    });\n    intersectionObserver.observe(root);\n    deform();\n    resize();\n    const loader = new THREE.TextureLoader();\n    loader.setCrossOrigin('anonymous');\n    function load(url) {\n      return new Promise((resolve, reject) => {\n        loader.load(\n          url,\n          texture => {\n            if (disposed) {\n              texture.dispose();\n              resolve(texture);\n              return;\n            }\n            textures.add(texture);\n            texture.colorSpace = THREE.SRGBColorSpace;\n            texture.anisotropy = Math.min(8, renderer.capabilities.getMaxAnisotropy());\n            const image = texture.image;\n            const imageAspect = image.width / image.height;\n            const targetAspect = paperWidth / paperHeight;\n            let rx = 1,\n              ry = 1;\n            if (imageFit === 'cover') {\n              if (imageAspect > targetAspect) rx = targetAspect / imageAspect;\n              else ry = imageAspect / targetAspect;\n            } else {\n              if (imageAspect > targetAspect) ry = imageAspect / targetAspect;\n              else rx = targetAspect / imageAspect;\n            }\n            texture.repeat.set(rx, ry);\n            texture.offset.set((1 - rx) / 2, (1 - ry) / 2);\n            resolve(texture);\n          },\n          undefined,\n          () => reject(new Error(`Unable to load paper image: ${url}. Remote images must allow CORS.`))\n        );\n      });\n    }\n    if (src) {\n      Promise.all([load(src), backSrc ? load(backSrc) : Promise.resolve(null)])\n        .then(([frontTexture, backTexture]) => {\n          if (disposed) return;\n          if (backTexture) {\n            backTexture.repeat.x *= -1;\n            backTexture.offset.x = 1 - backTexture.offset.x;\n          }\n          frontMaterial.map = frontTexture;\n          backMaterial.map = backTexture || frontTexture;\n          depthMaterial.map = frontTexture;\n          frontMaterial.needsUpdate = backMaterial.needsUpdate = depthMaterial.needsUpdate = true;\n          ready = true;\n          setStatus('ready');\n          hit.disabled = options.current.disabled;\n          wake();\n        })\n        .catch(error => {\n          if (disposed) return;\n          setStatus('error');\n          options.current.onError?.(error);\n        });\n    } else {\n      setStatus('error');\n      options.current.onError?.(new Error('PaperCrumple requires an image src.'));\n    }\n    return () => {\n      disposed = true;\n      cancelAnimationFrame(frame);\n      if (pointerId !== null && hit.hasPointerCapture(pointerId)) hit.releasePointerCapture(pointerId);\n      resetRef.current = cancelRef.current = null;\n      observer.disconnect();\n      intersectionObserver.disconnect();\n      hit.removeEventListener('pointerdown', pointerDown);\n      hit.removeEventListener('pointermove', pointerMove);\n      hit.removeEventListener('pointerup', pointerUp);\n      hit.removeEventListener('pointercancel', pointerCancel);\n      hit.removeEventListener('lostpointercapture', pointerUp);\n      hit.removeEventListener('keydown', keyDown);\n      hit.removeEventListener('keyup', keyUp);\n      hit.removeEventListener('blur', blur);\n      window.removeEventListener('blur', cancel);\n      document.removeEventListener('visibilitychange', visibility);\n      media.removeEventListener('change', motionChange);\n      canvas.removeEventListener('webglcontextlost', loseContext);\n      canvas.removeEventListener('webglcontextrestored', restoreContext);\n      geometry.dispose();\n      floorGeometry.dispose();\n      frontMaterial.dispose();\n      backMaterial.dispose();\n      depthMaterial.dispose();\n      floorMaterial.dispose();\n      grain.dispose();\n      textures.forEach(texture => texture.dispose());\n      light.shadow.dispose();\n      renderer.dispose();\n    };\n  }, [\n    src,\n    backSrc,\n    width,\n    height,\n    imageFit,\n    foldCount,\n    foldSharpness,\n    wrinkleDepth,\n    paperColor,\n    roughness,\n    paperTexture,\n    lightIntensity,\n    lightAngle,\n    shadow,\n    shadowOpacity,\n    rotation,\n    seed,\n    detail\n  ]);\n  useEffect(() => {\n    resetRef.current?.();\n  }, [resetKey]);\n  useEffect(() => {\n    if (disabled) cancelRef.current?.();\n  }, [disabled]);\n  const shadowFilter = shadow\n    ? `drop-shadow(0 6px 10px rgb(0 0 0 / ${clamp(finite(shadowOpacity, 0.08), 0, 1)}))`\n    : undefined;\n  return (\n    <div\n      ref={rootRef}\n      className={`relative isolate w-full overflow-hidden ${className}`}\n      style={{ height: sceneHeight, ...style }}\n      data-status={status}\n    >\n      <canvas\n        ref={canvasRef}\n        className=\"pointer-events-none absolute inset-0 block h-full w-full\"\n        aria-hidden=\"true\"\n        style={{ visibility: status === 'ready' ? 'visible' : 'hidden', filter: shadowFilter }}\n      />\n      {status !== 'ready' && (\n        <img\n          className=\"pointer-events-none absolute left-1/2 top-1/2 h-[var(--pc-image-height,72%)] w-[var(--pc-image-width,60%)] select-none\"\n          src={src || undefined}\n          alt={alt}\n          draggable={false}\n          style={{\n            objectFit: imageFit,\n            transform: `translate(-50%, -50%) rotate(${rotation}deg)`,\n            filter: shadowFilter\n          }}\n        />\n      )}\n      <button\n        ref={hitRef}\n        type=\"button\"\n        className=\"peer absolute left-0 top-0 m-0 cursor-grab touch-none appearance-none select-none rounded-[2px] border-0 bg-transparent p-0 !shadow-none !outline-none [-webkit-tap-highlight-color:transparent] data-[held=true]:cursor-grabbing focus:!outline-none focus-visible:!outline-none disabled:cursor-default\"\n        disabled={disabled || status !== 'ready'}\n        aria-label={`${alt}. Hold to crumple and drag. Keyboard: hold Space or Enter, arrow keys to move, Escape to reset.`}\n        aria-pressed=\"false\"\n        style={{ visibility: status === 'ready' ? 'visible' : 'hidden' }}\n      />\n      <span\n        className=\"pointer-events-none absolute bottom-6 left-[calc(50%-2px)] h-1 w-1 rounded-full bg-current opacity-0 peer-focus-visible:opacity-55 peer-data-[pointer=true]:!opacity-0\"\n        aria-hidden=\"true\"\n      />\n      <span className=\"sr-only\" role=\"status\">\n        {status === 'loading'\n          ? 'Loading interactive paper.'\n          : status === 'error'\n            ? 'Interactive paper unavailable. Showing the original image.'\n            : ''}\n      </span>\n    </div>\n  );\n};\nexport default PaperCrumple;\n"
		}
	],
	"registryDependencies": [],
	"dependencies": [
		"three@^0.180.0"
	]
}