{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "ink-stamp",
  "type": "registry:component",
  "title": "Ink Stamp",
  "description": "A rubber stamp that rises, winds up and slams onto the page with a quick squash and a jolt through the card, lifting away to leave a rough, slightly bled ink impression that comes out a little different every time.",
  "categories": [
    "motion",
    "Celebration"
  ],
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [
    "utils"
  ],
  "files": [
    {
      "path": "registry/devl/ink-stamp.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { AnimatePresence, motion, useAnimate, useReducedMotion } from \"motion/react\";\nimport { createContext, type ReactNode, useCallback, useContext, useEffect, useId, useState } from \"react\";\nimport { cn } from \"@/lib/utils\";\n\nexport type InkStampTone = \"emerald\" | \"rose\" | \"sky\" | \"violet\" | \"amber\";\n\nconst tones: Record<InkStampTone, string> = {\n  emerald: \"text-emerald-600 dark:text-emerald-400\",\n  rose: \"text-rose-600 dark:text-rose-400\",\n  sky: \"text-sky-600 dark:text-sky-400\",\n  violet: \"text-violet-600 dark:text-violet-400\",\n  amber: \"text-amber-600 dark:text-amber-400\",\n};\n\nconst ShakeContext = createContext<(() => void) | null>(null);\n\nfunction useSafeReducedMotion() {\n  const prefers = useReducedMotion();\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted && !!prefers;\n}\n\nfunction hash(value: string) {\n  let h = 2166136261;\n  for (let i = 0; i < value.length; i++) h = Math.imul(h ^ value.charCodeAt(i), 16777619);\n  return h >>> 0;\n}\n\nfunction random(seed: number) {\n  let s = seed || 1;\n  return () => {\n    s = Math.imul(s ^ (s >>> 15), 2246822507) ^ Math.imul(s ^ (s >>> 13), 3266489909);\n    return ((s ^= s >>> 16) >>> 0) / 4294967296;\n  };\n}\n\nexport interface StampSurfaceProps {\n  children: ReactNode;\n  className?: string;\n}\n\nexport function StampSurface({ children, className }: StampSurfaceProps) {\n  const [scope, animate] = useAnimate<HTMLDivElement>();\n  const reduce = useSafeReducedMotion();\n  const shake = useCallback(() => {\n    if (reduce || !scope.current) return;\n    animate(\n      scope.current,\n      { x: [0, -2.5, 2, -1.2, 0.5, 0], y: [0, 2, -1, 0.5, 0, 0] },\n      { duration: 0.3, ease: \"easeOut\" },\n    );\n  }, [animate, reduce, scope]);\n  return (\n    <ShakeContext.Provider value={shake}>\n      <div className={cn(\"relative\", className)} ref={scope}>\n        {children}\n      </div>\n    </ShakeContext.Provider>\n  );\n}\n\nexport interface InkStampProps {\n  label: string;\n  caption?: string;\n  tone?: InkStampTone;\n  rotate?: number;\n  stampKey?: string | number | null;\n  onImpact?: () => void;\n  className?: string;\n}\n\nexport function InkStamp({ label, caption, tone = \"emerald\", rotate = -8, stampKey, onImpact, className }: InkStampProps) {\n  const shake = useContext(ShakeContext);\n  const reduce = useSafeReducedMotion();\n  const filterBase = `ink-stamp-${useId().replace(/:/g, \"\")}`;\n  const impact = useCallback(() => {\n    shake?.();\n    onImpact?.();\n  }, [shake, onImpact]);\n\n  return (\n    <div className={cn(\"pointer-events-none grid select-none\", tones[tone], className)} role=\"status\">\n      <span className=\"sr-only\">{stampKey == null ? \"\" : `Stamped ${label}${caption ? `, ${caption}` : \"\"}`}</span>\n      <AnimatePresence initial={false}>\n        {stampKey != null && (\n          <Impression\n            caption={caption}\n            filterId={`${filterBase}-${String(stampKey).replace(/[^a-zA-Z0-9-]/g, \"\")}`}\n            key={stampKey}\n            label={label}\n            onImpact={impact}\n            reduce={reduce}\n            rotate={rotate}\n            seed={hash(`${stampKey}${label}`)}\n          />\n        )}\n      </AnimatePresence>\n    </div>\n  );\n}\n\ninterface ImpressionProps {\n  label: string;\n  caption?: string;\n  rotate: number;\n  seed: number;\n  filterId: string;\n  reduce: boolean;\n  onImpact: () => void;\n}\n\nconst SLAM = 0.36;\n\nfunction Impression({ label, caption, rotate, seed, filterId, reduce, onImpact }: ImpressionProps) {\n  const [rand] = useState(() => random(seed));\n  const [shape] = useState(() => ({\n    tilt: rotate + (rand() - 0.5) * 4,\n    dx: (rand() - 0.5) * 6,\n    dy: (rand() - 0.5) * 4,\n    specks: Array.from({ length: 5 }, () => ({\n      left: `${rand() * 100}%`,\n      top: `${rand() < 0.5 ? -10 - rand() * 14 : 110 + rand() * 14}%`,\n      size: 1.5 + rand() * 2.5,\n    })),\n    warp: Math.floor(rand() * 900),\n    grain: Math.floor(rand() * 900),\n  }));\n\n  useEffect(() => {\n    if (reduce) return;\n    const timer = setTimeout(onImpact, SLAM * 1000);\n    return () => clearTimeout(timer);\n  }, [onImpact, reduce]);\n\n  const ink = reduce\n    ? { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2 } }\n    : { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.04, delay: SLAM } };\n\n  const face = (\n    <div className=\"relative rounded-lg border-[3px] border-current px-4 pt-2 pb-1.5 text-center\">\n      <span className=\"absolute inset-[3px] rounded-[calc(var(--radius)*0.4)] border border-current\" />\n      <span className=\"block font-extrabold text-[26px] uppercase leading-none tracking-[0.16em]\">{label}</span>\n      {caption ? (\n        <span className=\"mt-1 block font-mono font-semibold text-[9px] uppercase tracking-[0.3em]\">{caption}</span>\n      ) : null}\n    </div>\n  );\n\n  return (\n    <motion.div\n      className=\"relative col-start-1 row-start-1\"\n      exit={{ opacity: 0, transition: { duration: 0.18 } }}\n      style={{ rotate: shape.tilt, x: shape.dx, y: shape.dy }}\n    >\n      <svg aria-hidden className=\"absolute size-0\">\n        <filter height=\"140%\" id={filterId} width=\"140%\" x=\"-20%\" y=\"-20%\">\n          <feTurbulence baseFrequency=\"0.045\" numOctaves={2} result=\"warp\" seed={shape.warp} type=\"fractalNoise\" />\n          <feDisplacementMap in=\"SourceGraphic\" in2=\"warp\" result=\"rough\" scale={3.5} xChannelSelector=\"R\" yChannelSelector=\"G\" />\n          <feTurbulence baseFrequency=\"0.85\" numOctaves={1} result=\"grain\" seed={shape.grain} type=\"fractalNoise\" />\n          <feColorMatrix in=\"grain\" result=\"holes\" type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  -9 0 0 0 6.5\" />\n          <feTurbulence baseFrequency=\"0.06\" numOctaves={1} result=\"blots\" seed={shape.warp + 7} type=\"fractalNoise\" />\n          <feColorMatrix in=\"blots\" result=\"density\" type=\"matrix\" values=\"0 0 0 0 0  0 0 0 0 0  0 0 0 0 0  1.9 0 0 0 0.05\" />\n          <feComposite in=\"rough\" in2=\"holes\" operator=\"in\" result=\"inked\" />\n          <feComposite in=\"inked\" in2=\"density\" operator=\"in\" />\n        </filter>\n      </svg>\n\n      <motion.div\n        aria-hidden\n        animate={{ opacity: 0.35, filter: \"blur(1.4px)\", scale: 1.012 }}\n        className=\"absolute inset-0 mix-blend-multiply dark:mix-blend-screen\"\n        initial={{ opacity: 0, filter: \"blur(0px)\", scale: 1 }}\n        transition={reduce ? { duration: 0 } : { duration: 1.1, delay: SLAM + 0.05, ease: [0.23, 1, 0.32, 1] }}\n      >\n        {face}\n      </motion.div>\n\n      <motion.div\n        {...ink}\n        aria-hidden\n        className=\"relative opacity-90 mix-blend-multiply dark:mix-blend-screen\"\n        style={{ filter: `url(#${filterId})` }}\n      >\n        {face}\n        {shape.specks.map((s) => (\n          <span\n            className=\"absolute rounded-full bg-current\"\n            key={`${s.left}${s.top}`}\n            style={{ left: s.left, top: s.top, width: s.size, height: s.size }}\n          />\n        ))}\n      </motion.div>\n\n      {!reduce && <StampBlock />}\n    </motion.div>\n  );\n}\n\nfunction StampBlock() {\n  const times = [0, 0.2, 0.37, 0.52, 0.58, 0.63, 1];\n  return (\n    <motion.div\n      animate={{\n        opacity: [0, 1, 1, 1, 1, 1, 0],\n        scale: [1.3, 1.18, 1.23, 1, 1, 1, 1.14],\n        scaleX: [1, 1, 1, 1, 1.035, 1, 1],\n        scaleY: [1, 1, 1, 1, 0.95, 1, 1],\n        rotate: [-4, -2, -1.5, 0, 0, 0, 1],\n      }}\n      aria-hidden\n      className=\"absolute -inset-1.5\"\n      initial={{ opacity: 0, scale: 1.3 }}\n      transition={{ duration: 0.7, times, ease: [\"easeOut\", \"easeInOut\", [0.6, 0, 0.9, 0.4], \"easeOut\", \"easeOut\", [0.23, 1, 0.32, 1]] }}\n    >\n      <motion.span\n        animate={{\n          x: [10, 7, 8, 0, 0, 0, 7],\n          y: [16, 11, 12, 1, 1, 1, 11],\n          opacity: [0, 0.22, 0.24, 0.5, 0.5, 0.4, 0],\n          filter: [\"blur(14px)\", \"blur(10px)\", \"blur(11px)\", \"blur(2px)\", \"blur(2px)\", \"blur(3px)\", \"blur(12px)\"],\n        }}\n        className=\"absolute inset-0 rounded-xl bg-black\"\n        transition={{ duration: 0.7, times }}\n      />\n      <span className=\"absolute inset-0 rounded-xl bg-[color-mix(in_oklab,currentColor_58%,var(--color-neutral-900))] shadow-[inset_0_1px_0_--theme(--color-white/22%),inset_0_-2px_0_--theme(--color-black/30%)]\" />\n      <span className=\"absolute inset-x-1.5 -bottom-[3px] h-1 rounded-b-lg bg-current\" />\n      <span className=\"absolute top-1/2 left-1/2 aspect-square h-[56%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[color-mix(in_oklab,currentColor_40%,var(--color-neutral-900))] shadow-[inset_0_1px_0_--theme(--color-white/18%),0_2px_4px_--theme(--color-black/30%)]\" />\n      <span className=\"absolute top-1/2 left-1/2 aspect-square h-[30%] -translate-x-1/2 -translate-y-1/2 rounded-full bg-[color-mix(in_oklab,currentColor_52%,var(--color-neutral-900))] shadow-[inset_0_1px_0_--theme(--color-white/14%)]\" />\n    </motion.div>\n  );\n}\n"
    }
  ]
}