{"$schema":"https://ui.shadcn.com/schema/registry-item.json","name":"apple-gallery","type":"registry:block","title":"Apple gallery","description":"An elegant, interactive media gallery inspired by Apple's iPhone 17 landing page. Designed with smooth transitions, interactive playback controls, and a highly polished user experience.","dependencies":["motion"],"registryDependencies":["button"],"files":[{"path":"registry/100xui/blocks/apple-gallery/components/apple-gallery.tsx","content":"\"use client\";\n\nimport React from \"react\";\nimport {\n  type HTMLMotionProps,\n  motion,\n  useScroll,\n  useTransform,\n  useMotionValue,\n  animate,\n} from \"motion/react\";\nimport { PlayIcon, PauseIcon } from \"lucide-react\";\n\nimport { Button } from \"@/components/ui/button\";\nimport { cn } from \"@/lib/utils\";\n\nconst AppleGalleryContext = React.createContext<null | {\n  slideCount: number;\n  isPlaying: boolean;\n  setActiveSlideIndex: React.Dispatch<React.SetStateAction<number>>;\n  containerRef: React.RefObject<HTMLDivElement | null>;\n  activeSlideIndex: number;\n  targetSlideIndexRef: React.RefObject<null | number>;\n  progressAnimationRef: React.RefObject<ReturnType<typeof animate> | null>;\n  setIsPlaying: React.Dispatch<React.SetStateAction<boolean>>;\n  scrollToSlide: (childIndex: number) => void;\n  durationInSec: number;\n}>(null);\n\nexport function useAppleGallery() {\n  const ctx = React.useContext(AppleGalleryContext);\n  if (ctx === null) {\n    throw new Error(\"useAppleGallery must be used within an <AppleGallery />\");\n  }\n  return ctx;\n}\n\nexport function AppleGallery({\n  children,\n  durationInSec = 4,\n  className,\n  ...props\n}: {\n  children: React.ReactNode;\n  durationInSec?: number;\n} & React.ComponentProps<\"div\">) {\n  const [slideCount] = React.useState(\n    (\n      (React.Children.toArray(children)[0] as React.ReactElement).props as {\n        children: React.ReactElement[];\n      }\n    ).children.length\n  );\n\n  const [isPlaying, setIsPlaying] = React.useState(false);\n  const [activeSlideIndex, setActiveSlideIndex] = React.useState(0);\n\n  const containerRef = React.useRef<HTMLDivElement | null>(null);\n  const progressAnimationRef = React.useRef<ReturnType<typeof animate> | null>(\n    null\n  );\n  const targetSlideIndexRef = React.useRef<null | number>(activeSlideIndex);\n\n  const scrollToSlide = React.useCallback(\n    (childIndex: number) => {\n      if (containerRef.current) {\n        setIsPlaying(false);\n        setActiveSlideIndex(childIndex);\n        targetSlideIndexRef.current = childIndex;\n        progressAnimationRef.current = null;\n        containerRef.current.children[childIndex].scrollIntoView({\n          behavior: \"smooth\",\n          block: \"nearest\",\n        });\n      }\n    },\n    [targetSlideIndexRef, setIsPlaying]\n  );\n\n  return (\n    <AppleGalleryContext.Provider\n      value={{\n        slideCount,\n        isPlaying,\n        activeSlideIndex,\n        progressAnimationRef,\n        setActiveSlideIndex,\n        containerRef,\n        scrollToSlide,\n        setIsPlaying,\n        durationInSec,\n        targetSlideIndexRef,\n      }}\n    >\n      <div\n        className={cn(\n          \"flex flex-col items-center justify-center gap-y-3\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </AppleGalleryContext.Provider>\n  );\n}\n\nexport function AppleGalleryContainer({\n  children,\n  gapInPx = 40,\n  paddingInlineInPx = 100,\n  className,\n  style,\n  ...props\n}: {\n  children: React.ReactElement[];\n  gapInPx?: number;\n  paddingInlineInPx?: number;\n} & React.ComponentPropsWithoutRef<\"div\">) {\n  const {\n    containerRef,\n    setIsPlaying,\n    targetSlideIndexRef,\n    setActiveSlideIndex,\n    progressAnimationRef,\n    scrollToSlide,\n  } = useAppleGallery();\n\n  const id = React.useId();\n\n  React.useEffect(() => {\n    if (containerRef.current === null) return;\n\n    const containerObserver = new IntersectionObserver(\n      ([entry]) => {\n        setIsPlaying(entry.isIntersecting);\n        if (progressAnimationRef.current) {\n          if (entry.isIntersecting) {\n            progressAnimationRef.current.play();\n          } else {\n            progressAnimationRef.current.pause();\n          }\n        }\n      },\n      { threshold: 0.5 }\n    );\n\n    const childrenObserver = new IntersectionObserver(\n      (entries) => {\n        entries.forEach((entry) => {\n          const childIndex = Array.from(containerRef.current!.children).indexOf(\n            entry.target\n          );\n\n          if (entry.isIntersecting) {\n            if (targetSlideIndexRef.current === null) {\n              setIsPlaying(false);\n              setActiveSlideIndex(childIndex);\n              progressAnimationRef.current = null;\n            } else if (targetSlideIndexRef.current === childIndex) {\n              targetSlideIndexRef.current = null;\n            }\n          }\n        });\n      },\n      {\n        root: containerRef.current,\n        rootMargin: `0px ${-paddingInlineInPx + gapInPx / 2}px 0px ${-paddingInlineInPx + gapInPx / 2}px`,\n        threshold: 0.5,\n      }\n    );\n\n    containerObserver.observe(containerRef.current);\n    Array.from(containerRef.current.children).forEach((child) => {\n      const childIndex = Array.from(containerRef.current!.children).indexOf(\n        child\n      );\n      childrenObserver.observe(child);\n      child.addEventListener(\"click\", () => {\n        scrollToSlide(childIndex);\n      });\n    });\n\n    return () => {\n      containerObserver.disconnect();\n      childrenObserver.disconnect();\n      if (containerRef.current === null) return;\n      Array.from(containerRef.current.children).forEach((child) => {\n        const childIndex = Array.from(containerRef.current!.children).indexOf(\n          child\n        );\n        child.removeEventListener(\"click\", () => {\n          scrollToSlide(childIndex);\n        });\n      });\n    };\n  }, [\n    containerRef,\n    setIsPlaying,\n    setActiveSlideIndex,\n    targetSlideIndexRef,\n    scrollToSlide,\n    gapInPx,\n    paddingInlineInPx,\n  ]);\n\n  return (\n    <>\n      <style>\n        {`\n        .no-scrollbar-${id}::-webkit-scrollbar {\n            display:none;\n        }\n        .no-scrollbar-${id}{\n        -ms-overflow-style:none;\n        scrollbar-width:none;\n        }\n        `}\n      </style>\n      <div\n        ref={containerRef}\n        style={{ ...style, paddingInline: paddingInlineInPx, gap: gapInPx }}\n        className={cn(\n          \"flex w-full snap-x snap-mandatory overflow-x-scroll py-1 *:w-full *:shrink-0 *:snap-center\",\n          `no-scrollbar-${id}`,\n          className,\n          \"gap-0 px-0\"\n        )}\n        {...props}\n      >\n        {children}\n      </div>\n    </>\n  );\n}\n\nexport function AppleGalleryControls({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { slideCount, progressAnimationRef, isPlaying, setIsPlaying } =\n    useAppleGallery();\n\n  return (\n    <div\n      className={cn(\"sticky bottom-2 z-20 my-2 flex h-10 gap-2\", className)}\n      {...props}\n    >\n      <div className=\"bg-secondary flex items-center justify-center gap-3 rounded-full p-4\">\n        {[...Array(slideCount)].map((_, index) => (\n          <SlideIndicator index={index} key={index} />\n        ))}\n      </div>\n      <div>\n        <Button\n          className=\"grid size-10 shrink-0 place-items-center rounded-full\"\n          size=\"icon-lg\"\n          variant=\"secondary\"\n          onClick={() => {\n            if (isPlaying) {\n              setIsPlaying(false);\n              if (progressAnimationRef.current) {\n                progressAnimationRef.current.pause();\n              }\n            } else {\n              setIsPlaying(true);\n              if (progressAnimationRef.current) {\n                progressAnimationRef.current.play();\n              }\n            }\n          }}\n        >\n          {isPlaying ? (\n            <PauseIcon className=\"text-secondary-foreground fill-secondary-foreground\" />\n          ) : (\n            <PlayIcon className=\"text-secondary-foreground fill-secondary-foreground\" />\n          )}\n        </Button>\n      </div>\n    </div>\n  );\n}\n\nconst MotionButton = motion.create(Button);\n\nfunction SlideIndicator({\n  index,\n  ...rest\n}: {\n  index: number;\n} & HTMLMotionProps<\"button\">) {\n  const { slideCount, activeSlideIndex, scrollToSlide, containerRef } =\n    useAppleGallery();\n\n  const { scrollXProgress } = useScroll({\n    container: containerRef,\n    offset: [\"start start\", \"end end\"],\n  });\n\n  const width = useTransform(\n    scrollXProgress,\n    [\n      (index - 1) / (slideCount - 1),\n      index / (slideCount - 1),\n      (index + 1) / (slideCount - 1),\n    ],\n    [\"8px\", \"48px\", \"8px\"]\n  );\n\n  return (\n    <MotionButton\n      style={{\n        borderRadius: \"999px\",\n        width,\n        height: \"8px\",\n      }}\n      onClick={() => scrollToSlide(index)}\n      className=\"bg-muted-foreground shrink-0 overflow-hidden border-none p-0 transition-none\"\n      {...rest}\n    >\n      {activeSlideIndex == index && <SlideProgressFiller index={index} />}\n    </MotionButton>\n  );\n}\n\nfunction SlideProgressFiller({ index }: { index: number }) {\n  const {\n    slideCount,\n    isPlaying,\n    activeSlideIndex,\n    setActiveSlideIndex,\n    progressAnimationRef,\n    containerRef,\n    targetSlideIndexRef,\n    durationInSec,\n  } = useAppleGallery();\n\n  const scaleX = useMotionValue(0);\n\n  React.useEffect(() => {\n    if (\n      isPlaying &&\n      containerRef.current &&\n      (progressAnimationRef.current === null ||\n        progressAnimationRef.current.state === \"finished\")\n    ) {\n      progressAnimationRef.current = animate(scaleX, 1, {\n        duration: durationInSec,\n        ease: \"linear\",\n        delay: 0.6,\n      });\n\n      progressAnimationRef.current.then(() => {\n        const newActiveSlideIndex = (index + 1) % slideCount;\n        setActiveSlideIndex((prev) => (prev + 1) % slideCount);\n        containerRef.current!.children[newActiveSlideIndex].scrollIntoView({\n          behavior: \"smooth\",\n          block: \"nearest\",\n        });\n        targetSlideIndexRef.current = newActiveSlideIndex;\n      });\n    }\n  }, [\n    isPlaying,\n    activeSlideIndex,\n    progressAnimationRef,\n    setActiveSlideIndex,\n    containerRef,\n    index,\n    scaleX,\n    slideCount,\n    durationInSec,\n    targetSlideIndexRef,\n  ]);\n\n  return (\n    <motion.div\n      style={{ scaleX }}\n      className=\"bg-secondary-foreground size-full origin-left rounded-full\"\n    />\n  );\n}\n","type":"registry:component"}]}