{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "intro-disclosure",
  "type": "registry:ui",
  "description": "Intro disclosure component with expandable content and smooth animations",
  "dependencies": [
    "motion",
    "react-use-measure",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "aspect-ratio",
    "checkbox",
    "dialog",
    "drawer",
    "progress"
  ],
  "files": [
    {
      "path": "registry/default/ui/intro-disclosure.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport Image from \"next/image\"\nimport { CheckIcon, ExternalLinkIcon } from \"lucide-react\"\nimport {\n  AnimatePresence,\n  motion,\n  useAnimation,\n  type PanInfo,\n} from \"motion/react\"\n\nimport { cn } from \"@/lib/utils\"\nimport { AspectRatio } from \"@/components/ui/aspect-ratio\"\nimport { Button } from \"@/components/ui/button\"\nimport { Checkbox } from \"@/components/ui/checkbox\"\nimport {\n  Dialog,\n  DialogContent,\n  DialogHeader,\n  DialogTitle,\n} from \"@/components/ui/dialog\"\nimport {\n  Drawer,\n  DrawerContent,\n  DrawerHeader,\n  DrawerTitle,\n} from \"@/components/ui/drawer\"\nimport { Progress } from \"@/components/ui/progress\"\n\nfunction useMediaQuery(query: string) {\n  const [matches, setMatches] = React.useState<boolean | null>(null)\n\n  React.useEffect(() => {\n    const media = window.matchMedia(query)\n    setMatches(media.matches)\n\n    const listener = (e: MediaQueryListEvent) => setMatches(e.matches)\n    media.addEventListener(\"change\", listener)\n    return () => media.removeEventListener(\"change\", listener)\n  }, [query])\n\n  return matches ?? false\n}\n\nfunction useFeatureVisibility(\n  featureId: string,\n  variant: \"desktop\" | \"mobile\"\n) {\n  const storageKey = `feature_${featureId}_${variant}`\n  const [isVisible, setIsVisible] = React.useState<boolean | null>(null)\n\n  React.useEffect(() => {\n    const storedValue = localStorage.getItem(storageKey)\n    setIsVisible(storedValue ? JSON.parse(storedValue) : true)\n  }, [storageKey])\n\n  const hideFeature = () => {\n    localStorage.setItem(storageKey, JSON.stringify(false))\n    setIsVisible(false)\n  }\n\n  return { isVisible: isVisible === null ? false : isVisible, hideFeature }\n}\n\nfunction useSwipe(onSwipe: (direction: \"left\" | \"right\") => void) {\n  const handleDragEnd = (\n    event: MouseEvent | TouchEvent | PointerEvent,\n    info: PanInfo\n  ) => {\n    if (info.offset.x > 100) {\n      onSwipe(\"right\")\n    } else if (info.offset.x < -100) {\n      onSwipe(\"left\")\n    }\n  }\n\n  return { handleDragEnd }\n}\n\nconst fadeInScale = {\n  initial: { opacity: 0, scale: 0.95 },\n  animate: { opacity: 1, scale: 1 },\n  exit: { opacity: 0, scale: 0.95 },\n  transition: { duration: 0.2, ease: [0.23, 1, 0.32, 1] as const },\n}\n\nconst slideInOut = (direction: 1 | -1) => ({\n  initial: { opacity: 0, x: 20 * direction },\n  animate: { opacity: 1, x: 0 },\n  exit: { opacity: 0, x: -20 * direction },\n  transition: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] as const },\n})\n\nconst hoverScale = {\n  whileHover: { scale: 1.01 },\n  whileTap: { scale: 0.95 },\n  transition: { duration: 0.2 },\n}\n\nfunction StepPreview({ step, direction }: { step: Step; direction: 1 | -1 }) {\n  const controls = useAnimation()\n\n  React.useEffect(() => {\n    controls.start({\n      opacity: 1,\n      y: 0,\n      transition: { delay: 0.2, duration: 0.3 },\n    })\n  }, [controls, step])\n\n  return (\n    <motion.div\n      {...slideInOut(direction)}\n      className=\"rounded-rb-lg relative h-full w-full overflow-hidden rounded-sm rounded-tl-xl ring-2 ring-black/10 ring-offset-8 dark:ring-black/10 dark:ring-offset-black\"\n    >\n      {step.media ? (\n        <div className=\"relative h-full w-full bg-black\">\n          <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={controls}\n            className=\"h-full max-h-[700px] w-full\"\n          >\n            {step.media.type === \"image\" ? (\n              <Image\n                src={step.media.src || \"/placeholder.svg\"}\n                alt={step.media.alt || \"\"}\n                fill\n                className=\"object-cover\"\n              />\n            ) : (\n              <video\n                src={step.media.src}\n                controls\n                className=\"h-full w-full object-cover\"\n              />\n            )}\n          </motion.div>\n          <div className=\"absolute inset-0 bg-gradient-to-t from-black/80 to-transparent\" />\n          <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={controls}\n            className=\"absolute right-0 bottom-0 left-0 p-6\"\n          >\n            <h3 className=\"mb-2 text-2xl font-semibold text-white\">\n              {step.title}\n            </h3>\n            <p className=\"hidden text-white md:block\">\n              {step.full_description}\n            </p>\n          </motion.div>\n        </div>\n      ) : (\n        <div className=\"flex h-full items-center justify-center p-6\">\n          <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={controls}\n            className=\"text-center\"\n          >\n            <h3 className=\"text-primary mb-2 text-2xl font-semibold\">\n              {step.title}\n            </h3>\n            <p className=\"text-muted-foreground\">{step.full_description}</p>\n          </motion.div>\n        </div>\n      )}\n    </motion.div>\n  )\n}\n\ninterface StepTabProps {\n  step: Step\n  isActive: boolean\n  onClick: () => void\n  isCompleted: boolean\n}\n\nfunction StepTab({ step, isActive, onClick, isCompleted }: StepTabProps) {\n  return (\n    <motion.button\n      {...hoverScale}\n      onClick={onClick}\n      className={cn(\n        \"flex w-full flex-col items-start rounded-lg px-4 py-2 text-left transition-colors\",\n        isActive ? \"bg-muted border-border border\" : \"hover:bg-muted/70\",\n        \"relative\"\n      )}\n      aria-current={isActive ? \"step\" : undefined}\n      aria-label={`${step.title}${isCompleted ? \" (completed)\" : \"\"}`}\n    >\n      <div className=\"mb-1 text-sm font-medium\">{step.title}</div>\n      <div className=\"text-muted-foreground line-clamp-2 hidden text-xs md:block\">\n        {step.short_description}\n      </div>\n      {isCompleted && (\n        <motion.div {...fadeInScale} className=\"absolute top-2 right-2\">\n          <div className=\"bg-primary rounded-full p-1\">\n            <CheckIcon className=\"text-primary-foreground h-2 w-2\" />\n          </div>\n        </motion.div>\n      )}\n    </motion.button>\n  )\n}\n\ninterface Step {\n  title: string\n  short_description: string\n  full_description: string\n  action?: {\n    label: string\n    onClick?: () => void\n    href?: string\n  }\n  media?: {\n    type: \"image\" | \"video\"\n    src: string\n    alt?: string\n  }\n}\n\ninterface FeatureDisclosureProps {\n  steps: Step[]\n  featureId: string\n  onComplete?: () => void\n  onSkip?: () => void\n  showProgressBar?: boolean\n  open: boolean\n  setOpen: (open: boolean) => void\n  forceVariant?: \"mobile\" | \"desktop\"\n}\n\ninterface StepContentProps {\n  steps: Step[]\n  currentStep: number\n  onSkip: () => void\n  onNext: () => void\n  onPrevious: () => void\n  hideFeature: () => void\n  completedSteps: number[]\n  onStepSelect: (index: number) => void\n  direction: 1 | -1\n  isDesktop: boolean\n}\n\nfunction StepContent({\n  steps,\n  currentStep,\n  onSkip,\n  onNext,\n  onPrevious,\n  hideFeature,\n  completedSteps,\n  onStepSelect,\n  direction,\n  isDesktop,\n  stepRef,\n}: StepContentProps & { stepRef: React.RefObject<HTMLButtonElement | null> }) {\n  const [skipNextTime, setSkipNextTime] = React.useState(false)\n\n  const renderActionButton = (action: Step[\"action\"]) => {\n    if (!action) return null\n\n    if (action.href) {\n      return (\n        <Button asChild className=\"w-full\" size=\"sm\" variant=\"link\">\n          <a href={action.href} target=\"_blank\" rel=\"noopener noreferrer\">\n            <span className=\"flex items-center gap-2\">\n              {action.label}\n              <ExternalLinkIcon className=\"h-4 w-4\" />\n            </span>\n          </a>\n        </Button>\n      )\n    }\n\n    return (\n      <Button\n        className=\"w-full rounded-full\"\n        size=\"sm\"\n        variant=\"secondary\"\n        onClick={action.onClick}\n      >\n        {action.label}\n      </Button>\n    )\n  }\n\n  return (\n    <div className=\"mx-auto flex h-full max-w-3xl flex-col\">\n      {isDesktop && (\n        <div className=\"flex-1 px-2 py-3\">\n          <motion.div\n            initial={{ opacity: 0, y: 20 }}\n            animate={{ opacity: 1, y: 0 }}\n            transition={{ duration: 0.5, ease: [0.25, 0.1, 0.25, 1] }}\n            className=\"flex flex-col items-center justify-center space-y-2 px-1\"\n          >\n            {steps.map((step, index) => (\n              <StepTab\n                key={index}\n                step={step}\n                isActive={currentStep === index}\n                onClick={() => onStepSelect(index)}\n                isCompleted={completedSteps.includes(index)}\n              />\n            ))}\n          </motion.div>\n        </div>\n      )}\n      <AnimatePresence mode=\"wait\" initial={false}>\n        <motion.div\n          key={currentStep}\n          {...slideInOut(direction)}\n          className=\"mt-6 space-y-4\"\n        >\n          {!isDesktop && steps[currentStep]?.media && (\n            <AspectRatio\n              ratio={16 / 9}\n              className=\"bg-muted rounded-lg lg:overflow-hidden\"\n            >\n              {steps[currentStep]?.media?.type === \"image\" ? (\n                <Image\n                  src={steps[currentStep]?.media?.src || \"/placeholder.svg\"}\n                  alt={steps[currentStep]?.media?.alt || \"\"}\n                  fill\n                  className=\"object-cover\"\n                />\n              ) : (\n                <video\n                  src={steps[currentStep]?.media?.src}\n                  controls\n                  className=\"h-full w-full object-cover\"\n                />\n              )}\n            </AspectRatio>\n          )}\n\n          {steps[currentStep]?.action ? (\n            <div className=\"px-2\">\n              {renderActionButton(steps[currentStep]?.action)}\n            </div>\n          ) : (\n            <div className=\"h-10\" />\n          )}\n\n          {/* Navigation buttons */}\n          <div className=\"flex items-center justify-between pr-4\">\n            <Button\n              variant=\"ghost\"\n              onClick={onSkip}\n              className=\"text-muted-foreground hover:bg-card rounded-full\"\n            >\n              Skip all\n            </Button>\n            <div className=\"space-x-2\">\n              {currentStep > 0 && (\n                <Button\n                  onClick={onPrevious}\n                  size=\"sm\"\n                  variant=\"ghost\"\n                  className=\"rounded-full hover:bg-transparent\"\n                >\n                  Previous\n                </Button>\n              )}\n              <Button\n                onClick={() => {\n                  if (skipNextTime) {\n                    hideFeature()\n                  }\n                  onNext()\n                }}\n                size=\"sm\"\n                ref={stepRef}\n                className=\"rounded-full\"\n              >\n                {currentStep === steps.length - 1 ? \"Done\" : \"Next\"}\n              </Button>\n            </div>\n            {/* Don't show again checkbox */}\n          </div>\n          <div className=\"flex items-center space-x-2 px-4 pb-4\">\n            <Checkbox\n              id=\"skipNextTime\"\n              checked={skipNextTime}\n              onCheckedChange={(checked) => setSkipNextTime(checked as boolean)}\n            />\n            <label\n              htmlFor=\"skipNextTime\"\n              className=\"text-muted-foreground text-sm\"\n            >\n              Don't show this again\n            </label>\n          </div>\n        </motion.div>\n      </AnimatePresence>\n    </div>\n  )\n}\n\nexport function IntroDisclosure({\n  steps,\n  open,\n  setOpen,\n  featureId,\n  onComplete,\n  onSkip,\n  showProgressBar = true,\n  forceVariant,\n}: FeatureDisclosureProps) {\n  const [currentStep, setCurrentStep] = React.useState(0)\n  const [completedSteps, setCompletedSteps] = React.useState<number[]>([0])\n  const [direction, setDirection] = React.useState<1 | -1>(1)\n  const isDesktopQuery = useMediaQuery(\"(min-width: 768px)\")\n  const isDesktop = forceVariant ? forceVariant === \"desktop\" : isDesktopQuery\n  const variant = isDesktop ? \"desktop\" : \"mobile\"\n  const { isVisible, hideFeature } = useFeatureVisibility(featureId, variant)\n  const stepRef = React.useRef<HTMLButtonElement>(null)\n\n  // Close the dialog if feature is hidden\n  React.useEffect(() => {\n    if (!isVisible) {\n      setOpen(false)\n    }\n  }, [isVisible, setOpen])\n\n  // Focus management\n  React.useEffect(() => {\n    if (open && stepRef.current) {\n      stepRef.current.focus()\n    }\n  }, [open, currentStep])\n\n  // Early return if feature should be hidden\n  if (!isVisible || !open) {\n    return null\n  }\n\n  const handleNext = () => {\n    setDirection(1)\n    setCompletedSteps((prev) =>\n      prev.includes(currentStep) ? prev : [...prev, currentStep]\n    )\n    if (currentStep < steps.length - 1) {\n      setCurrentStep(currentStep + 1)\n    } else {\n      setOpen(false)\n      onComplete?.()\n    }\n  }\n\n  const handlePrevious = () => {\n    setDirection(-1)\n    if (currentStep > 0) {\n      setCurrentStep(currentStep - 1)\n    }\n  }\n\n  const handleSkip = () => {\n    setOpen(false)\n    onSkip?.()\n  }\n\n  const handleStepSelect = (index: number) => {\n    setDirection(index > currentStep ? 1 : -1)\n    // Mark all steps up to and including the selected step as completed\n    setCompletedSteps((prev) => {\n      const newCompletedSteps = new Set(prev)\n      // If moving forward, mark all steps up to the selected one as completed\n      if (index > currentStep) {\n        for (let i = currentStep; i <= index; i++) {\n          newCompletedSteps.add(i)\n        }\n      }\n      return Array.from(newCompletedSteps)\n    })\n    setCurrentStep(index)\n  }\n\n  const handleSwipe = (swipeDirection: \"left\" | \"right\") => {\n    if (swipeDirection === \"left\") {\n      handleNext()\n    } else {\n      handlePrevious()\n    }\n  }\n\n  const handleKeyDown = (event: React.KeyboardEvent) => {\n    if (event.key === \"ArrowRight\" || event.key === \"ArrowDown\") {\n      handleNext()\n    } else if (event.key === \"ArrowLeft\" || event.key === \"ArrowUp\") {\n      handlePrevious()\n    }\n  }\n\n  const { handleDragEnd } = useSwipe(handleSwipe)\n\n  if (isDesktop) {\n    return (\n      <Dialog open={open} onOpenChange={setOpen}>\n        <DialogContent\n          className=\"max-w-[calc(100dvw-2rem)] gap-0 overflow-hidden p-0 sm:max-w-5xl\"\n          onKeyDown={handleKeyDown}\n        >\n          <DialogHeader className=\"bg-muted border-border space-y-2 border-b p-6\">\n            <DialogTitle>Feature Tour</DialogTitle>\n            {showProgressBar && (\n              <div className=\"mt-2 flex w-full justify-center\">\n                <Progress\n                  value={((currentStep + 1) / steps.length) * 100}\n                  className=\"h-1\"\n                />\n              </div>\n            )}\n          </DialogHeader>\n\n          <div className=\"grid h-full grid-cols-2\">\n            <div className=\"p-2 pr-[18px]\">\n              <StepContent\n                steps={steps}\n                currentStep={currentStep}\n                onSkip={handleSkip}\n                onNext={handleNext}\n                onPrevious={handlePrevious}\n                hideFeature={hideFeature}\n                completedSteps={completedSteps}\n                onStepSelect={handleStepSelect}\n                direction={direction}\n                isDesktop={isDesktop}\n                stepRef={stepRef}\n              />\n            </div>\n            <AnimatePresence mode=\"wait\" initial={false}>\n              <StepPreview\n                key={currentStep}\n                step={steps[currentStep]}\n                direction={direction}\n              />\n            </AnimatePresence>\n          </div>\n        </DialogContent>\n      </Dialog>\n    )\n  }\n\n  return (\n    <Drawer open={open} onOpenChange={setOpen}>\n      <DrawerContent className=\"h-[95vh] max-h-[95vh]\">\n        <motion.div\n          drag=\"x\"\n          dragConstraints={{ left: 0, right: 0 }}\n          onDragEnd={handleDragEnd}\n          onKeyDown={handleKeyDown}\n          className=\"mx-auto flex h-full max-w-3xl flex-col\"\n        >\n          <DrawerHeader className=\"space-y-4 pb-4 text-left\">\n            {showProgressBar && (\n              <Progress\n                value={((currentStep + 1) / steps.length) * 100}\n                className=\"mb-4\"\n              />\n            )}\n            <DrawerTitle>{steps[currentStep]?.title}</DrawerTitle>\n          </DrawerHeader>\n\n          <div className=\"flex-1 overflow-y-auto\">\n            <div className=\"space-y-4 p-4 pb-32\">\n              {/* Step tabs */}\n              <div className=\"mb-6 grid grid-cols-2 gap-2\">\n                {steps.map((step, index) => (\n                  <StepTab\n                    key={index}\n                    step={step}\n                    isActive={currentStep === index}\n                    onClick={() => handleStepSelect(index)}\n                    isCompleted={completedSteps.includes(index)}\n                  />\n                ))}\n              </div>\n              {/* Preview */}\n              <div className=\"ring-border ring-offset-background relative aspect-[16/9] overflow-hidden rounded-lg ring-2 ring-offset-8\">\n                <StepPreview step={steps[currentStep]} direction={direction} />\n              </div>\n\n              {/* Step content */}\n              <div className=\"border-border space-y-4 rounded-lg border p-3\">\n                <p className=\"text-muted-foreground\">\n                  {steps[currentStep]?.short_description}\n                </p>\n                {steps[currentStep]?.action && (\n                  <Button\n                    asChild\n                    className=\"w-full\"\n                    variant={\n                      steps[currentStep]?.action?.href ? \"outline\" : \"default\"\n                    }\n                  >\n                    {steps[currentStep]?.action?.href ? (\n                      <a\n                        href={steps[currentStep]?.action?.href}\n                        target=\"_blank\"\n                        rel=\"noopener noreferrer\"\n                        className=\"flex items-center justify-center gap-2\"\n                      >\n                        {steps[currentStep]?.action?.label}\n                        <ExternalLinkIcon className=\"h-4 w-4\" />\n                      </a>\n                    ) : (\n                      <button onClick={steps[currentStep]?.action?.onClick}>\n                        {steps[currentStep]?.action?.label}\n                      </button>\n                    )}\n                  </Button>\n                )}\n              </div>\n            </div>\n          </div>\n\n          {/* Fixed bottom navigation */}\n          <div className=\"bg-background absolute right-0 bottom-0 left-0 border-t\">\n            <div className=\"p-4\">\n              <div className=\"mb-4 flex items-center justify-between\">\n                <Button\n                  variant=\"ghost\"\n                  onClick={onSkip}\n                  className=\"text-muted-foreground hover:bg-card rounded-full\"\n                >\n                  Skip all\n                </Button>\n                <div className=\"space-x-2\">\n                  {currentStep > 0 && (\n                    <Button\n                      onClick={handlePrevious}\n                      size=\"sm\"\n                      variant=\"ghost\"\n                      className=\"rounded-full hover:bg-transparent\"\n                    >\n                      Previous\n                    </Button>\n                  )}\n                  <Button\n                    onClick={() => {\n                      handleNext()\n                    }}\n                    size=\"sm\"\n                    ref={stepRef}\n                    className=\"rounded-full\"\n                  >\n                    {currentStep === steps.length - 1 ? \"Done\" : \"Next\"}\n                  </Button>\n                </div>\n              </div>\n              <div className=\"flex items-center space-x-2\">\n                <Checkbox\n                  id=\"skipNextTime\"\n                  onCheckedChange={(checked) => {\n                    hideFeature()\n                  }}\n                />\n                <label\n                  htmlFor=\"skipNextTime\"\n                  className=\"text-muted-foreground text-sm\"\n                >\n                  Don't show this again\n                </label>\n              </div>\n            </div>\n          </div>\n        </motion.div>\n      </DrawerContent>\n    </Drawer>\n  )\n}\n\nexport default IntroDisclosure\n",
      "type": "registry:ui"
    }
  ]
}