{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "crumple-toss",
  "title": "Crumple Toss",
  "description": "Crushes an element into a ball of paper and throws it out of frame on an arc, leaving the scene around it running.",
  "dependencies": ["remotion"],
  "registryDependencies": ["@remocn/stop-motion"],
  "files": [
    {
      "path": "registry/remocn/crumple-toss/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useCurrentFrame } from \"remotion\";\nimport { DEFAULT_STEP, hashRange, qstep } from \"@/lib/remocn/stop-motion\";\n\nconst GRAVITY = 0.9;\nconst FLIGHT_SHRINK = 0.5;\nconst EXIT_OPACITY = 0.4;\n\nconst FOLD_TILT = 46;\nconst WAD_REACH = 0.27;\nconst WAD_SPREAD = 0.07;\nconst ANGLE_JITTER = 0.22;\nconst SCALE_SPREAD = 0.25;\nconst DEFAULT_RANDOMNESS = 0.6;\nconst DEFAULT_LAYERS = 2;\nconst LAYER_TILT_FLOOR = 0.45;\nconst FACET_SIZE = 0.5;\nconst FACET_CAP = 0.5;\n\nconst TONE_PATTERN = [0, 1, 3, 1, 0, 2, 1, 2, 0];\n\nconst TONES = [\n  { lit: \"rgba(255,255,255,0.66)\", dark: \"rgba(255,255,255,0.18)\" },\n  { lit: \"rgba(255,255,255,0.30)\", dark: \"rgba(38,36,44,0.10)\" },\n  { lit: \"rgba(255,255,255,0.08)\", dark: \"rgba(38,36,44,0.20)\" },\n  { lit: \"rgba(38,36,44,0.06)\", dark: \"rgba(38,36,44,0.30)\" },\n];\n\nconst LIGHT_ANGLE = -55;\nconst CREASE_BLEND = 9;\n\nexport type CrumplePhase = \"idle\" | \"crumple\" | \"toss\" | \"gone\";\n\nexport type CrumplePose = {\n  phase: CrumplePhase;\n  crush: number;\n  scale: number;\n  rotate: number;\n  x: number;\n  y: number;\n  opacity: number;\n};\n\nexport type CrumpleSegment = {\n  clip: [number, number][];\n  origin: [number, number];\n  dx: number;\n  dy: number;\n  scale: number;\n  rotate: number;\n  tone: number;\n  crease: number;\n};\n\nexport type CrumpleTossTiming = {\n  at: number;\n  crumpleSteps?: number;\n  tossSteps?: number;\n  direction?: number;\n  distance?: number;\n  spin?: number;\n  crushTo?: number;\n  seed?: string;\n  step?: number;\n};\n\nconst rayToEdge = (\n  angle: number,\n  halfWidth: number,\n  halfHeight: number,\n): [number, number] => {\n  const cos = Math.cos(angle);\n  const sin = Math.sin(angle);\n  const t = Math.min(\n    halfWidth / (Math.abs(cos) || 1e-9),\n    halfHeight / (Math.abs(sin) || 1e-9),\n  );\n  return [cos * t, sin * t];\n};\n\nconst normalize = (angle: number) => {\n  const turn = Math.PI * 2;\n  return ((angle % turn) + turn) % turn;\n};\n\nconst shrinkToward = (\n  [x, y]: [number, number],\n  factor: number,\n): [number, number] => [x * factor, y * factor];\n\nconst averageOf = (points: [number, number][]): [number, number] => [\n  points.reduce((sum, p) => sum + p[0], 0) / points.length,\n  points.reduce((sum, p) => sum + p[1], 0) / points.length,\n];\n\nexport function crumpleSegments(args: {\n  width: number;\n  height: number;\n  segments?: number;\n  layers?: number;\n  crushTo?: number;\n  randomness?: number;\n  seed?: string;\n}): CrumpleSegment[] {\n  const count = Math.max(3, Math.round(args.segments ?? 9));\n  const layers = Math.max(1, Math.round(args.layers ?? DEFAULT_LAYERS));\n  const crushTo = args.crushTo ?? 0.34;\n  const chaos = Math.min(1, Math.max(0, args.randomness ?? DEFAULT_RANDOMNESS));\n  const seed = args.seed ?? \"toss\";\n  const halfWidth = args.width / 2;\n  const halfHeight = args.height / 2;\n  const wad = Math.min(args.width, args.height) * crushTo;\n\n  const angles = Array.from(\n    { length: count },\n    (_, i) =>\n      (i / count) * Math.PI * 2 +\n      hashRange(`${seed}:ang${i}`, -ANGLE_JITTER, ANGLE_JITTER) * chaos,\n  );\n\n  const corners: [number, number][] = [\n    [halfWidth, halfHeight],\n    [-halfWidth, halfHeight],\n    [-halfWidth, -halfHeight],\n    [halfWidth, -halfHeight],\n  ];\n\n  const toPercent = ([x, y]: [number, number]): [number, number] => [\n    ((x + halfWidth) / args.width) * 100,\n    ((y + halfHeight) / args.height) * 100,\n  ];\n\n  const panels: CrumpleSegment[] = [];\n\n  angles.forEach((angle, i) => {\n    const nextAngle =\n      angles[(i + 1) % count] + (i === count - 1 ? Math.PI * 2 : 0);\n    const from = rayToEdge(angle, halfWidth, halfHeight);\n    const to = rayToEdge(nextAngle, halfWidth, halfHeight);\n\n    const span = normalize(nextAngle - angle);\n    const between = corners\n      .map((corner): [[number, number], number] => [\n        corner,\n        normalize(Math.atan2(corner[1], corner[0]) - angle),\n      ])\n      .filter(([, spanned]) => spanned > 0 && spanned < span)\n      .sort((a, b) => a[1] - b[1])\n      .map(([corner]) => corner);\n\n    const rim: [number, number][] = [from, ...between, to];\n\n    for (let layer = 0; layer < layers; layer++) {\n      const inner = layer / layers;\n      const outer = (layer + 1) / layers;\n      const depth = (layer + 1) / layers;\n      const shape: [number, number][] =\n        layer === 0\n          ? [[0, 0], ...rim.map((point) => shrinkToward(point, outer))]\n          : [\n              ...rim.map((point) => shrinkToward(point, outer)),\n              ...[...rim].reverse().map((point) => shrinkToward(point, inner)),\n            ];\n\n      const centroid = averageOf(shape);\n      const extent =\n        Math.max(\n          ...shape.map((point) =>\n            Math.hypot(point[0] - centroid[0], point[1] - centroid[1]),\n          ),\n        ) || 1;\n      const key = `${seed}:${i}:${layer}`;\n\n      const reach =\n        wad *\n        (WAD_REACH * depth +\n          hashRange(`${key}:reach`, -WAD_SPREAD, WAD_SPREAD) * chaos * depth);\n      const heading = Math.atan2(centroid[1], centroid[0]);\n\n      panels.push({\n        clip: shape.map(toPercent),\n        origin: toPercent(centroid),\n        dx: Math.cos(heading) * reach - centroid[0],\n        dy: Math.sin(heading) * reach - centroid[1],\n        scale:\n          Math.min(FACET_CAP, (wad * FACET_SIZE) / extent) *\n          (1 + hashRange(`${key}:size`, -SCALE_SPREAD, SCALE_SPREAD) * chaos),\n        rotate:\n          chaos === 0\n            ? 0\n            : hashRange(`${seed}:tilt${i}`, -FOLD_TILT, FOLD_TILT) *\n              chaos *\n              (LAYER_TILT_FLOOR + (1 - LAYER_TILT_FLOOR) * depth),\n        tone: Math.min(\n          TONES.length - 1,\n          TONE_PATTERN[panels.length % TONE_PATTERN.length] +\n            Math.ceil((1 - depth) * 2),\n        ),\n        crease: 0.5 + hashRange(`${key}:crease`, -0.2, 0.2) * chaos,\n      });\n    }\n  });\n\n  return panels;\n}\n\nexport function creaseShading(\n  piece: Pick<CrumpleSegment, \"tone\" | \"rotate\" | \"crease\">,\n  crush: number,\n): string {\n  const tone = TONES[piece.tone];\n  const fold = piece.crease * 100;\n  const lit = Math.max(0, fold - CREASE_BLEND).toFixed(1);\n  const dark = Math.min(100, fold + CREASE_BLEND).toFixed(1);\n  const angle = (LIGHT_ANGLE - piece.rotate * crush).toFixed(1);\n  return `linear-gradient(${angle}deg, ${tone.lit} 0%, ${tone.lit} ${lit}%, ${tone.dark} ${dark}%, ${tone.dark} 100%)`;\n}\n\nexport function crumpleTossLanding(options?: {\n  direction?: number;\n  distance?: number;\n}): { x: number; y: number } {\n  const distance = options?.distance ?? 900;\n  const radians = ((options?.direction ?? -35) * Math.PI) / 180;\n  return {\n    x: distance * Math.cos(radians),\n    y: distance * Math.sin(radians) + GRAVITY * distance,\n  };\n}\n\nexport function crumpleTossPose(\n  args: CrumpleTossTiming & { frame: number },\n): CrumplePose {\n  const crumpleSteps = args.crumpleSteps ?? 4;\n  const tossSteps = args.tossSteps ?? 5;\n  const direction = args.direction ?? -35;\n  const distance = args.distance ?? 900;\n  const spin = args.spin ?? 220;\n  const step = args.step ?? DEFAULT_STEP;\n\n  if (args.frame < args.at) {\n    return {\n      phase: \"idle\",\n      crush: 0,\n      scale: 1,\n      rotate: 0,\n      x: 0,\n      y: 0,\n      opacity: 1,\n    };\n  }\n\n  const pose = qstep(args.frame - args.at, step);\n\n  if (pose < crumpleSteps) {\n    return {\n      phase: \"crumple\",\n      crush: (pose + 1) / crumpleSteps,\n      scale: 1,\n      rotate: 0,\n      x: 0,\n      y: 0,\n      opacity: 1,\n    };\n  }\n\n  if (pose < crumpleSteps + tossSteps) {\n    const t = (pose - crumpleSteps + 1) / tossSteps;\n    const radians = (direction * Math.PI) / 180;\n    return {\n      phase: \"toss\",\n      crush: 1,\n      scale: 1 - FLIGHT_SHRINK * t,\n      rotate: spin * t,\n      x: distance * Math.cos(radians) * t,\n      y: distance * Math.sin(radians) * t + GRAVITY * distance * t * t,\n      opacity: t === 1 ? EXIT_OPACITY : 1,\n    };\n  }\n\n  return {\n    phase: \"gone\",\n    crush: 1,\n    scale: 0,\n    rotate: 0,\n    x: 0,\n    y: 0,\n    opacity: 0,\n  };\n}\n\nexport interface CrumpleTossProps extends CrumpleTossTiming {\n  children: React.ReactNode;\n  width: number;\n  height: number;\n  segments?: number;\n  layers?: number;\n  randomness?: number;\n}\n\nexport function CrumpleToss({\n  children,\n  width,\n  height,\n  segments,\n  layers,\n  randomness,\n  ...timing\n}: CrumpleTossProps) {\n  const frame = useCurrentFrame();\n  const pose = crumpleTossPose({ frame, ...timing });\n  if (pose.phase === \"gone\") return null;\n\n  const pieces = crumpleSegments({\n    width,\n    height,\n    segments,\n    layers,\n    randomness,\n    crushTo: timing.crushTo,\n    seed: timing.seed,\n  });\n  const c = pose.crush;\n\n  return (\n    <div\n      style={{\n        position: \"relative\",\n        width,\n        height,\n        transformOrigin: \"center\",\n        opacity: pose.opacity,\n        transform: `translate(${pose.x}px, ${pose.y}px) rotate(${pose.rotate}deg) scale(${pose.scale})`,\n      }}\n    >\n      {pieces.map((piece, i) => (\n        <div\n          key={`piece:${i}`}\n          style={{\n            position: \"absolute\",\n            inset: 0,\n            clipPath: `polygon(${piece.clip.map(([x, y]) => `${x}% ${y}%`).join(\", \")})`,\n            transformOrigin: `${piece.origin[0]}% ${piece.origin[1]}%`,\n            transform: `translate(${piece.dx * c}px, ${piece.dy * c}px) rotate(${piece.rotate * c}deg) scale(${1 + (piece.scale - 1) * c})`,\n            filter:\n              c > 0\n                ? `drop-shadow(0 0 ${0.6 * c}px rgba(38,36,44,0.55))`\n                : undefined,\n          }}\n        >\n          <div style={{ position: \"absolute\", inset: 0 }}>{children}</div>\n          <div\n            style={{\n              position: \"absolute\",\n              inset: 0,\n              background: creaseShading(piece, c),\n              opacity: c,\n            }}\n          />\n        </div>\n      ))}\n    </div>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/crumple-toss.tsx"
    }
  ],
  "type": "registry:component"
}
