{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "video-button",
  "title": "Video button",
  "description": "A button with theme-specific resting colours that reveals a looping clip or GIF on hover, under a dark scrim with a light label.",
  "dependencies": [
    "motion"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/items/video-button/video-button.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { useReducedMotion, useReducedMotionConfig } from \"motion/react\";\n\nimport { cn } from \"@/lib/utils\";\n\nexport type VideoButtonSize = \"sm\" | \"md\" | \"lg\";\n\nexport type VideoButtonMediaType = \"video\" | \"image\";\n\ninterface BaseProps {\n  /** The label. Defaults to \"Get in touch\". */\n  label?: React.ReactNode;\n  /** A looping clip (mp4, webm) or an image or GIF, revealed behind the label. */\n  mediaSrc: string;\n  /** Overrides detection from the file extension (mp4, webm, mov and m4v are video). */\n  mediaType?: VideoButtonMediaType;\n  /** Video only: the frame shown before playback, and the still under reduced motion. */\n  poster?: string;\n  size?: VideoButtonSize;\n  className?: string;\n}\n\ntype AnchorProps = BaseProps &\n  Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, \"children\" | keyof BaseProps> & {\n    /** Given, the button renders as an `<a>`; otherwise as a `<button>`. */\n    href: string;\n  };\n\ntype ButtonProps = BaseProps &\n  Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, \"children\" | keyof BaseProps> & {\n    href?: undefined;\n  };\n\nexport type VideoButtonProps = AnchorProps | ButtonProps;\n\nconst SIZES = {\n  sm: \"h-10 rounded-xl px-5 text-sm\",\n  md: \"h-11 rounded-2xl px-6 text-base\",\n  lg: \"h-13 rounded-2xl px-6 text-lg\",\n} as const;\n\nconst SHELL =\n  \"group/media relative isolate inline-flex cursor-pointer touch-manipulation items-center justify-center overflow-hidden bg-media-rest-fill font-heading font-semibold whitespace-nowrap no-underline shadow-lg select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50\";\n\n/**\n * Where the media is always shown: screens without hover and narrow screens,\n * where there is no other way to see it. The class and the playback effect\n * use the same query.\n */\nconst ALWAYS_SHOWN_QUERY = \"(hover: none), (max-width: 767px)\";\n\n/** Shown on hover and keyboard focus, and always where ALWAYS_SHOWN_QUERY matches. */\nconst REVEAL =\n  \"opacity-0 transition-opacity duration-300 group-hover/media:opacity-100 group-focus-visible/media:opacity-100 [@media(hover:none)]:opacity-100 max-md:opacity-100 motion-reduce:transition-none\";\n\nconst VIDEO_EXTENSION = /\\.(mp4|webm|mov|m4v)(?:[?#]|$)/i;\n\nfunction subscribeToHydration() {\n  return () => {};\n}\n\n/**\n * A button that reveals a looping clip or GIF behind its label on hover.\n * Its resting fill and label follow the theme. Over footage, a dark scrim\n * and light label keep the text readable in either mode.\n *\n * Video plays only while it can be seen: on hover or focus where there is a\n * pointer that hovers, and while on screen where there is not. Under reduced\n * motion a video holds its first frame (or `poster`) and a GIF, which cannot\n * be paused, is not shown at all.\n */\nexport function VideoButton({\n  label = \"Get in touch\",\n  mediaSrc,\n  mediaType,\n  poster,\n  size = \"lg\",\n  className,\n  ...props\n}: VideoButtonProps) {\n  const configuredReduce = useReducedMotionConfig();\n  const prefersReducedMotion = useReducedMotion();\n  const hydrated = React.useSyncExternalStore(subscribeToHydration, () => true, () => false);\n  const reduce = hydrated && Boolean(configuredReduce || prefersReducedMotion);\n  const kind: VideoButtonMediaType = mediaType ?? (VIDEO_EXTENSION.test(mediaSrc) ? \"video\" : \"image\");\n  const showMedia = !(reduce && kind === \"image\");\n\n  const rootRef = React.useRef<HTMLElement | null>(null);\n  const videoRef = React.useRef<HTMLVideoElement>(null);\n\n  React.useEffect(() => {\n    const root = rootRef.current;\n    const video = videoRef.current;\n    if (!root || !video || reduce) return;\n    video.muted = true;\n    const play = () => {\n      video.play().catch(() => {});\n    };\n    const pause = () => video.pause();\n\n    if (window.matchMedia(ALWAYS_SHOWN_QUERY).matches) {\n      const observer = new IntersectionObserver(([entry]) => (entry.isIntersecting ? play() : pause()));\n      observer.observe(root);\n      return () => {\n        observer.disconnect();\n        pause();\n      };\n    }\n\n    root.addEventListener(\"pointerenter\", play);\n    root.addEventListener(\"pointerleave\", pause);\n    root.addEventListener(\"focusin\", play);\n    root.addEventListener(\"focusout\", pause);\n    return () => {\n      root.removeEventListener(\"pointerenter\", play);\n      root.removeEventListener(\"pointerleave\", pause);\n      root.removeEventListener(\"focusin\", play);\n      root.removeEventListener(\"focusout\", pause);\n      pause();\n    };\n  }, [reduce, kind, mediaSrc]);\n\n  const classes = cn(SHELL, SIZES[size], className);\n\n  const body = (\n    <>\n      {showMedia ? (\n        <>\n          {kind === \"video\" ? (\n            <video\n              ref={videoRef}\n              src={mediaSrc}\n              poster={poster}\n              muted\n              loop\n              playsInline\n              preload=\"auto\"\n              aria-hidden=\"true\"\n              tabIndex={-1}\n              className={cn(\"pointer-events-none absolute inset-0 -z-10 size-full object-cover\", REVEAL)}\n            />\n          ) : (\n            // eslint-disable-next-line @next/next/no-img-element -- ships to any React app, not only Next\n            <img\n              src={mediaSrc}\n              alt=\"\"\n              aria-hidden=\"true\"\n              className={cn(\"pointer-events-none absolute inset-0 -z-10 size-full object-cover\", REVEAL)}\n            />\n          )}\n          <span aria-hidden=\"true\" className={cn(\"pointer-events-none absolute inset-0 -z-10 bg-media-fill/50\", REVEAL)} />\n        </>\n      ) : null}\n      <span className={cn(\n        \"relative text-media-rest-ink transition-colors duration-300 motion-reduce:transition-none\",\n        showMedia && \"group-hover/media:text-on-media group-focus-visible/media:text-on-media [@media(hover:none)]:text-on-media max-md:text-on-media group-hover/media:[text-shadow:0_1.2px_1.2px_color-mix(in_srgb,var(--media-fill)_80%,transparent)] group-focus-visible/media:[text-shadow:0_1.2px_1.2px_color-mix(in_srgb,var(--media-fill)_80%,transparent)] [@media(hover:none)]:[text-shadow:0_1.2px_1.2px_color-mix(in_srgb,var(--media-fill)_80%,transparent)] max-md:[text-shadow:0_1.2px_1.2px_color-mix(in_srgb,var(--media-fill)_80%,transparent)]\",\n      )}>\n        {label}\n      </span>\n    </>\n  );\n\n  if (props.href !== undefined) {\n    return (\n      <a\n        ref={rootRef as React.Ref<HTMLAnchorElement>}\n        className={classes}\n        {...(props as Omit<AnchorProps, keyof BaseProps>)}\n      >\n        {body}\n      </a>\n    );\n  }\n\n  const { type = \"button\", ...rest } = props as Omit<ButtonProps, keyof BaseProps>;\n  return (\n    <button ref={rootRef as React.Ref<HTMLButtonElement>} type={type} className={classes} {...rest}>\n      {body}\n    </button>\n  );\n}\n\nexport default VideoButton;\n",
      "type": "registry:ui"
    }
  ],
  "meta": {
    "kind": "primitive",
    "whenToUse": "Use for one playful, low-stakes action with personality, such as Get in touch at the end of an FAQ. Do not use for primary flows or repeated actions; the media is a flourish and costs bandwidth.",
    "origin": {
      "project": "Steeze",
      "path": "src/components/ui/media-button.jsx (used by src/components/ui/faq-section.jsx)"
    },
    "credits": [
      {
        "name": "GIPHY clip used on steeze.ng (creator not recorded; GIPHY's terms cover embedding through GIPHY, not rehosting the file); gallery preview only, not shipped",
        "license": "unknown"
      }
    ]
  },
  "type": "registry:ui"
}
