{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "theme-reveal",
  "type": "registry:ui",
  "title": "Theme reveal",
  "description": "Reveals a new theme outward from the exact toggle point.",
  "dependencies": [],
  "categories": [
    "actions"
  ],
  "docs": "https://motion-lexicon.pages.dev/en/components/theme-reveal/",
  "meta": {
    "engines": [
      "css"
    ],
    "runtimeCost": "light",
    "signature": "Circular View Transition theme reveal",
    "sceneFamily": "product-mono",
    "motionRole": "lively",
    "primaryState": "Stable interface after a theme transition",
    "assetProvenance": "self-contained"
  },
  "files": [
    {
      "path": "src/registry/components/theme-reveal.tsx",
      "type": "registry:ui",
      "target": "components/motion-lexicon/theme-reveal.tsx",
      "content": "\"use client\";\n\nimport { useEffect, useRef, useState, type ButtonHTMLAttributes } from \"react\";\nimport { flushSync } from \"react-dom\";\n\ntype ViewTransitionLike = { ready: Promise<void>; finished: Promise<void> };\ntype TransitionDocument = Document & { startViewTransition?: (update: () => void | Promise<void>) => ViewTransitionLike };\n\nexport type ThemeRevealProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, \"onChange\" | \"onClick\" | \"onError\"> & {\n  theme: \"light\" | \"dark\";\n  onThemeChange: (theme: \"light\" | \"dark\") => void | Promise<void>;\n  onError?: (error: unknown) => void;\n  lightLabel?: string;\n  darkLabel?: string;\n  duration?: number;\n};\n\nexport function ThemeReveal({\n  theme,\n  onThemeChange,\n  onError,\n  lightLabel = \"Use light theme\",\n  darkLabel = \"Use dark theme\",\n  duration = 460,\n  className = \"\",\n  disabled,\n  ...props\n}: ThemeRevealProps) {\n  const button = useRef<HTMLButtonElement>(null);\n  const pendingRef = useRef(false);\n  const [reduced, setReduced] = useState(false);\n  const [pending, setPending] = useState(false);\n\n  useEffect(() => {\n    const query = window.matchMedia(\"(prefers-reduced-motion: reduce)\");\n    const update = () => setReduced(query.matches);\n    update();\n    query.addEventListener(\"change\", update);\n    return () => query.removeEventListener(\"change\", update);\n  }, []);\n\n  const toggle = async () => {\n    if (disabled || pendingRef.current) return;\n    pendingRef.current = true;\n    setPending(true);\n    const next = theme === \"dark\" ? \"light\" : \"dark\";\n    try {\n      const start = (document as TransitionDocument).startViewTransition;\n      if (!start || reduced || !button.current) {\n        await onThemeChange(next);\n        return;\n      }\n\n      const box = button.current.getBoundingClientRect();\n      const x = box.left + box.width / 2;\n      const y = box.top + box.height / 2;\n      const radius = Math.hypot(Math.max(x, innerWidth - x), Math.max(y, innerHeight - y));\n      const transition = start.call(document, () => {\n        let result: void | Promise<void> = undefined;\n        flushSync(() => { result = onThemeChange(next); });\n        return result;\n      });\n      await transition.ready;\n      document.documentElement.animate(\n        { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },\n        { duration, easing: \"cubic-bezier(.23,1,.32,1)\", pseudoElement: \"::view-transition-new(root)\" },\n      );\n      await transition.finished;\n    } catch (error: unknown) {\n      onError?.(error);\n    } finally {\n      pendingRef.current = false;\n      setPending(false);\n    }\n  };\n\n  const dark = theme === \"dark\";\n  const unavailable = Boolean(disabled || pending);\n  return (\n    <button\n      ref={button}\n      {...props}\n      type=\"button\"\n      aria-label={dark ? lightLabel : darkLabel}\n      aria-pressed={dark}\n      aria-busy={pending || undefined}\n      aria-disabled={unavailable || undefined}\n      disabled={disabled}\n      onClick={toggle}\n      className={`relative grid size-11 place-items-center overflow-hidden rounded-full border border-neutral-200 bg-white text-neutral-700 outline-none transition-[border-color,box-shadow,background-color,color] duration-150 focus-visible:border-[#4568FF] focus-visible:shadow-[0_0_0_3px_rgba(69,104,255,.22)] disabled:opacity-45 dark:border-white/15 dark:bg-[#202020] dark:text-neutral-200 ${className}`}\n    >\n      <span aria-hidden className={`absolute size-4 rounded-full bg-current transition-[opacity,transform] ${reduced ? \"duration-0\" : \"duration-200\"} ${dark ? \"scale-[.72] opacity-0\" : \"scale-100 opacity-100\"}`} />\n      <svg aria-hidden viewBox=\"0 0 24 24\" width=\"18\" height=\"18\" fill=\"none\" className={`absolute transition-[opacity,transform] ${reduced ? \"duration-0\" : \"duration-200\"} ${dark ? \"scale-100 opacity-100\" : \"scale-[.72] opacity-0\"}`}>\n        <path d=\"M20 15.2A8.1 8.1 0 0 1 8.8 4a8.2 8.2 0 1 0 11.2 11.2Z\" fill=\"currentColor\" />\n      </svg>\n    </button>\n  );\n}\n"
    }
  ]
}
