{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "cloth-reveal",
  "title": "Cloth Reveal",
  "description": "A satin sheet draped over the content is yanked from one corner, folding and fluttering as a simulated cloth before it clears the frame.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/cloth-reveal/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  random,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type ClothRevealDirection = \"right\" | \"left\" | \"up\" | \"down\";\n\nexport type ClothRevealProps = {\n  /** Content hidden under the sheet. Falls back to a placeholder \"remocn\" lockup. */\n  children?: React.ReactNode;\n  clothColor?: string;\n  /** Underside tint, seen where the sheet folds over. */\n  undersideColor?: string;\n  /** Background behind the children. */\n  background?: string;\n  /** Which way the sheet is yanked. */\n  direction?: ClothRevealDirection;\n  /** Frame (at 30fps) when the tug starts. */\n  delay?: number;\n  seed?: number;\n  speed?: number;\n};\n\nexport const clothRevealDefaults = {\n  clothColor: \"#E5322D\",\n  undersideColor: \"#8E1A16\",\n  background: \"#111111\",\n  direction: \"right\" as ClothRevealDirection,\n  delay: 6,\n  seed: 2,\n  speed: 1,\n};\n\nconst COLS = 40;\nconst ROWS = 24;\nconst FRAMES = 72;\nconst SUBSTEPS = 5;\nconst ITERATIONS = 12;\n\ntype Sim = { frames: Float32Array[]; heights: Float32Array };\n\nconst PATHS: Record<\n  ClothRevealDirection,\n  { corner: [number, number]; dir: [number, number] }\n> = {\n  right: { corner: [1, 0], dir: [1, -0.32] },\n  left: { corner: [0, 0], dir: [-1, -0.32] },\n  up: { corner: [1, 0], dir: [0.4, -1] },\n  down: { corner: [0, 1], dir: [-0.4, 1] },\n};\n\nconst smooth = (v: number) => {\n  const t = Math.min(1, Math.max(0, v));\n  return t * t * (3 - 2 * t);\n};\n\n// Verlet cloth in table space: x/y on the table, z up toward the camera. The\n// sheet rests on a soft bump (whatever it hides) with seeded wrinkles baked\n// into its rest shape, so frame 0 is already at equilibrium.\nfunction simulate(\n  width: number,\n  height: number,\n  direction: ClothRevealDirection,\n  delay: number,\n  seed: number,\n): Sim {\n  const n = COLS * ROWS;\n  const pos = new Float64Array(n * 3);\n  const prev = new Float64Array(n * 3);\n  const floorZ = new Float64Array(n);\n  const pinned = new Uint8Array(n);\n  const unit = Math.min(width, height);\n  const bump = (x: number, y: number) => {\n    const dx = (x - width / 2) / (width * 0.3);\n    const dy = (y - height / 2) / (height * 0.34);\n    return unit * 0.09 * Math.exp(-(dx * dx + dy * dy) * 1.4);\n  };\n  // Folds radiate from the hidden object and deepen toward the hem, plus a\n  // couple of long soft waves so the sheet never looks ironed.\n  const folds = 7 + Math.floor(random(`cloth-${seed}-n`) * 4);\n  const twist = random(`cloth-${seed}-t`) * Math.PI * 2;\n  const waves = [0, 1].map((k) => ({\n    a: random(`cloth-${seed}-a${k}`) * Math.PI,\n    f: 0.8 + random(`cloth-${seed}-f${k}`) * 1.2,\n    p: random(`cloth-${seed}-p${k}`) * Math.PI * 2,\n  }));\n\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      const i = r * COLS + c;\n      const u = c / (COLS - 1);\n      const v = r / (ROWS - 1);\n      const x = width * (-0.06 + u * 1.12);\n      const y = height * (-0.08 + v * 1.16);\n      const dx = (x - width / 2) / width;\n      const dy = (y - height / 2) / width;\n      const rho = Math.hypot(dx, dy);\n      const theta = Math.atan2(dy, dx);\n      const radial =\n        (0.5 + 0.5 * Math.sin(theta * folds + twist + rho * 9)) ** 1.6 *\n        smooth((rho - 0.12) / 0.35);\n      let wave = 0;\n      for (const w of waves) {\n        const s = u * Math.cos(w.a) + v * Math.sin(w.a);\n        wave += 0.5 + 0.5 * Math.sin(s * w.f * Math.PI * 2 + w.p);\n      }\n      floorZ[i] = unit * (0.045 * radial + 0.012 * wave);\n      pos[i * 3] = x;\n      pos[i * 3 + 1] = y;\n      pos[i * 3 + 2] = floorZ[i] + bump(x, y);\n    }\n  }\n  prev.set(pos);\n  const heights = new Float32Array(n);\n  for (let i = 0; i < n; i++) heights[i] = pos[i * 3 + 2];\n\n  const links: number[] = [];\n  const add = (a: number, b: number, stiff: number) => {\n    const dx = pos[a * 3] - pos[b * 3];\n    const dy = pos[a * 3 + 1] - pos[b * 3 + 1];\n    const dz = pos[a * 3 + 2] - pos[b * 3 + 2];\n    links.push(a, b, Math.hypot(dx, dy, dz), stiff);\n  };\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      const i = r * COLS + c;\n      if (c + 1 < COLS) add(i, i + 1, 1);\n      if (r + 1 < ROWS) add(i, i + COLS, 1);\n      if (c + 1 < COLS && r + 1 < ROWS) {\n        add(i, i + COLS + 1, 0.7);\n        add(i + 1, i + COLS, 0.7);\n      }\n      if (c + 2 < COLS) add(i, i + 2, 0.25);\n      if (r + 2 < ROWS) add(i, i + COLS * 2, 0.25);\n    }\n  }\n\n  // The hand pinches a small patch around the corner, not a single vertex.\n  const path = PATHS[direction];\n  const cc = path.corner[0] * (COLS - 1);\n  const cr = path.corner[1] * (ROWS - 1);\n  const grip: number[] = [];\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      if (Math.abs(c - cc) + Math.abs(r - cr) <= 3) grip.push(r * COLS + c);\n    }\n  }\n  const gripStart = grip.map((i) => [\n    pos[i * 3],\n    pos[i * 3 + 1],\n    pos[i * 3 + 2],\n  ]);\n  for (const i of grip) pinned[i] = 1;\n  const len = Math.hypot(path.dir[0], path.dir[1]);\n  const dir = [path.dir[0] / len, path.dir[1] / len];\n  const reach = Math.abs(dir[0]) * width + Math.abs(dir[1]) * height;\n  const g = unit * 0.006;\n  const dt = 1 / SUBSTEPS;\n\n  const hand = (t: number) => {\n    const s = Math.max(0, t - delay);\n    // Quick lift, then an accelerating yank that keeps going off-frame.\n    const lift = unit * 0.5 * smooth(s / 9);\n    const travel =\n      s < 30\n        ? reach * 1.25 * (s / 30) ** 2\n        : reach * 1.25 * (1 + ((s - 30) * 2) / 30);\n    return [dir[0] * travel, dir[1] * travel, lift];\n  };\n\n  const frames: Float32Array[] = [];\n  const record = () => {\n    const out = new Float32Array(n * 3);\n    for (let i = 0; i < n * 3; i++) out[i] = pos[i];\n    frames.push(out);\n  };\n  record();\n\n  for (let f = 1; f <= FRAMES; f++) {\n    for (let s = 1; s <= SUBSTEPS; s++) {\n      const t = f - 1 + s * dt;\n      const [hx, hy, hz] = hand(t);\n      for (let i = 0; i < n; i++) {\n        const k = i * 3;\n        if (pinned[i]) continue;\n        const z = pos[k + 2];\n        const lifted = z - floorZ[i] - bump(pos[k], pos[k + 1]) > 1;\n        const drag = lifted ? 0.985 : 0.6;\n        const vx = (pos[k] - prev[k]) * drag;\n        const vy = (pos[k + 1] - prev[k + 1]) * drag;\n        const vz = (pos[k + 2] - prev[k + 2]) * 0.985;\n        prev[k] = pos[k];\n        prev[k + 1] = pos[k + 1];\n        prev[k + 2] = z;\n        // Flutter: a travelling ripple that only bites on fast-moving cloth.\n        const speedXY = Math.hypot(vx, vy);\n        const flutter =\n          Math.sin(pos[k] * 0.02 + pos[k + 1] * 0.013 + t * 1.7) *\n          speedXY *\n          0.06;\n        pos[k] += vx;\n        pos[k + 1] += vy;\n        pos[k + 2] += vz + flutter - g * dt * dt;\n      }\n      grip.forEach((i, j) => {\n        pos[i * 3] = gripStart[j][0] + hx;\n        pos[i * 3 + 1] = gripStart[j][1] + hy;\n        pos[i * 3 + 2] = gripStart[j][2] + hz;\n      });\n      for (let it = 0; it < ITERATIONS; it++) {\n        for (let l = 0; l < links.length; l += 4) {\n          const a = links[l];\n          const b = links[l + 1];\n          const rest = links[l + 2];\n          const stiff = links[l + 3];\n          const ka = a * 3;\n          const kb = b * 3;\n          const dx = pos[kb] - pos[ka];\n          const dy = pos[kb + 1] - pos[ka + 1];\n          const dz = pos[kb + 2] - pos[ka + 2];\n          const d = Math.hypot(dx, dy, dz) || 1e-6;\n          const wa = pinned[a] ? 0 : 1;\n          const wb = pinned[b] ? 0 : 1;\n          if (wa + wb === 0) continue;\n          const diff = ((d - rest) / d / (wa + wb)) * stiff;\n          pos[ka] += dx * diff * wa;\n          pos[ka + 1] += dy * diff * wa;\n          pos[ka + 2] += dz * diff * wa;\n          pos[kb] -= dx * diff * wb;\n          pos[kb + 1] -= dy * diff * wb;\n          pos[kb + 2] -= dz * diff * wb;\n        }\n        for (let i = 0; i < n; i++) {\n          const k = i * 3;\n          const ground = floorZ[i] + bump(pos[k], pos[k + 1]);\n          if (pos[k + 2] < ground) pos[k + 2] = ground;\n        }\n      }\n    }\n    record();\n  }\n  return { frames, heights };\n}\n\nfunction parseHex(hex: string): [number, number, number] {\n  const h = hex.replace(\"#\", \"\");\n  const full = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.slice(0, 6);\n  const v = Number.parseInt(full, 16);\n  return [(v >> 16) & 255, (v >> 8) & 255, v & 255];\n}\n\n// Wordmark color for the placeholder: dark ink on light backgrounds.\nfunction placeholderText(background: string): string {\n  const [r, g, b] = parseHex(background);\n  return 0.299 * r + 0.587 * g + 0.114 * b > 150 ? \"#111111\" : \"#FFFFFF\";\n}\n\nconst LIGHT = (() => {\n  const l = [-0.45, -0.6, 0.75];\n  const m = Math.hypot(l[0], l[1], l[2]);\n  return l.map((c) => c / m);\n})();\nconst HALF = (() => {\n  const h = [LIGHT[0], LIGHT[1], LIGHT[2] + 1];\n  const m = Math.hypot(h[0], h[1], h[2]);\n  return h.map((c) => c / m);\n})();\n\nconst PLACEHOLDER_FONT =\n  \"-apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, Helvetica, Arial, sans-serif\";\n\n// Neutral lockup shown when no children are passed.\nfunction PlaceholderLockup({\n  mark,\n  dot,\n  text,\n  scale = 1,\n}: {\n  mark: string;\n  dot: string;\n  text: string;\n  scale?: number;\n}) {\n  return (\n    <div style={{ display: \"flex\", alignItems: \"center\", gap: 28 * scale }}>\n      <div\n        style={{\n          width: 120 * scale,\n          height: 120 * scale,\n          borderRadius: 32 * scale,\n          backgroundColor: mark,\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n        }}\n      >\n        <div\n          style={{\n            width: 36 * scale,\n            height: 36 * scale,\n            borderRadius: \"50%\",\n            backgroundColor: dot,\n          }}\n        />\n      </div>\n      <span\n        style={{\n          fontFamily: PLACEHOLDER_FONT,\n          fontSize: 104 * scale,\n          fontWeight: 800,\n          letterSpacing: \"-0.04em\",\n          color: text,\n        }}\n      >\n        remocn\n      </span>\n    </div>\n  );\n}\n\nexport function ClothReveal({\n  children,\n  clothColor = clothRevealDefaults.clothColor,\n  undersideColor = clothRevealDefaults.undersideColor,\n  background = clothRevealDefaults.background,\n  direction = clothRevealDefaults.direction,\n  delay = clothRevealDefaults.delay,\n  seed = clothRevealDefaults.seed,\n  speed = clothRevealDefaults.speed,\n}: ClothRevealProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const uid = `cloth-reveal-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const shadowId = `${uid}-shadow`;\n  const softenId = `${uid}-soften`;\n  const sim = useMemo(\n    () => simulate(width, height, direction, delay, seed),\n    [width, height, direction, delay, seed],\n  );\n  const t = Math.min(FRAMES, Math.max(0, (frame * speed * 30) / fps));\n  const f0 = Math.floor(t);\n  const f1 = Math.min(FRAMES, f0 + 1);\n  const mix = t - f0;\n  const A = sim.frames[f0];\n  const B = sim.frames[f1];\n\n  // Pinhole camera above the table: higher cloth projects larger.\n  const camera = Math.max(width, height) * 1.6;\n  const cx = width / 2;\n  const cy = height / 2;\n  const n = COLS * ROWS;\n  const world = new Float64Array(n * 3);\n  const screen = new Float64Array(n * 2);\n  for (let i = 0; i < n * 3; i++) world[i] = A[i] + (B[i] - A[i]) * mix;\n  for (let i = 0; i < n; i++) {\n    const z = Math.min(world[i * 3 + 2], camera * 0.8);\n    const s = camera / (camera - z);\n    screen[i * 2] = cx + (world[i * 3] - cx) * s;\n    screen[i * 2 + 1] = cy + (world[i * 3 + 1] - cy) * s;\n  }\n\n  const front = parseHex(clothColor);\n  const back = parseHex(undersideColor);\n  // Per-vertex normals from central differences; each quad averages its four\n  // corners, which hides the mesh facets.\n  const shades = new Float64Array(n * 3);\n  const at = (c: number, r: number, k: number) =>\n    world[\n      (Math.min(ROWS - 1, Math.max(0, r)) * COLS +\n        Math.min(COLS - 1, Math.max(0, c))) *\n        3 +\n        k\n    ];\n  for (let r = 0; r < ROWS; r++) {\n    for (let c = 0; c < COLS; c++) {\n      const ux = at(c + 1, r, 0) - at(c - 1, r, 0);\n      const uy = at(c + 1, r, 1) - at(c - 1, r, 1);\n      const uz = at(c + 1, r, 2) - at(c - 1, r, 2);\n      const vx = at(c, r + 1, 0) - at(c, r - 1, 0);\n      const vy = at(c, r + 1, 1) - at(c, r - 1, 1);\n      const vz = at(c, r + 1, 2) - at(c, r - 1, 2);\n      let nx = uy * vz - uz * vy;\n      let ny = uz * vx - ux * vz;\n      let nz = ux * vy - uy * vx;\n      const m = Math.hypot(nx, ny, nz) || 1;\n      const flip = nz < 0 ? -1 : 1;\n      nx = (nx / m) * flip;\n      ny = (ny / m) * flip;\n      nz = (nz / m) * flip;\n      const diffuse = Math.max(\n        0,\n        nx * LIGHT[0] + ny * LIGHT[1] + nz * LIGHT[2],\n      );\n      const spec =\n        Math.max(0, nx * HALF[0] + ny * HALF[1] + nz * HALF[2]) ** 36 * 0.28;\n      const base = flip < 0 ? back : front;\n      const shade = 0.22 + 0.95 * diffuse;\n      const i = r * COLS + c;\n      for (let k = 0; k < 3; k++) {\n        shades[i * 3 + k] = Math.min(255, base[k] * shade + 255 * spec);\n      }\n    }\n  }\n\n  const quads: { d: string; fill: string; z: number; shadow: string | null }[] =\n    [];\n  const offscreen = (x: number, y: number) =>\n    x < -width * 0.1 ||\n    x > width * 1.1 ||\n    y < -height * 0.1 ||\n    y > height * 1.1;\n\n  for (let r = 0; r < ROWS - 1; r++) {\n    for (let c = 0; c < COLS - 1; c++) {\n      const ids = [\n        r * COLS + c,\n        r * COLS + c + 1,\n        (r + 1) * COLS + c + 1,\n        (r + 1) * COLS + c,\n      ];\n      const pts = ids.map((i) => [screen[i * 2], screen[i * 2 + 1]]);\n      if (pts.every(([x, y]) => offscreen(x, y))) {\n        const xs = pts.map((p) => p[0]);\n        const ys = pts.map((p) => p[1]);\n        const spans =\n          (Math.min(...xs) < 0 && Math.max(...xs) > width) ||\n          (Math.min(...ys) < 0 && Math.max(...ys) > height);\n        if (!spans) continue;\n      }\n      const rgb = [0, 1, 2].map((k) =>\n        Math.round(ids.reduce((sum, i) => sum + shades[i * 3 + k], 0) / 4),\n      );\n      const fill = `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`;\n      const P = ids.map((i) => [\n        world[i * 3],\n        world[i * 3 + 1],\n        world[i * 3 + 2],\n      ]);\n      const z = (P[0][2] + P[1][2] + P[2][2] + P[3][2]) / 4;\n      const d = `M${pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(\"L\")}Z`;\n      const lift = z - (sim.heights[ids[0]] ?? 0);\n      const shadow =\n        lift > 4\n          ? `M${P.map(([x, y, h]) => `${(x + h * 0.35).toFixed(1)},${(y + h * 0.45).toFixed(1)}`).join(\"L\")}Z`\n          : null;\n      quads.push({ d, fill, z, shadow });\n    }\n  }\n  quads.sort((a, b) => a.z - b.z);\n\n  return (\n    <AbsoluteFill style={{ backgroundColor: background, overflow: \"hidden\" }}>\n      <AbsoluteFill style={{ alignItems: \"center\", justifyContent: \"center\" }}>\n        {children ?? (\n          <PlaceholderLockup\n            mark={clothColor}\n            dot={background}\n            text={placeholderText(background)}\n          />\n        )}\n      </AbsoluteFill>\n      <svg\n        width={width}\n        height={height}\n        viewBox={`0 0 ${width} ${height}`}\n        style={{ position: \"absolute\", inset: 0 }}\n        aria-hidden\n      >\n        <defs>\n          <filter id={shadowId} x=\"-20%\" y=\"-20%\" width=\"140%\" height=\"140%\">\n            <feGaussianBlur stdDeviation={Math.min(width, height) * 0.02} />\n          </filter>\n          {/* Normalized blur: blurring premultiplied color and then forcing\n              alpha to 1 averages only covered pixels, which smooths the facet\n              steps without bleeding the background in. The source alpha then\n              restores the crisp silhouette. */}\n          <filter\n            id={softenId}\n            filterUnits=\"userSpaceOnUse\"\n            x={-width}\n            y={-height}\n            width={width * 3}\n            height={height * 3}\n            colorInterpolationFilters=\"sRGB\"\n          >\n            <feGaussianBlur\n              in=\"SourceGraphic\"\n              stdDeviation={(width * 1.12) / (COLS - 1) / 2.2}\n              result=\"blur\"\n            />\n            <feColorMatrix\n              in=\"blur\"\n              type=\"matrix\"\n              values=\"1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 1\"\n              result=\"smooth\"\n            />\n            <feGaussianBlur\n              in=\"SourceAlpha\"\n              stdDeviation={Math.min(width, height) / 600}\n              result=\"edge\"\n            />\n            <feComposite in=\"smooth\" in2=\"edge\" operator=\"in\" />\n          </filter>\n        </defs>\n        <g filter={`url(#${shadowId})`} opacity={0.45}>\n          {quads.map((q, i) =>\n            q.shadow ? <path key={i} d={q.shadow} fill=\"#000000\" /> : null,\n          )}\n        </g>\n        {/* Aliased quads share edges without hairline seams. */}\n        <g filter={`url(#${softenId})`}>\n          {quads.map((q, i) => (\n            <path key={i} d={q.d} fill={q.fill} shapeRendering=\"crispEdges\" />\n          ))}\n        </g>\n      </svg>\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/cloth-reveal.tsx"
    }
  ],
  "type": "registry:component"
}
