{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "share-dialog",
  "type": "registry:component",
  "title": "Share Dialog",
  "description": "Dialog that mints a shareable download or upload link with a chosen expiry.",
  "dependencies": [
    "files-sdk",
    "lucide-react"
  ],
  "registryDependencies": [
    "button",
    "dialog",
    "input"
  ],
  "files": [
    {
      "path": "registry/files-sdk/share-dialog/share-dialog.tsx",
      "type": "registry:component",
      "target": "components/files-sdk/share-dialog.tsx",
      "content": "\"use client\";\n\nimport type { AdapterCapabilities } from \"files-sdk\";\nimport type { UseFilesResult } from \"files-sdk/react\";\nimport { CheckIcon, CopyIcon, Link2Icon, Loader2Icon } from \"lucide-react\";\nimport type { ReactNode } from \"react\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\n\nimport { Button, buttonVariants } from \"@/components/ui/button\";\nimport {\n  Dialog,\n  DialogContent,\n  DialogDescription,\n  DialogHeader,\n  DialogTitle,\n  DialogTrigger,\n} from \"@/components/ui/dialog\";\nimport { Input } from \"@/components/ui/input\";\nimport { cn } from \"@/lib/utils\";\n\nexport interface ShareDialogProps {\n  /** A `useFiles()` instance — the link is minted through it. */\n  files: UseFilesResult;\n  /** The key to share. */\n  fileKey: string;\n  /** `\"download\"` mints a `url()`; `\"upload\"` mints a `signedUploadUrl()`. Default `\"download\"`. */\n  mode?: \"download\" | \"upload\";\n  /** Initial expiry in seconds. Default `3600` (1 hour). */\n  defaultExpiresIn?: number;\n  /** Custom trigger content, rendered inside the trigger button. Defaults to a styled \"Share\" label. */\n  children?: ReactNode;\n  className?: string;\n}\n\nconst EXPIRY_PRESETS = [\n  { label: \"5 min\", seconds: 300 },\n  { label: \"1 hour\", seconds: 3600 },\n  { label: \"1 day\", seconds: 86_400 },\n  { label: \"7 days\", seconds: 604_800 },\n];\n\n/**\n * A dialog that mints a shareable link for one key. Download links go through\n * `url()` (a signed URL where the adapter supports it, otherwise a public one);\n * upload links go through `signedUploadUrl()`. Pick an expiry, copy the result.\n */\nexport const ShareDialog = ({\n  files,\n  fileKey,\n  mode = \"download\",\n  defaultExpiresIn = 3600,\n  children,\n  className,\n}: ShareDialogProps) => {\n  const [open, setOpen] = useState(false);\n  const [expiresIn, setExpiresIn] = useState(defaultExpiresIn);\n  const [disposition, setDisposition] = useState<\"attachment\" | \"inline\">(\n    \"attachment\"\n  );\n  const [url, setUrl] = useState<string>();\n  const [isGenerating, setIsGenerating] = useState(false);\n  const [copied, setCopied] = useState(false);\n  const [caps, setCaps] = useState<AdapterCapabilities>();\n\n  const filesRef = useRef(files);\n  filesRef.current = files;\n\n  useEffect(() => {\n    if (!open) {\n      return;\n    }\n    let cancelled = false;\n    const run = async () => {\n      try {\n        const result = await filesRef.current.capabilities();\n        if (!cancelled) {\n          setCaps(result);\n        }\n      } catch {\n        // Non-fatal: we just lose the expiry clamp + support hint.\n      }\n    };\n    void run();\n    return () => {\n      cancelled = true;\n    };\n  }, [open]);\n\n  const maxExpiresIn = caps?.signedUrl.maxExpiresIn;\n  const presets = EXPIRY_PRESETS.filter(\n    (preset) => !maxExpiresIn || preset.seconds <= maxExpiresIn\n  );\n\n  const generate = useCallback(async () => {\n    setIsGenerating(true);\n    setCopied(false);\n    setUrl(undefined);\n    try {\n      const ttl = maxExpiresIn ? Math.min(expiresIn, maxExpiresIn) : expiresIn;\n      if (mode === \"upload\") {\n        const signed = await filesRef.current.signedUploadUrl(fileKey, {\n          expiresIn: ttl,\n        });\n        setUrl(signed.url);\n      } else {\n        const link = await filesRef.current.url(fileKey, {\n          expiresIn: ttl,\n          responseContentDisposition: disposition,\n        });\n        setUrl(link);\n      }\n    } catch {\n      // The hook mirrors the error to `files.error` for display.\n    } finally {\n      setIsGenerating(false);\n    }\n  }, [mode, fileKey, expiresIn, disposition, maxExpiresIn]);\n\n  const copy = useCallback(async () => {\n    if (!url) {\n      return;\n    }\n    await navigator.clipboard.writeText(url);\n    setCopied(true);\n  }, [url]);\n\n  return (\n    <Dialog onOpenChange={setOpen} open={open}>\n      {/* Styled via buttonVariants instead of asChild-wrapping a Button: the\n          trigger must work with both the Radix and Base UI shadcn flavors, and\n          Base UI has no asChild (nesting a Button renders <button> inside\n          <button>). */}\n      <DialogTrigger\n        className={cn(\n          !children && buttonVariants({ size: \"sm\", variant: \"outline\" }),\n          className\n        )}\n      >\n        {children ?? (\n          <>\n            <Link2Icon />\n            Share\n          </>\n        )}\n      </DialogTrigger>\n      <DialogContent>\n        <DialogHeader>\n          <DialogTitle>\n            {mode === \"upload\" ? \"Upload link\" : \"Share link\"}\n          </DialogTitle>\n          <DialogDescription className=\"truncate\">{fileKey}</DialogDescription>\n        </DialogHeader>\n\n        <div className=\"flex flex-col gap-4\">\n          <div className=\"flex flex-col gap-1.5\">\n            <span className=\"font-medium text-sm\">Expires after</span>\n            <div className=\"flex flex-wrap gap-1.5\">\n              {presets.map((preset) => (\n                <Button\n                  key={preset.seconds}\n                  onClick={() => {\n                    setExpiresIn(preset.seconds);\n                    setUrl(undefined);\n                  }}\n                  size=\"xs\"\n                  type=\"button\"\n                  variant={preset.seconds === expiresIn ? \"secondary\" : \"ghost\"}\n                >\n                  {preset.label}\n                </Button>\n              ))}\n            </div>\n          </div>\n\n          {mode === \"download\" && (\n            <div className=\"flex flex-col gap-1.5\">\n              <span className=\"font-medium text-sm\">Open as</span>\n              <div className=\"flex flex-wrap gap-1.5\">\n                {([\"attachment\", \"inline\"] as const).map((value) => (\n                  <Button\n                    key={value}\n                    onClick={() => {\n                      setDisposition(value);\n                      setUrl(undefined);\n                    }}\n                    size=\"xs\"\n                    type=\"button\"\n                    variant={value === disposition ? \"secondary\" : \"ghost\"}\n                  >\n                    {value === \"attachment\" ? \"Download\" : \"Inline\"}\n                  </Button>\n                ))}\n              </div>\n            </div>\n          )}\n\n          {caps && !caps.signedUrl.supported && mode === \"download\" && (\n            <p className=\"text-muted-foreground text-xs\">\n              This adapter can't sign URLs, so the link is a permanent public\n              URL and ignores the expiry.\n            </p>\n          )}\n\n          {url ? (\n            <div className=\"flex gap-2\">\n              <Input readOnly value={url} />\n              <Button\n                onClick={() => void copy()}\n                type=\"button\"\n                variant=\"outline\"\n              >\n                {copied ? (\n                  <CheckIcon className=\"text-emerald-600 dark:text-emerald-400\" />\n                ) : (\n                  <CopyIcon />\n                )}\n                {copied ? \"Copied\" : \"Copy\"}\n              </Button>\n            </div>\n          ) : (\n            <Button\n              disabled={isGenerating}\n              onClick={() => void generate()}\n              type=\"button\"\n            >\n              {isGenerating && <Loader2Icon className=\"animate-spin\" />}\n              Generate link\n            </Button>\n          )}\n        </div>\n      </DialogContent>\n    </Dialog>\n  );\n};\n"
    }
  ]
}
