{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "impact-shake",
  "title": "Impact Shake",
  "description": "Heavy content slams in from above with squash, a damped camera jolt, a two-frame RGB split and dust puffs kicking out sideways.",
  "dependencies": ["remotion"],
  "files": [
    {
      "path": "registry/remocn/impact-shake/index.tsx",
      "content": "\"use client\";\n\nimport type React from \"react\";\nimport { useId, useMemo } from \"react\";\nimport {\n  AbsoluteFill,\n  Easing,\n  interpolate,\n  random,\n  spring,\n  useCurrentFrame,\n  useVideoConfig,\n} from \"remotion\";\n\nexport type ImpactShakeColors = {\n  background: string;\n  foreground: string;\n  splitA: string;\n  splitB: string;\n  dust: string;\n};\n\nexport type ImpactShakeProps = {\n  /** The heavy subject. Defaults to a big white word. */\n  children?: React.ReactNode;\n  /** Word used when no children are passed. */\n  text?: string;\n  /** Shake, split and dust strength, 0 to 2. */\n  intensity?: number;\n  /** Frame (at 30fps) when the subject hits. */\n  impact?: number;\n  colors?: Partial<ImpactShakeColors>;\n  /** Default word and flash color. Overrides `colors.foreground`. */\n  foregroundColor?: string;\n  /** Background color. Overrides `colors.background`. */\n  backgroundColor?: string;\n  seed?: number;\n  speed?: number;\n};\n\nexport const impactShakeDefaults = {\n  text: \"LAUNCH\",\n  intensity: 1,\n  impact: 7,\n  colors: {\n    background: \"#111111\",\n    foreground: \"#FFFFFF\",\n    splitA: \"#FF2A2A\",\n    splitB: \"#2A6BFF\",\n    dust: \"#D9D9D9\",\n  } satisfies ImpactShakeColors,\n  seed: 1,\n  speed: 1,\n};\n\nconst FONT = \"Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif\";\nconst DUST = 36;\n\n// Turns any content into a flat silhouette of one color, keeping its alpha.\nfunction tintMatrix(hex: string) {\n  const v = Number.parseInt(hex.replace(\"#\", \"\").slice(0, 6), 16);\n  const r = ((v >> 16) & 255) / 255;\n  const g = ((v >> 8) & 255) / 255;\n  const b = (v & 255) / 255;\n  return `0 0 0 0 ${r} 0 0 0 0 ${g} 0 0 0 0 ${b} 0 0 0 1 0`;\n}\n\nexport function ImpactShake({\n  children,\n  text = impactShakeDefaults.text,\n  intensity = impactShakeDefaults.intensity,\n  impact = impactShakeDefaults.impact,\n  colors: colorOverrides,\n  foregroundColor,\n  backgroundColor,\n  seed = impactShakeDefaults.seed,\n  speed = impactShakeDefaults.speed,\n}: ImpactShakeProps) {\n  const frame = useCurrentFrame();\n  const { width, height, fps } = useVideoConfig();\n  const uid = `impact-shake-${useId().replace(/[^a-zA-Z0-9_-]/g, \"\")}`;\n  const baseColors = { ...impactShakeDefaults.colors, ...colorOverrides };\n  const colors = {\n    ...baseColors,\n    foreground: foregroundColor ?? baseColors.foreground,\n    background: backgroundColor ?? baseColors.background,\n  };\n  const t = (frame * speed * 30) / fps;\n  const k = Math.max(0, intensity);\n  const unit = height / 720;\n  const since = t - impact;\n\n  // Fall: pure acceleration into the hit, no easing out.\n  const fall = interpolate(t, [0, impact], [-height * 0.85, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n    easing: Easing.in(Easing.quad),\n  });\n  const falling = since < 0;\n  const stretch = falling\n    ? interpolate(t, [0, impact], [1, 1.18], { extrapolateRight: \"clamp\" })\n    : 1;\n  const squash = falling\n    ? 0\n    : 1 -\n      spring({ frame: since, fps: 30, config: { damping: 9, stiffness: 260 } });\n  const sx = falling\n    ? 1 / Math.sqrt(stretch)\n    : 1 + 0.16 * squash * Math.min(1, k);\n  const sy = falling ? stretch : 1 - 0.2 * squash * Math.min(1, k);\n\n  // Damped camera shake with incommensurate frequencies so it never looks looped.\n  const decay = since >= 0 ? Math.exp(-since / 3.2) : 0;\n  const phase = random(`impact-shake-${seed}`) * Math.PI * 2;\n  const shakeX = decay * Math.sin(since * 2.9 + phase) * 26 * unit * k;\n  const shakeY = decay * Math.cos(since * 3.7 + phase) * 34 * unit * k;\n  const shakeR = decay * Math.sin(since * 2.3 + phase * 2) * 1.6 * k;\n\n  const split =\n    since >= 0 && since < 2 ? (1 - since / 2) * 18 * unit * k + 6 * unit : 0;\n  const flash = interpolate(since, [0, 0.5, 2.5], [0, 0.12, 0], {\n    extrapolateLeft: \"clamp\",\n    extrapolateRight: \"clamp\",\n  });\n\n  const floorY = height * 0.62;\n  const dust = useMemo(\n    () =>\n      Array.from({ length: DUST }, (_, i) => {\n        const key = `impact-shake-${seed}-dust-${i}`;\n        const side = i % 2 === 0 ? -1 : 1;\n        return {\n          chip: i % 3 === 0,\n          x0: (0.04 + random(`${key}-x`) * 0.24) * side,\n          vx: side * (0.4 + random(`${key}-v`) * 1),\n          vy: 0.3 + random(`${key}-u`) * 0.9,\n          size: 0.5 + random(`${key}-s`) * 0.9,\n          life: 11 + random(`${key}-l`) * 9,\n        };\n      }),\n    [seed],\n  );\n\n  const subject = children ?? (\n    <span\n      style={{\n        fontFamily: FONT,\n        fontSize: height * 0.24,\n        fontWeight: 900,\n        letterSpacing: \"-0.05em\",\n        lineHeight: 1,\n        color: colors.foreground,\n      }}\n    >\n      {text}\n    </span>\n  );\n\n  const layer = (filter: string | undefined, dx: number, key: string) => (\n    <AbsoluteFill\n      key={key}\n      style={{\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        filter,\n        transform: `translate(${dx}px, ${fall}px) scale(${sx}, ${sy})`,\n        transformOrigin: `50% ${(floorY / height) * 100}%`,\n        mixBlendMode: filter ? \"screen\" : undefined,\n      }}\n    >\n      {subject}\n    </AbsoluteFill>\n  );\n\n  return (\n    <AbsoluteFill\n      style={{ backgroundColor: colors.background, overflow: \"hidden\" }}\n    >\n      <svg width={0} height={0} style={{ position: \"absolute\" }} aria-hidden>\n        <filter id={`${uid}-a`} colorInterpolationFilters=\"sRGB\">\n          <feColorMatrix type=\"matrix\" values={tintMatrix(colors.splitA)} />\n        </filter>\n        <filter id={`${uid}-b`} colorInterpolationFilters=\"sRGB\">\n          <feColorMatrix type=\"matrix\" values={tintMatrix(colors.splitB)} />\n        </filter>\n      </svg>\n      <AbsoluteFill\n        style={{\n          transform: `translate(${shakeX}px, ${shakeY}px) rotate(${shakeR}deg) scale(${1 + decay * 0.04 * k})`,\n        }}\n      >\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          <filter\n            id={`${uid}-puff`}\n            x=\"-50%\"\n            y=\"-50%\"\n            width=\"200%\"\n            height=\"200%\"\n          >\n            <feGaussianBlur stdDeviation={height * 0.007} />\n          </filter>\n          {since >= 0\n            ? dust.map((p, i) => {\n                const age = since;\n                if (age > p.life) return null;\n                const life = age / p.life;\n                const spread = Math.max(0.4, k);\n                if (p.chip) {\n                  // Debris: ballistic arc, small and bright.\n                  const x =\n                    width / 2 + p.x0 * width + p.vx * age * 10 * unit * spread;\n                  const y =\n                    floorY - p.vy * age * 16 * unit + age * age * 1.6 * unit;\n                  const size = p.size * 6 * unit;\n                  return (\n                    <rect\n                      key={i}\n                      x={x - size / 2}\n                      y={y - size / 2}\n                      width={size}\n                      height={size}\n                      fill={colors.dust}\n                      opacity={(1 - life) * 0.9}\n                      transform={`rotate(${age * 40 * p.vx} ${x} ${y})`}\n                    />\n                  );\n                }\n                // Puffs: burst outward against heavy drag, then hang and swell.\n                const travel = 1 - Math.exp(-age / 3.5);\n                const x =\n                  width / 2 +\n                  p.x0 * width +\n                  p.vx * travel * width * 0.16 * spread;\n                const y = floorY - p.vy * travel * height * 0.07;\n                const r =\n                  height * p.size * (0.015 + 0.05 * travel + 0.02 * life);\n                return (\n                  <circle\n                    key={i}\n                    cx={x}\n                    cy={y}\n                    r={r}\n                    fill={colors.dust}\n                    opacity={(1 - life) ** 1.4 * 0.38}\n                    filter={`url(#${uid}-puff)`}\n                  />\n                );\n              })\n            : null}\n        </svg>\n        {split > 0 ? (\n          <>\n            {layer(`url(#${uid}-a)`, -split, \"a\")}\n            {layer(`url(#${uid}-b)`, split, \"b\")}\n          </>\n        ) : null}\n        {layer(undefined, 0, \"main\")}\n      </AbsoluteFill>\n      <AbsoluteFill\n        style={{ backgroundColor: colors.foreground, opacity: flash * k }}\n      />\n    </AbsoluteFill>\n  );\n}\n",
      "type": "registry:component",
      "target": "components/remocn/impact-shake.tsx"
    }
  ],
  "type": "registry:component"
}
