{"$schema":"https://ui.shadcn.com/schema/registry-item.json","author":"Eduardo Calvo <educlopez93@gmail.com>","css":{},"dependencies":["motion"],"description":"A RansomNote component for SmoothUI.","devDependencies":[],"files":[{"content":"\"use client\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  motion,\n  type TargetAndTransition,\n  useReducedMotion,\n} from \"motion/react\";\nimport { useMemo } from \"react\";\n\nexport type RansomNoteAnimate = \"none\" | \"assemble\" | \"jitter\";\n\nexport interface RansomNoteProps {\n  /** How the scraps enter/behave: fly in, idly wobble, or render static. */\n  animate?: RansomNoteAnimate;\n  className?: string;\n  /** Font-family stacks to pick from for each character. */\n  fonts?: string[];\n  /** How wild the per-character variation gets, from 0 (subtle) to 1 (chaotic). */\n  intensity?: number;\n  /** Background colors used for each cut-out paper scrap. */\n  palette?: string[];\n  /** Maximum rotation, in degrees, applied to any scrap. */\n  rotation?: number;\n  /** Deterministic seed for the scrap layout. Same seed + text = same layout. */\n  seed?: number;\n  /** Delay, in seconds, added between each scrap's entrance/wobble. */\n  stagger?: number;\n  text: string;\n}\n\ninterface RansomNoteScrap {\n  backgroundColor?: string;\n  char: string;\n  entranceOffsetX: number;\n  entranceOffsetY: number;\n  entranceRotation: number;\n  fontFamily: string;\n  fontScale: number;\n  isWhitespace: boolean;\n  jitterDuration: number;\n  jitterRotation: number;\n  rotation: number;\n  skew: number;\n}\n\ntype RansomNoteRestState = {\n  opacity: number;\n  rotate: number;\n  x: number;\n  y: number;\n};\n\ninterface RansomNoteTransition {\n  bounce?: number;\n  delay?: number;\n  duration?: number;\n  ease?: readonly [number, number, number, number];\n  repeat?: number;\n  repeatType?: \"mirror\";\n  type?: \"spring\";\n}\n\n// mulberry32 constants\nconst MULBERRY32_INCREMENT = 0x6d_2b_79_f5;\nconst UINT32_DIVISOR = 4_294_967_296;\n\n/**\n * Deterministic, seedable PRNG (mulberry32). Given the same seed it always\n * produces the same sequence of numbers in [0, 1), so layouts stay stable\n * across renders and are safe to compute during SSR.\n */\nexport const createSeededRandom = (seed: number): (() => number) => {\n  let state = seed;\n  return () => {\n    state = (state + MULBERRY32_INCREMENT) | 0;\n    let t = Math.imul(state ^ (state >>> 15), 1 | state);\n    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;\n    return ((t ^ (t >>> 14)) >>> 0) / UINT32_DIVISOR;\n  };\n};\n\nconst DEFAULT_SEED = 1;\nconst DEFAULT_INTENSITY = 0.6;\nconst DEFAULT_ROTATION_MAX = 8;\nconst DEFAULT_STAGGER_SECONDS = 0.03;\nconst MIN_INTENSITY = 0;\nconst MAX_INTENSITY = 1;\n\nconst DEFAULT_FONTS = [\n  \"Georgia, 'Times New Roman', serif\",\n  \"'Courier New', Courier, monospace\",\n  \"Impact, 'Arial Narrow Bold', sans-serif\",\n  \"'Brush Script MT', cursive\",\n  \"Verdana, Geneva, sans-serif\",\n  \"'Palatino Linotype', 'Book Antiqua', Palatino, serif\",\n];\n\nconst DEFAULT_PALETTE = [\n  \"#f7f1e3\",\n  \"#fdf6e3\",\n  \"#f1e4c3\",\n  \"#e9dcc0\",\n  \"#fbeee0\",\n  \"#eee2cf\",\n];\n\nconst SCRAP_TEXT_COLOR = \"#1f1b16\";\n\nconst FONT_SCALE_BASE = 0.85;\nconst FONT_SCALE_VARIANCE = 0.5;\nconst ENTRANCE_OFFSET_BASE = 24;\nconst ENTRANCE_OFFSET_VARIANCE = 60;\nconst ENTRANCE_ROTATION_VARIANCE = 30;\nconst SKEW_MAX = 6;\nconst JITTER_ROTATION_BASE = 1.5;\nconst JITTER_ROTATION_VARIANCE = 2.5;\nconst JITTER_DURATION_MIN_SECONDS = 0.6;\nconst JITTER_DURATION_VARIANCE_SECONDS = 0.6;\n\nconst SPRING_DURATION = 0.25;\nconst SPRING_BOUNCE = 0.1;\nconst STATIC_TRANSITION: { duration: number } = { duration: 0 };\n// ease-in-out cubic-bezier for the moving (idle wobble) loop, per the\n// project's animation guidelines.\nconst JITTER_EASE: [number, number, number, number] = [0.645, 0.045, 0.355, 1];\n\nconst WHITESPACE_PATTERN = /\\s/;\n\nconst randomInRange = (rand: () => number, min: number, max: number) =>\n  min + rand() * (max - min);\n\nconst clamp = (value: number, min: number, max: number) =>\n  Math.min(Math.max(value, min), max);\n\nconst RansomNote = ({\n  animate = \"assemble\",\n  className,\n  fonts = DEFAULT_FONTS,\n  intensity = DEFAULT_INTENSITY,\n  palette = DEFAULT_PALETTE,\n  rotation = DEFAULT_ROTATION_MAX,\n  seed = DEFAULT_SEED,\n  stagger = DEFAULT_STAGGER_SECONDS,\n  text,\n}: RansomNoteProps) => {\n  const shouldReduceMotion = useReducedMotion();\n  const clampedIntensity = clamp(intensity, MIN_INTENSITY, MAX_INTENSITY);\n\n  const scraps = useMemo<RansomNoteScrap[]>(() => {\n    const rand = createSeededRandom(seed);\n    const characters = Array.from(text);\n    const fontStack = fonts.length > 0 ? fonts : DEFAULT_FONTS;\n    const colorStack = palette.length > 0 ? palette : DEFAULT_PALETTE;\n\n    return characters.map((char) => {\n      const isWhitespace = WHITESPACE_PATTERN.test(char);\n      const fontFamily = fontStack[Math.floor(rand() * fontStack.length)];\n      const backgroundColor = isWhitespace\n        ? undefined\n        : colorStack[Math.floor(rand() * colorStack.length)];\n      const fontScale =\n        FONT_SCALE_BASE + rand() * FONT_SCALE_VARIANCE * clampedIntensity;\n      const restRotation =\n        randomInRange(rand, -rotation, rotation) * clampedIntensity;\n      const entranceSpread =\n        ENTRANCE_OFFSET_BASE + ENTRANCE_OFFSET_VARIANCE * clampedIntensity;\n      const entranceOffsetX = randomInRange(\n        rand,\n        -entranceSpread,\n        entranceSpread\n      );\n      const entranceOffsetY = randomInRange(\n        rand,\n        -entranceSpread,\n        entranceSpread\n      );\n      const entranceRotation =\n        randomInRange(\n          rand,\n          -ENTRANCE_ROTATION_VARIANCE,\n          ENTRANCE_ROTATION_VARIANCE\n        ) * clampedIntensity;\n      const skew = randomInRange(rand, -SKEW_MAX, SKEW_MAX) * clampedIntensity;\n      const jitterRotation =\n        JITTER_ROTATION_BASE +\n        rand() * JITTER_ROTATION_VARIANCE * clampedIntensity;\n      const jitterDuration =\n        JITTER_DURATION_MIN_SECONDS + rand() * JITTER_DURATION_VARIANCE_SECONDS;\n\n      return {\n        backgroundColor,\n        char,\n        entranceOffsetX,\n        entranceOffsetY,\n        entranceRotation,\n        fontFamily,\n        fontScale,\n        isWhitespace,\n        jitterDuration,\n        jitterRotation,\n        rotation: restRotation,\n        skew,\n      };\n    });\n    // fonts/palette/rotation are included because they change the derived\n    // per-character values below; the layout otherwise stays stable for a\n    // given text + seed + intensity combination.\n  }, [text, seed, clampedIntensity, fonts, palette, rotation]);\n\n  return (\n    <div className={cn(\"relative inline-block\", className)}>\n      <span className=\"sr-only\">{text}</span>\n      <div\n        aria-hidden=\"true\"\n        className=\"flex flex-wrap items-baseline leading-none\"\n      >\n        {scraps.map((scrap, index) => {\n          const restState: RansomNoteRestState = {\n            opacity: 1,\n            rotate: scrap.rotation,\n            x: 0,\n            y: 0,\n          };\n\n          let initial: TargetAndTransition = restState;\n          let animateState: TargetAndTransition = restState;\n          let transition: RansomNoteTransition = STATIC_TRANSITION;\n\n          if (!shouldReduceMotion && animate === \"assemble\") {\n            initial = {\n              opacity: 0,\n              rotate: scrap.rotation + scrap.entranceRotation,\n              x: scrap.entranceOffsetX,\n              y: scrap.entranceOffsetY,\n            };\n            animateState = restState;\n            transition = {\n              bounce: SPRING_BOUNCE,\n              delay: index * stagger,\n              duration: SPRING_DURATION,\n              type: \"spring\",\n            };\n          } else if (!shouldReduceMotion && animate === \"jitter\") {\n            initial = restState;\n            animateState = {\n              rotate: [\n                scrap.rotation - scrap.jitterRotation,\n                scrap.rotation + scrap.jitterRotation,\n              ],\n            };\n            transition = {\n              delay: index * stagger,\n              duration: scrap.jitterDuration,\n              ease: JITTER_EASE,\n              repeat: Number.POSITIVE_INFINITY,\n              repeatType: \"mirror\",\n            };\n          }\n\n          return (\n            <motion.span\n              animate={animateState}\n              className={cn(\n                \"inline-block select-none whitespace-pre\",\n                !scrap.isWhitespace &&\n                  \"rounded-sm px-1 py-0.5 shadow-sm ring-1 ring-black/10\"\n              )}\n              initial={initial}\n              // biome-ignore lint/suspicious/noArrayIndexKey: characters have no stable id\n              key={index}\n              style={{\n                backgroundColor: scrap.backgroundColor,\n                color: scrap.isWhitespace ? undefined : SCRAP_TEXT_COLOR,\n                fontFamily: scrap.fontFamily,\n                fontSize: `${scrap.fontScale}em`,\n                skewX: scrap.skew,\n                transformOrigin: \"50% 50%\",\n              }}\n              transition={transition}\n            >\n              {scrap.char}\n            </motion.span>\n          );\n        })}\n      </div>\n    </div>\n  );\n};\n\nexport default RansomNote;\n","path":"index.tsx","target":"components/smoothui/ransom-note/index.tsx","type":"registry:ui"}],"name":"ransom-note","registryDependencies":[],"title":"Ransom Note","type":"registry:ui"}