{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "image-lightbox",
  "title": "Image Lightbox",
  "description": "Image lightbox primitive.",
  "registryDependencies": [
    "@circle-ui/overlay-action",
    "@circle-ui/utils"
  ],
  "files": [
    {
      "path": "registry/berlin/blocks/media.tsx",
      "content": "// Generated from packages/ui/src/components/media.tsx\n\"use client\";\n\nimport * as React from \"react\";\n\nimport { OverlayAction } from \"@/registry/berlin/circle-ui/overlay-action\";\nimport { cn } from \"@/registry/berlin/lib/utils\";\n\ntype VideoContextValue = {\n  isMuted: boolean;\n  isPlaying: boolean;\n  toggleMute: () => void;\n  togglePlay: () => void;\n};\n\nconst VideoContext = React.createContext<VideoContextValue | null>(null);\n\nfunction useVideoControls() {\n  const context = React.useContext(VideoContext);\n  if (!context) {\n    throw new Error(\"Video controls must be used within Video.\");\n  }\n  return context;\n}\n\nfunction parseTextContent(text: unknown): React.ReactNode {\n  if (!text) return null;\n  if (typeof text === \"string\") return text;\n  if (React.isValidElement(text)) return text;\n  if (Array.isArray(text)) {\n    return text\n      .map((item) => {\n        if (typeof item === \"string\") return item;\n        if (\n          item &&\n          typeof item === \"object\" &&\n          \"children\" in item &&\n          Array.isArray((item as { children?: unknown[] }).children)\n        ) {\n          return (item as { children: Array<{ text?: string }> }).children\n            .map((child) => child.text ?? \"\")\n            .join(\"\");\n        }\n        return \"\";\n      })\n      .join(\" \");\n  }\n  return String(text);\n}\n\nfunction positionClasses(\n  position: \"top left\" | \"bottom right\" | \"center\" = \"bottom right\",\n) {\n  if (position === \"top left\") return \"left-4 top-4\";\n  if (position === \"center\")\n    return \"left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2\";\n  return \"bottom-4 right-4\";\n}\n\nfunction PlayIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"11\" stroke=\"currentColor\" strokeWidth=\"2\" />\n      <path d=\"m10 8 6 4-6 4V8Z\" fill=\"currentColor\" />\n    </svg>\n  );\n}\n\nfunction PauseIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <circle cx=\"12\" cy=\"12\" r=\"11\" stroke=\"currentColor\" strokeWidth=\"2\" />\n      <path d=\"M9 8h2v8H9zm4 0h2v8h-2z\" fill=\"currentColor\" />\n    </svg>\n  );\n}\n\nfunction VolumeIcon({\n  className,\n  muted,\n}: {\n  className?: string;\n  muted: boolean;\n}) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 24 24\"\n      fill=\"none\"\n      aria-hidden=\"true\"\n    >\n      <path\n        d=\"M5 9h4l5-4v14l-5-4H5V9Z\"\n        stroke=\"currentColor\"\n        strokeWidth=\"2\"\n        strokeLinejoin=\"round\"\n      />\n      {muted ? (\n        <path\n          d=\"m17 9 4 6m0-6-4 6\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n        />\n      ) : (\n        <path\n          d=\"M18 9c1.333 1 2 2 2 3s-.667 2-2 3\"\n          stroke=\"currentColor\"\n          strokeWidth=\"2\"\n          strokeLinecap=\"round\"\n        />\n      )}\n    </svg>\n  );\n}\n\nfunction StarIcon({ className }: { className?: string }) {\n  return (\n    <svg\n      className={className}\n      viewBox=\"0 0 16 16\"\n      fill=\"currentColor\"\n      aria-hidden=\"true\"\n    >\n      <path d=\"m8 1.5 1.9 3.86 4.26.62-3.08 3 0.72 4.25L8 11.2l-3.8 2 0.72-4.25-3.08-3 4.26-.62L8 1.5Z\" />\n    </svg>\n  );\n}\n\nexport interface VideoProps extends React.VideoHTMLAttributes<HTMLVideoElement> {\n  src: string;\n  config?: {\n    autoPlay?: boolean;\n    muted?: boolean;\n    loop?: boolean;\n  };\n}\n\ntype PositionedControlProps = React.PropsWithChildren<{\n  className?: string;\n  position?: \"top left\" | \"bottom right\" | \"center\";\n}>;\n\nfunction PlayControl({\n  className,\n  position = \"bottom right\",\n}: PositionedControlProps) {\n  const { isPlaying, togglePlay } = useVideoControls();\n  return (\n    <OverlayAction\n      aria-label={isPlaying ? \"Pause video\" : \"Play video\"}\n      onClick={togglePlay}\n      tone=\"light\"\n      className={cn(\n        positionClasses(position),\n        className,\n      )}\n    >\n      {isPlaying ? (\n        <PauseIcon className=\"size-8\" />\n      ) : (\n        <PlayIcon className=\"size-8\" />\n      )}\n    </OverlayAction>\n  );\n}\n\nfunction MuteControl({\n  className,\n  position = \"top left\",\n}: PositionedControlProps) {\n  const { isMuted, toggleMute } = useVideoControls();\n  return (\n    <OverlayAction\n      aria-label={isMuted ? \"Unmute video\" : \"Mute video\"}\n      onClick={toggleMute}\n      tone=\"light\"\n      className={cn(\n        positionClasses(position),\n        className,\n      )}\n    >\n      <VolumeIcon muted={isMuted} className=\"size-8\" />\n    </OverlayAction>\n  );\n}\n\nfunction PlayWithText({\n  className,\n  position = \"center\",\n  children,\n}: PositionedControlProps) {\n  const { isPlaying, togglePlay } = useVideoControls();\n  return (\n    <OverlayAction\n      aria-label={isPlaying ? \"Pause video\" : \"Play video\"}\n      onClick={togglePlay}\n      kind=\"pill\"\n      tone=\"light\"\n      className={cn(\n        positionClasses(position),\n        className,\n      )}\n    >\n      {isPlaying ? (\n        <PauseIcon className=\"size-5\" />\n      ) : (\n        <PlayIcon className=\"size-5\" />\n      )}\n      <span>{children}</span>\n    </OverlayAction>\n  );\n}\n\ntype VideoComponent = React.FC<VideoProps> & {\n  Mute: typeof MuteControl;\n  Play: typeof PlayControl;\n  PlayWithText: typeof PlayWithText;\n};\n\nconst VideoBase: React.FC<VideoProps> = ({\n  src,\n  children,\n  className,\n  config,\n  poster,\n  ...props\n}) => {\n  const videoRef = React.useRef<HTMLVideoElement>(null);\n  const [isPlaying, setIsPlaying] = React.useState(Boolean(config?.autoPlay));\n  const [isMuted, setIsMuted] = React.useState(Boolean(config?.muted));\n\n  const togglePlay = React.useCallback(() => {\n    const node = videoRef.current;\n    if (!node) return;\n    if (node.paused) {\n      void node.play();\n    } else {\n      node.pause();\n    }\n  }, []);\n\n  const toggleMute = React.useCallback(() => {\n    const node = videoRef.current;\n    if (!node) return;\n    node.muted = !node.muted;\n    setIsMuted(node.muted);\n  }, []);\n\n  React.useEffect(() => {\n    const node = videoRef.current;\n    if (!node) return;\n    node.muted = Boolean(config?.muted);\n    setIsMuted(Boolean(config?.muted));\n  }, [config?.muted]);\n\n  return (\n    <VideoContext.Provider\n      value={{ isMuted, isPlaying, toggleMute, togglePlay }}\n    >\n      <div\n        className={cn(\n          \"relative w-full overflow-hidden rounded-[var(--radius-media)] bg-circle-black [aspect-ratio:9/16]\",\n          className,\n        )}\n      >\n        <video\n          ref={videoRef}\n          className=\"absolute inset-0 block h-full w-full object-cover\"\n          poster={poster}\n          playsInline\n          preload=\"metadata\"\n          onPlay={() => setIsPlaying(true)}\n          onPause={() => setIsPlaying(false)}\n          autoPlay={config?.autoPlay}\n          muted={config?.muted}\n          loop={config?.loop}\n          {...props}\n        >\n          <source src={src} />\n        </video>\n        {children ?? (\n          <div className=\"pointer-events-none absolute inset-0 z-[2]\">\n            <div className=\"pointer-events-auto\">\n              <PlayControl />\n              <MuteControl />\n            </div>\n          </div>\n        )}\n        <div\n          className=\"absolute inset-0 z-[1] cursor-pointer\"\n          onClick={togglePlay}\n        />\n      </div>\n    </VideoContext.Provider>\n  );\n};\n\nexport const Video = VideoBase as VideoComponent;\nVideo.Play = PlayControl;\nVideo.Mute = MuteControl;\nVideo.PlayWithText = PlayWithText;\n\nexport interface MediaSlideProps {\n  type: \"image\" | \"video\";\n  src: string;\n  text?: unknown;\n  poster?: string;\n  altText?: string;\n  desktopWidth?: number;\n  label?: string;\n  className?: string;\n}\n\nexport function MediaSlide({\n  type,\n  src,\n  text,\n  poster,\n  altText,\n  label,\n  className,\n}: MediaSlideProps) {\n  const textContent = parseTextContent(text);\n\n  return (\n    <div\n      className={cn(\n        \"relative w-full overflow-hidden rounded-[var(--radius-media)] bg-card [aspect-ratio:9/16]\",\n        className,\n      )}\n    >\n      {type === \"video\" ? (\n        <Video\n          src={src}\n          poster={poster}\n          className=\"h-full rounded-[var(--radius-media)]\"\n        />\n      ) : (\n        <img\n          src={src}\n          alt={altText || \"Media\"}\n          className=\"absolute inset-0 h-full w-full object-cover\"\n        />\n      )}\n      {label || textContent ? (\n        <div className=\"pointer-events-none absolute inset-x-0 bottom-0 flex flex-col gap-3 bg-[var(--media-scrim)] p-6 text-inverse-background\">\n          {label ? (\n            <div className=\"text-[10px] font-semibold uppercase leading-[15px] tracking-[0.8px] text-inverse-background\">\n              {label}\n            </div>\n          ) : null}\n          {textContent ? (\n            <div className=\"text-[16px] font-medium leading-[24px] tracking-[-0.16px]\">\n              {textContent}\n            </div>\n          ) : null}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport interface ImageLightboxProps {\n  src: string;\n  children: React.ReactNode;\n  alt?: string;\n}\n\nexport interface LightboxProps {\n  image: string;\n  alt: string;\n  onClose: () => void;\n}\n\nexport function Lightbox({ image, alt, onClose }: LightboxProps) {\n  React.useEffect(() => {\n    function onKeyDown(event: KeyboardEvent) {\n      if (event.key === \"Escape\") onClose();\n    }\n    document.body.style.overflow = \"hidden\";\n    window.addEventListener(\"keydown\", onKeyDown);\n    return () => {\n      document.body.style.overflow = \"\";\n      window.removeEventListener(\"keydown\", onKeyDown);\n    };\n  }, [onClose]);\n\n  return (\n    <div\n      className=\"fixed inset-0 z-[1000] flex items-center justify-center bg-[var(--lightbox-backdrop)]\"\n      onClick={onClose}\n    >\n      <OverlayAction\n        aria-label=\"Close lightbox\"\n        tone=\"light\"\n        className=\"fixed right-6 top-6 z-[1001]\"\n        onClick={onClose}\n      >\n        <span className=\"text-xl leading-none\">×</span>\n      </OverlayAction>\n      <div\n        className=\"relative flex max-h-[96vh] w-full max-w-[1280px] items-center justify-center\"\n        onClick={(event) => event.stopPropagation()}\n      >\n        <div className=\"relative flex w-full items-center justify-center [aspect-ratio:16/9]\">\n          <img\n            src={image}\n            alt={alt}\n            className=\"max-h-full max-w-full object-contain\"\n          />\n        </div>\n      </div>\n    </div>\n  );\n}\n\nexport function ImageWithLightbox({\n  src,\n  alt = \"\",\n  width,\n  height,\n}: {\n  src: string;\n  alt?: string;\n  width: number;\n  height: number;\n}) {\n  return (\n    <ImageLightbox src={src} alt={alt}>\n      <img\n        src={src}\n        alt={alt}\n        width={width}\n        height={height}\n        className=\"h-auto max-w-full cursor-pointer object-cover\"\n      />\n    </ImageLightbox>\n  );\n}\n\nexport function ImageLightbox({ src, children, alt = \"\" }: ImageLightboxProps) {\n  const [isOpen, setIsOpen] = React.useState(false);\n  const child = React.Children.only(children) as React.ReactElement<{\n    onClick?: React.MouseEventHandler;\n  }>;\n\n  return (\n    <>\n      <span className=\"inline-block cursor-pointer\">\n        {React.cloneElement(child, {\n          onClick: (event: React.MouseEvent) => {\n            child.props.onClick?.(event);\n            setIsOpen(true);\n          },\n        })}\n      </span>\n      {isOpen ? (\n        <Lightbox image={src} alt={alt} onClose={() => setIsOpen(false)} />\n      ) : null}\n    </>\n  );\n}\n\nexport interface TextOnlyTestimonialProps {\n  reviewer: string;\n  text: string;\n  stars: number;\n  source: string;\n  link: string;\n}\n\nexport function TextOnlyTestimonial({\n  reviewer,\n  text,\n  stars,\n  source,\n  link,\n}: TextOnlyTestimonialProps) {\n  return (\n    <div className=\"flex h-[228px] w-[345px] flex-col items-start gap-3 rounded-[var(--border-radius)] bg-card p-4\">\n      <p className=\"text-sm font-medium leading-6\">{reviewer}</p>\n      <p className=\"line-clamp-4 text-sm leading-6 text-muted-foreground\">\n        {text}\n      </p>\n      <div className=\"mt-auto flex w-full items-center justify-between gap-2 border-t pt-4\">\n        <div className=\"flex items-center gap-0.5 text-warning\">\n          {Array.from({ length: stars }).map((_, index) => (\n            <StarIcon key={index} className=\"size-4\" />\n          ))}\n        </div>\n        <a href={link} className=\"text-sm text-foreground/40\">\n          {source}\n        </a>\n      </div>\n    </div>\n  );\n}\n\nexport function TestimonialsCarousel({\n  children,\n}: {\n  children: React.ReactNode;\n}) {\n  return (\n    <div className=\"w-full overflow-x-auto pb-3\">\n      <div className=\"flex min-w-full snap-x gap-5 px-6\">\n        {React.Children.map(children, (child, index) => (\n          <div key={index} className=\"snap-start\">\n            {child}\n          </div>\n        ))}\n      </div>\n    </div>\n  );\n}\n\nexport interface HeroMediaItem {\n  image?: { url: string; alternativeText?: string };\n  video?: { url: string };\n  visualText?: { text: string; name: string };\n}\n\nexport interface HeroGalleryProps {\n  slides: HeroMediaItem[];\n}\n\nexport function HeroGallery({ slides }: HeroGalleryProps) {\n  const [currentIndex, setCurrentIndex] = React.useState(0);\n  const safeSlides = slides.length\n    ? slides\n    : [{ image: { url: \"\", alternativeText: \"\" } }];\n\n  function next() {\n    setCurrentIndex((index) => (index + 1) % safeSlides.length);\n  }\n\n  function previous() {\n    setCurrentIndex(\n      (index) => (index - 1 + safeSlides.length) % safeSlides.length,\n    );\n  }\n\n  function mediaProps(media: HeroMediaItem) {\n    if (media.video) {\n      return {\n        type: \"video\" as const,\n        src: media.video.url,\n        poster: media.image?.url || \"\",\n        altText: media.image?.alternativeText || \"\",\n      };\n    }\n    return {\n      type: \"image\" as const,\n      src: media.image?.url || \"\",\n      altText: media.image?.alternativeText || \"\",\n      text: media.visualText\n        ? `${media.visualText.text} - ${media.visualText.name}`\n        : undefined,\n    };\n  }\n\n  return (\n    <div className=\"space-y-4\">\n      <div className=\"grid gap-4 md:grid-cols-[2fr_1fr_1fr]\">\n        <div className=\"md:col-span-1\">\n          <MediaSlide {...mediaProps(safeSlides[currentIndex])} />\n        </div>\n        <div className=\"hidden md:block\">\n          <MediaSlide\n            {...mediaProps(safeSlides[(currentIndex + 1) % safeSlides.length])}\n          />\n        </div>\n        <div className=\"hidden md:block\">\n          <MediaSlide\n            {...mediaProps(safeSlides[(currentIndex + 2) % safeSlides.length])}\n          />\n        </div>\n      </div>\n      <div className=\"flex items-center justify-between\">\n        <button\n          type=\"button\"\n          onClick={previous}\n          className=\"rounded-full border px-4 py-2 text-sm\"\n        >\n          Previous\n        </button>\n        <div className=\"text-sm text-muted-foreground\">\n          {currentIndex + 1} / {safeSlides.length}\n        </div>\n        <button\n          type=\"button\"\n          onClick={next}\n          className=\"rounded-full border px-4 py-2 text-sm\"\n        >\n          Next\n        </button>\n      </div>\n    </div>\n  );\n}\n",
      "type": "registry:ui",
      "target": "src/components/ui/media.tsx"
    }
  ],
  "type": "registry:ui"
}