{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"blur-up-image","type":"registry:ui","title":"Blur-up Image","description":"Placeholder resolves into the photo.","dependencies":["motion"],"categories":["content"],"docs":"https://www.interior.dev/docs/blur-up-image","files":[{"path":"registry/interior/blur-up-image.tsx","type":"registry:ui","target":"components/interior/blur-up-image.tsx","content":"\"use client\";\n\nimport { useRef, useState } from \"react\";\nimport {\n  motion,\n  useIsomorphicLayoutEffect,\n  useReducedMotion,\n} from \"motion/react\";\n\nconst DEVELOP = { duration: 0.65, ease: [0.23, 1, 0.32, 1] } as const;\n\nconst INSTANT = { duration: 0 } as const;\n\nexport type BlurUpStatus = \"loading\" | \"ready\" | \"error\";\n\nexport type UseBlurUpImageOptions = {\n  src?: string;\n  srcSet?: string;\n  onReady?: () => void;\n  onError?: () => void;\n};\n\nexport function useBlurUpImage({\n  src,\n  srcSet,\n  onReady,\n  onError,\n}: UseBlurUpImageOptions) {\n  const ref = useRef<HTMLImageElement>(null);\n  const [state, setState] = useState<{\n    status: BlurUpStatus;\n    instant: boolean;\n  }>({ status: \"loading\", instant: false });\n\n  const ready = useRef(onReady);\n  ready.current = onReady;\n  const failed = useRef(onError);\n  failed.current = onError;\n\n  useIsomorphicLayoutEffect(() => {\n    const img = ref.current;\n\n    const set = (status: BlurUpStatus, instant: boolean) =>\n      setState((prev) =>\n        prev.status === status && prev.instant === instant\n          ? prev\n          : { status, instant },\n      );\n\n    if (!img || !src) {\n      set(\"loading\", false);\n      return;\n    }\n\n    let alive = true;\n\n    const cached = img.complete && img.naturalWidth > 0;\n\n    const reveal = () => {\n      if (!alive) return;\n      set(\"ready\", cached);\n      ready.current?.();\n    };\n\n    const fail = () => {\n      if (!alive) return;\n      set(\"error\", cached);\n      failed.current?.();\n    };\n\n    if (img.complete) {\n      if (cached) reveal();\n      else fail();\n      return () => {\n        alive = false;\n      };\n    }\n\n    set(\"loading\", false);\n\n    const onLoad = () => {\n      if (!alive) return;\n      if (typeof img.decode === \"function\") {\n        img.decode().then(reveal, fail);\n        return;\n      }\n      reveal();\n    };\n\n    img.addEventListener(\"load\", onLoad);\n    img.addEventListener(\"error\", fail);\n\n    return () => {\n      alive = false;\n      img.removeEventListener(\"load\", onLoad);\n      img.removeEventListener(\"error\", fail);\n    };\n  }, [src, srcSet]);\n\n  return {\n    ref,\n    status: state.status,\n    instant: state.instant,\n    loaded: state.status === \"ready\",\n  };\n}\n\nexport type BlurUpImageProps = {\n  src?: string;\n  alt: string;\n  width: number;\n  height: number;\n  placeholder?: string;\n  color?: string;\n  blur?: number;\n  radius?: 5 | 6 | 9 | 11 | 14;\n  srcSet?: string;\n  sizes?: string;\n  loading?: \"lazy\" | \"eager\";\n  fetchPriority?: \"high\" | \"low\" | \"auto\";\n  onReady?: () => void;\n  onError?: () => void;\n  className?: string;\n};\n\nexport function BlurUpImage({\n  src,\n  alt,\n  width,\n  height,\n  placeholder,\n  color,\n  blur = 14,\n  radius = 11,\n  srcSet,\n  sizes,\n  loading = \"lazy\",\n  fetchPriority,\n  onReady,\n  onError,\n  className = \"\",\n}: BlurUpImageProps) {\n  const reduced = useReducedMotion();\n  const { ref, status, instant } = useBlurUpImage({\n    src,\n    srcSet,\n    onReady,\n    onError,\n  });\n\n  const shown = status === \"ready\";\n  const still = reduced === true || instant;\n  const transition = still ? INSTANT : DEVELOP;\n\n  return (\n    <div\n      aria-busy={status === \"loading\"}\n      style={{\n        aspectRatio: `${width} / ${height}`,\n        borderRadius: radius,\n        backgroundColor: color,\n      }}\n      className={`relative w-full overflow-hidden bg-stone-200 dark:bg-white/15 ${className}`}\n    >\n      {placeholder ? (\n        // eslint-disable-next-line @next/next/no-img-element\n        <img\n          src={placeholder}\n          alt=\"\"\n          aria-hidden\n          draggable={false}\n          className=\"absolute inset-0 h-full w-full object-cover\"\n          style={{ filter: `blur(${blur}px)`, transform: \"scale(1.08)\" }}\n        />\n      ) : null}\n\n      <motion.img\n        ref={ref}\n        src={src}\n        srcSet={srcSet}\n        sizes={sizes}\n        alt={alt}\n        width={width}\n        height={height}\n        loading={loading}\n        fetchPriority={fetchPriority}\n        decoding=\"async\"\n        draggable={false}\n        className=\"absolute inset-0 h-full w-full object-cover\"\n        initial={false}\n        animate={\n          still\n            ? { opacity: shown ? 1 : 0 }\n            : shown\n              ? {\n                  opacity: 1,\n                  filter: \"blur(0px) saturate(1)\",\n                  scale: 1,\n                }\n              : {\n                  opacity: 0,\n                  filter: \"blur(18px) saturate(0.6)\",\n                  scale: 1.06,\n                }\n        }\n        transition={transition}\n      />\n\n      {status === \"error\" ? (\n        <motion.div\n          aria-hidden\n          initial={{ opacity: 0 }}\n          animate={{ opacity: 1 }}\n          transition={transition}\n          className=\"absolute inset-0 grid place-items-center bg-white text-stone-400 dark:bg-[#1D1D1A] dark:text-stone-500\"\n        >\n          <svg width=\"22\" height=\"22\" viewBox=\"0 0 256 256\" fill=\"currentColor\">\n            <path d=\"M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16h64a8,8,0,0,0,7.59-5.47l14.83-44.48L163,151.43a8.07,8.07,0,0,0,4.46-4.46l14.62-36.55,44.48-14.83A8,8,0,0,0,232,88V56A16,16,0,0,0,216,40ZM112.41,157.47,98.23,200H40V172l52-52,30.42,30.42L117,152.57A8,8,0,0,0,112.41,157.47ZM216,82.23,173.47,96.41a8,8,0,0,0-4.9,4.62l-14.72,36.82L138.58,144l-35.27-35.27a16,16,0,0,0-22.62,0L40,149.37V56H216Zm12.68,33a8,8,0,0,0-7.21-1.1l-23.8,7.94a8,8,0,0,0-4.9,4.61l-14.31,35.77-35.77,14.31a8,8,0,0,0-4.61,4.9l-7.94,23.8A8,8,0,0,0,137.73,216H216a16,16,0,0,0,16-16V121.73A8,8,0,0,0,228.68,115.24ZM216,200H148.83l3.25-9.75,35.51-14.2a8.07,8.07,0,0,0,4.46-4.46l14.2-35.51,9.75-3.25Z\" />\n          </svg>\n        </motion.div>\n      ) : null}\n    </div>\n  );\n}\n"}]}