{"$schema":"https://ui.shadcn.com/schema/registry.json","author":"Kaiyu Hsu <uicapsule@kyh.io>","dependencies":["lucide-react","motion"],"devDependencies":[],"files":[{"content":"{\n  \"name\": \"@uicapsule/screenshot-capture\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"exports\": {\n    \"./preview\": \"./preview.tsx\"\n  },\n  \"scripts\": {\n    \"clean\": \"git clean -xdf .cache .turbo dist node_modules\"\n  },\n  \"dependencies\": {\n    \"lucide-react\": \"^1.16.0\",\n    \"motion\": \"^12.40.0\",\n    \"react\": \"catalog:\",\n    \"react-dom\": \"catalog:\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"catalog:\",\n    \"@types/react-dom\": \"catalog:\"\n  }\n}\n","path":"/package.json","target":"uicapsule/screenshot-capture/package.json","type":"registry:file"},{"content":"import type { ReactNode } from \"react\";\n\n// Screen is 330×716 (iPhone 15 proportions); the clay shell adds a 16px lip on every side.\nexport const SCREEN_WIDTH = 330;\nexport const SCREEN_HEIGHT = 716;\n\nconst CLAY_SHADOW = [\n  \"0 60px 100px -40px rgb(0 0 0 / 0.8)\",\n  \"0 26px 40px -24px rgb(0 0 0 / 0.6)\",\n  \"inset 0 7px 7px rgb(255 255 255 / 0.85)\",\n  \"inset 0 -14px 20px rgb(122 100 78 / 0.5)\",\n  \"inset 12px 0 18px -12px rgb(255 255 255 / 0.55)\",\n  \"inset -12px 0 18px -12px rgb(122 100 78 / 0.35)\",\n].join(\", \");\n\nconst Nub = ({ className }: { className: string }) => (\n  <span\n    aria-hidden=\"true\"\n    className={`absolute w-[9px] rounded-full bg-[#ddd2c3] ${className}`}\n    style={{\n      boxShadow:\n        \"inset 2px 2px 2px rgb(255 255 255 / 0.8), inset -2px -2px 3px rgb(122 100 78 / 0.45), 0 2px 4px rgb(0 0 0 / 0.35)\",\n    }}\n  />\n);\n\ninterface PhoneFrameProps {\n  children: ReactNode;\n  /** Classes for the screen: background, text color, pointer handling. */\n  className?: string;\n  /** Hide the Dynamic Island when the screen draws its own status area. */\n  island?: boolean;\n}\n\nexport const PhoneFrame = ({ children, className, island = true }: PhoneFrameProps) => (\n  <div\n    className=\"relative shrink-0 rounded-[66px] bg-[linear-gradient(165deg,#f3ece2_0%,#e2d7c9_50%,#cdbfae_100%)] p-[16px]\"\n    style={{ boxShadow: CLAY_SHADOW }}\n  >\n    <Nub className=\"top-[132px] -left-[6px] h-[36px]\" />\n    <Nub className=\"top-[188px] -left-[6px] h-[62px]\" />\n    <Nub className=\"top-[262px] -left-[6px] h-[62px]\" />\n    <Nub className=\"top-[206px] -right-[6px] h-[92px]\" />\n    <div\n      className={`relative isolate overflow-hidden rounded-[50px] select-none ${className ?? \"bg-black\"}`}\n      style={{\n        boxShadow:\n          \"0 0 0 2px rgb(122 100 78 / 0.35), 0 -2px 3px rgb(122 100 78 / 0.35), 0 3px 4px rgb(255 255 255 / 0.7)\",\n        height: SCREEN_HEIGHT,\n        width: SCREEN_WIDTH,\n      }}\n    >\n      {children}\n      {island && (\n        <div\n          aria-hidden=\"true\"\n          className=\"pointer-events-none absolute top-[11px] left-1/2 z-50 h-[30px] w-[98px] -translate-x-1/2 rounded-full bg-black\"\n        />\n      )}\n    </div>\n  </div>\n);\n","path":"/phone-frame.tsx","target":"uicapsule/screenshot-capture/phone-frame.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { ScreenshotCapture } from \"./screenshot-capture\";\n\nconst Preview = () => (\n  <main className=\"flex h-dvh items-center justify-center overflow-hidden bg-[radial-gradient(circle_at_50%_20%,#1b1d26_0%,#0d0e14_55%,#050609_100%)] p-5\">\n    <ScreenshotCapture />\n  </main>\n);\n\nexport default Preview;\n","path":"/preview.tsx","target":"uicapsule/screenshot-capture/preview.tsx","type":"registry:file"},{"content":"\"use client\";\n\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport type { FC } from \"react\";\nimport { Camera } from \"lucide-react\";\nimport { AnimatePresence, MotionConfig, motion } from \"motion/react\";\n\nimport { PhoneFrame, SCREEN_HEIGHT, SCREEN_WIDTH } from \"./phone-frame\";\n\nconst LINGER_MS = 4500;\nconst THUMB_SCALE = 0.2;\nconst THUMB_INSET = 17;\nconst THUMB_BOTTOM = 30;\nconst thumbX = THUMB_INSET - (SCREEN_WIDTH * (1 - THUMB_SCALE)) / 2;\nconst thumbY = (SCREEN_HEIGHT * (1 - THUMB_SCALE)) / 2 - THUMB_BOTTOM;\n\n// The \"wallpaper\" is pure CSS so the thumbnail can be an exact copy.\nconst Scene: FC = () => (\n  <div className=\"absolute inset-0 overflow-hidden bg-[#0b1120]\">\n    <div className=\"absolute inset-0 bg-[radial-gradient(circle_at_70%_18%,#fbbf24_0%,#f97316_18%,transparent_40%)]\" />\n    <div className=\"absolute inset-0 bg-[radial-gradient(circle_at_50%_120%,#312e81_0%,transparent_60%)]\" />\n    <div\n      className=\"absolute bottom-0 left-[-30%] h-80 w-[90%] bg-[#1e2749]\"\n      style={{ clipPath: \"polygon(0 100%, 50% 0, 100% 100%)\" }}\n    />\n    <div\n      className=\"absolute bottom-0 right-[-25%] h-64 w-[80%] bg-[#141b36]\"\n      style={{ clipPath: \"polygon(0 100%, 45% 8%, 100% 100%)\" }}\n    />\n    <div className=\"absolute right-0 bottom-0 left-0 h-32 bg-gradient-to-t from-[#0b1120]/80 to-transparent\" />\n    <p className=\"absolute top-[76px] left-6 text-[13px] font-medium text-white/60\">Golden hour</p>\n    <p className=\"absolute top-[96px] left-6 text-[26px] font-semibold tracking-tight text-white\">\n      Dolomites, Italy\n    </p>\n  </div>\n);\n\ninterface Shot {\n  id: number;\n}\n\nexport const ScreenshotCapture = () => {\n  const [flashKey, setFlashKey] = useState(0);\n  const [shot, setShot] = useState<Shot | null>(null);\n  const nextIdRef = useRef(1);\n  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n  useEffect(\n    () => () => {\n      if (timerRef.current) {\n        clearTimeout(timerRef.current);\n      }\n    },\n    [],\n  );\n\n  const capture = useCallback(() => {\n    const id = nextIdRef.current;\n    nextIdRef.current += 1;\n    setFlashKey(id);\n    setShot({ id });\n    if (timerRef.current) {\n      clearTimeout(timerRef.current);\n    }\n    timerRef.current = setTimeout(() => setShot(null), LINGER_MS);\n  }, []);\n\n  return (\n    <MotionConfig reducedMotion=\"user\">\n      <PhoneFrame className=\"bg-[#0b1120]\">\n        <Scene />\n\n        <AnimatePresence>\n          {flashKey > 0 && (\n            <motion.div\n              key={flashKey}\n              aria-hidden\n              initial={{ opacity: 0.95 }}\n              animate={{ opacity: 0 }}\n              exit={{ opacity: 0 }}\n              transition={{ duration: 0.45, ease: \"easeOut\" }}\n              className=\"pointer-events-none absolute inset-0 z-30 bg-white\"\n            />\n          )}\n        </AnimatePresence>\n\n        <AnimatePresence>\n          {shot && (\n            <motion.div\n              key={shot.id}\n              drag=\"x\"\n              dragConstraints={{ left: 0, right: 0 }}\n              dragElastic={{ left: 0.9, right: 0.05 }}\n              onDragEnd={(_, info) => {\n                if (info.offset.x < -40) {\n                  setShot(null);\n                }\n              }}\n              initial={{ borderRadius: 50, scale: 1, x: 0, y: 0 }}\n              animate={{ borderRadius: 40, scale: THUMB_SCALE, x: thumbX, y: thumbY }}\n              exit={{ opacity: 0, transition: { duration: 0.25, ease: \"easeIn\" }, x: -420 }}\n              transition={{ bounce: 0.22, delay: 0.12, duration: 0.65, type: \"spring\" }}\n              className=\"absolute inset-0 z-20 cursor-grab overflow-hidden shadow-2xl shadow-black/80 ring-[10px] ring-white active:cursor-grabbing\"\n            >\n              <Scene />\n            </motion.div>\n          )}\n        </AnimatePresence>\n\n        <div className=\"absolute inset-x-0 bottom-10 z-10 flex justify-center\">\n          <motion.button\n            type=\"button\"\n            onClick={capture}\n            whileTap={{ scale: 0.92 }}\n            className=\"flex items-center gap-2 rounded-full bg-white/12 px-4 py-2.5 text-[13px] font-medium text-white backdrop-blur-xl ring-1 ring-white/20 transition-colors hover:bg-white/20\"\n          >\n            <Camera className=\"size-4\" />\n            Screenshot\n          </motion.button>\n        </div>\n\n        <AnimatePresence>\n          {!shot && (\n            <motion.p\n              key=\"hint\"\n              initial={{ opacity: 0 }}\n              animate={{ opacity: 1 }}\n              exit={{ opacity: 0 }}\n              className=\"pointer-events-none absolute inset-x-0 bottom-[92px] z-10 text-center text-[11px] text-white/35\"\n            >\n              Capture, then swipe the thumbnail away\n            </motion.p>\n          )}\n        </AnimatePresence>\n      </PhoneFrame>\n    </MotionConfig>\n  );\n};\n","path":"/screenshot-capture.tsx","target":"uicapsule/screenshot-capture/screenshot-capture.tsx","type":"registry:file"}],"homepage":"https://uicapsule.com/ui/screenshot-capture","name":"screenshot-capture","registryDependencies":[],"type":"registry:block"}