{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "icon-scatter",
  "title": "Icon Scatter",
  "description": "Scatter a field of icons over the cut, hiding the scene swap at the field's peak.",
  "dependencies": ["remotion", "@remotion/transitions"],
  "files": [
    {
      "path": "registry/remocn/icon-scatter/index.tsx",
      "content": "\"use client\";\n\nimport type {\n  TransitionPresentation,\n  TransitionPresentationComponentProps,\n} from \"@remotion/transitions\";\nimport type React from \"react\";\nimport { AbsoluteFill, Easing, interpolate, random } from \"remotion\";\n\n// ===========================================================================\n// icon-scatter — a scene change made of icons.\n//\n// The outgoing scene shatters into a deterministic field of line-icons that\n// draw themselves on, spin, and scatter outward; at mid-transition the field\n// is densest and covers the swap; then the icons fly off and draw away as the\n// incoming scene resolves through them. Motion-forward and self-contained —\n// the paths are inlined, the draw-on is CSS `pathLength` only, so the\n// transition ships as one file with no icon dependencies.\n//\n// Pace it with `linearTiming`/`springTiming` like any presentation. Reads\n// best over ~16–26 frames.\n// ===========================================================================\n\nconst clampOpts = {\n  extrapolateLeft: \"clamp\" as const,\n  extrapolateRight: \"clamp\" as const,\n};\n\n// Single-path Lucide-style glyphs (viewBox 0 0 24 24) — each draws cleanly\n// from one continuous stroke via a normalized dashoffset.\nconst ICON_PATHS: string[] = [\n  // star\n  \"M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z\",\n  // heart\n  \"M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5\",\n  // zap\n  \"M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z\",\n  // sparkles\n  \"M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z\",\n  // moon\n  \"M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401\",\n  // play\n  \"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\",\n  // crown\n  \"M11.562 3.266a.5.5 0 0 1 .876 0L15.39 8.87a1 1 0 0 0 1.516.294L21.183 5.5a.5.5 0 0 1 .798.519l-2.834 10.246a1 1 0 0 1-.956.734H5.81a1 1 0 0 1-.957-.734L2.02 6.02a.5.5 0 0 1 .798-.519l4.276 3.664a1 1 0 0 0 1.516-.294z\",\n  // circle\n  \"M12 2 A10 10 0 0 1 22 12 A10 10 0 0 1 12 22 A10 10 0 0 1 2 12 A10 10 0 0 1 12 2\",\n  // square (rounded)\n  \"M7 4h10a3 3 0 0 1 3 3v10a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V7a3 3 0 0 1 3-3z\",\n  // triangle\n  \"M13.73 4a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z\",\n  // plus\n  \"M12 5v14 M5 12h14\",\n];\n\nexport type IconScatterProps = {\n  /** How many icons populate the field. Default 15. */\n  count?: number;\n  /** Icon stroke color. Default near-white ink. */\n  color?: string;\n  /** The fill that hides the swap at the field's peak. Default warm obsidian. */\n  coverColor?: string;\n  /** Peak opacity of the cover fill (0–1). Lower = more of the swap shows. */\n  coverOpacity?: number;\n  /** Stroke width of the icons, in their 24px viewBox units. Default 2. */\n  strokeWidth?: number;\n  /** How far icons fly on scatter, in px. Default 260. */\n  flyDistance?: number;\n  /** Seed so two scatters in one video can differ. Default \"icon-scatter\". */\n  seed?: string;\n};\n\ntype Placed = {\n  path: string;\n  xPct: number;\n  yPct: number;\n  size: number;\n  spin: number; // degrees swept over the whole transition\n  dir: number; // outward scatter angle offset\n  strokeOpacity: number;\n  pStart: number; // when this icon begins drawing (p-space)\n  pOut: number; // when this icon begins flying out (p-space)\n};\n\n// Jittered grid so the field reads as scattered but never clumps or overlaps.\nfunction placeIcons(count: number, seed: string): Placed[] {\n  const cols = Math.ceil(Math.sqrt((count * 16) / 9));\n  const rows = Math.ceil(count / cols);\n  const out: Placed[] = [];\n  for (let i = 0; i < count; i++) {\n    const col = i % cols;\n    const row = Math.floor(i / cols);\n    const cellW = 100 / cols;\n    const cellH = 100 / rows;\n    const jx = (random(`${seed}-jx-${i}`) - 0.5) * cellW * 0.7;\n    const jy = (random(`${seed}-jy-${i}`) - 0.5) * cellH * 0.7;\n    out.push({\n      path: ICON_PATHS[\n        Math.floor(random(`${seed}-p-${i}`) * ICON_PATHS.length)\n      ],\n      xPct: cellW * (col + 0.5) + jx,\n      yPct: cellH * (row + 0.5) + jy,\n      size: 34 + random(`${seed}-s-${i}`) * 40,\n      spin: (random(`${seed}-r-${i}`) - 0.5) * 320,\n      dir: (random(`${seed}-d-${i}`) - 0.5) * 1.2,\n      strokeOpacity: 0.5 + random(`${seed}-o-${i}`) * 0.5,\n      pStart: random(`${seed}-a-${i}`) * 0.16,\n      pOut: 0.52 + random(`${seed}-b-${i}`) * 0.16,\n    });\n  }\n  return out;\n}\n\nconst IconField: React.FC<{\n  p: number;\n  placed: Placed[];\n  color: string;\n  strokeWidth: number;\n  flyDistance: number;\n}> = ({ p, placed, color, strokeWidth, flyDistance }) => (\n  <AbsoluteFill style={{ pointerEvents: \"none\" }}>\n    {placed.map((icon, i) => {\n      // Draw on over ~0.34 of p from the icon's start, then fly out.\n      const draw = interpolate(p, [icon.pStart, icon.pStart + 0.34], [0, 1], {\n        ...clampOpts,\n        easing: Easing.out(Easing.cubic),\n      });\n      const scaleIn = interpolate(\n        p,\n        [icon.pStart, icon.pStart + 0.3],\n        [0.4, 1],\n        {\n          ...clampOpts,\n          easing: Easing.out(Easing.back(1.6)),\n        },\n      );\n      const outP = interpolate(p, [icon.pOut, 1], [0, 1], {\n        ...clampOpts,\n        easing: Easing.in(Easing.cubic),\n      });\n\n      // Scatter outward from the frame center, along the icon's own radial.\n      const cx = icon.xPct - 50;\n      const cy = icon.yPct - 50;\n      const angle = Math.atan2(cy, cx) + icon.dir;\n      const fly = outP * flyDistance;\n      const tx = Math.cos(angle) * fly;\n      const ty = Math.sin(angle) * fly;\n\n      const spin =\n        icon.spin * interpolate(p, [icon.pStart, 1], [0, 1], clampOpts);\n      const opacity = Math.min(draw, 1 - outP) * icon.strokeOpacity;\n      if (opacity <= 0.001) return null;\n\n      return (\n        <svg\n          key={i}\n          width={icon.size}\n          height={icon.size}\n          viewBox=\"0 0 24 24\"\n          fill=\"none\"\n          stroke={color}\n          strokeWidth={strokeWidth}\n          strokeLinecap=\"round\"\n          strokeLinejoin=\"round\"\n          style={{\n            position: \"absolute\",\n            left: `${icon.xPct}%`,\n            top: `${icon.yPct}%`,\n            opacity,\n            overflow: \"visible\",\n            transform: `translate(-50%, -50%) translate(${tx}px, ${ty}px) scale(${\n              scaleIn * (1 - outP * 0.3)\n            }) rotate(${spin}deg)`,\n          }}\n        >\n          <path\n            d={icon.path}\n            pathLength={1}\n            strokeDasharray={1}\n            strokeDashoffset={1 - draw}\n          />\n        </svg>\n      );\n    })}\n  </AbsoluteFill>\n);\n\nconst IconScatterPresentation: React.FC<\n  TransitionPresentationComponentProps<IconScatterProps>\n> = ({\n  children,\n  presentationProgress,\n  presentationDirection,\n  passedProps,\n}) => {\n  const {\n    count = 15,\n    color = \"#fafafa\",\n    coverColor = \"#0a0a0a\",\n    coverOpacity = 0.92,\n    strokeWidth = 2,\n    flyDistance = 260,\n    seed = \"icon-scatter\",\n  } = passedProps;\n  const entering = presentationDirection === \"entering\";\n  const p = presentationProgress;\n\n  if (!entering) {\n    // The outgoing scene sinks back and blurs as the field rises over it.\n    const exitStyle: React.CSSProperties = {\n      opacity: interpolate(p, [0.28, 0.5], [1, 0], clampOpts),\n      scale: `${interpolate(p, [0, 0.5], [1, 0.94], {\n        ...clampOpts,\n        easing: Easing.in(Easing.cubic),\n      })})`,\n      filter: `blur(${interpolate(p, [0.1, 0.45], [0, 10], clampOpts)}px)`,\n    };\n    return <AbsoluteFill style={exitStyle}>{children}</AbsoluteFill>;\n  }\n\n  const placed = placeIcons(count, seed);\n\n  // The incoming scene resolves once the field starts clearing.\n  const childStyle: React.CSSProperties = {\n    opacity: interpolate(p, [0.5, 0.68], [0, 1], clampOpts),\n    scale: `${interpolate(p, [0.5, 1], [1.06, 1], {\n      ...clampOpts,\n      easing: Easing.out(Easing.cubic),\n    })})`,\n    filter: `blur(${interpolate(p, [0.5, 0.72], [8, 0], clampOpts)}px)`,\n  };\n\n  // The cover fill hides the swap while the field is densest.\n  const cover = interpolate(p, [0, 0.3, 0.62, 0.9], [0, 1, 1, 0], clampOpts);\n\n  return (\n    <AbsoluteFill>\n      <AbsoluteFill style={childStyle}>{children}</AbsoluteFill>\n      <AbsoluteFill\n        style={{\n          background: coverColor,\n          opacity: cover * coverOpacity,\n          pointerEvents: \"none\",\n        }}\n      />\n      <IconField\n        p={p}\n        placed={placed}\n        color={color}\n        strokeWidth={strokeWidth}\n        flyDistance={flyDistance}\n      />\n    </AbsoluteFill>\n  );\n};\n\nexport function iconScatter(\n  props: IconScatterProps = {},\n): TransitionPresentation<IconScatterProps> {\n  return {\n    component: IconScatterPresentation,\n    props,\n  };\n}\n",
      "type": "registry:component",
      "target": "components/remocn/icon-scatter.tsx"
    }
  ],
  "type": "registry:component"
}
