{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "viewer-controls",
  "title": "Viewer Controls",
  "description": "A shared controls row for viewer chrome: title, position, zoom, rotate, download, loading, and extra controls.",
  "dependencies": [
    "lucide-react@^0.514.0",
    "class-variance-authority@^0.7.1",
    "radix-ui@^1.5.0"
  ],
  "registryDependencies": [
    "@retab/utils",
    "button",
    "separator",
    "@retab/skeleton",
    "@retab/spinner",
    "dropdown-menu",
    "@retab/effect-key",
    "@retab/use-keyed-mount-effect"
  ],
  "files": [
    {
      "path": "registry/new-york-v4/ui/viewer-controls.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Download, Maximize, Minus, Plus, RotateCw } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport type { ViewerDownloadAction } from \"@/lib/viewer-download-actions\";\nimport { Button } from \"@/components/ui/button\";\nimport { Separator } from \"@/components/ui/separator\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  ViewerDownloadControl,\n  type ViewerDownloadErrorHandler,\n} from \"@/components/ui/viewer-download\";\n\nexport type ViewerControlPosition =\n  | {\n      kind: \"page\" | \"slide\" | \"frame\";\n      current: number;\n      total?: number;\n    }\n  | {\n      label: React.ReactNode;\n    };\n\nexport type ViewerZoomControl = {\n  scale: number | null;\n  onZoomOut: () => void;\n  onZoomIn: () => void;\n  onFit?: () => void;\n  onReset?: () => void;\n  fitLabel?: string;\n  isDisabled?: boolean;\n};\n\nexport type ViewerRotateControl = {\n  onRotate: () => void;\n  isDisabled?: boolean;\n};\n\nexport type ViewerControlsState = {\n  title?: React.ReactNode;\n  subtitle?: React.ReactNode;\n  position?: ViewerControlPosition | null;\n  zoom?: ViewerZoomControl | null;\n  rotate?: ViewerRotateControl | null;\n  downloads?: ViewerDownloadAction[];\n  loading?: boolean;\n  extra?: React.ReactNode;\n};\n\ntype ViewerControlsRegistration = (state: ViewerControlsState | null) => void;\ntype ViewerControlsRegistrar = (\n  registrationId: symbol,\n  state: ViewerControlsState | null,\n) => void;\n\nconst ViewerControlsRegistrationContext =\n  React.createContext<ViewerControlsRegistrar | null>(null);\n\nexport function ViewerControlsRegistrationProvider({\n  children,\n  onControlsChange,\n}: {\n  children: React.ReactNode;\n  onControlsChange: ViewerControlsRegistration;\n}) {\n  const activeRegistrationRef = React.useRef<symbol | null>(null);\n  const registerControls = React.useCallback<ViewerControlsRegistrar>(\n    (registrationId, state) => {\n      if (state) {\n        activeRegistrationRef.current = registrationId;\n        onControlsChange(state);\n        return;\n      }\n\n      if (activeRegistrationRef.current !== registrationId) return;\n      activeRegistrationRef.current = null;\n      onControlsChange(null);\n    },\n    [onControlsChange],\n  );\n\n  return (\n    <ViewerControlsRegistrationContext.Provider value={registerControls}>\n      {children}\n    </ViewerControlsRegistrationContext.Provider>\n  );\n}\n\nexport function useViewerControlsRegistration(): ViewerControlsRegistration | null {\n  const registerControls = React.useContext(ViewerControlsRegistrationContext);\n  const registrationId = React.useMemo(\n    () => Symbol(\"viewer-controls-registration\"),\n    [],\n  );\n\n  return React.useMemo(() => {\n    if (!registerControls) return null;\n    return (state) => registerControls(registrationId, state);\n  }, [registerControls, registrationId]);\n}\n\nexport type ViewerControlsProps = Omit<React.ComponentProps<\"div\">, \"title\"> & {\n  title?: React.ReactNode;\n  subtitle?: React.ReactNode;\n  position?: ViewerControlPosition | null;\n  zoom?: ViewerZoomControl | null;\n  rotate?: ViewerRotateControl | null;\n  downloads?: ViewerDownloadAction[];\n  onDownloadError?: ViewerDownloadErrorHandler;\n  loading?: boolean;\n  size?: \"default\" | \"sm\";\n  extra?: React.ReactNode;\n};\n\nexport type ViewerControlsSkeletonProps = Omit<\n  React.ComponentProps<\"div\">,\n  \"title\"\n> & {\n  title?: boolean;\n  subtitle?: boolean;\n  position?: boolean;\n  zoom?: boolean;\n  rotate?: boolean;\n  download?: boolean;\n  extra?: React.ReactNode;\n};\n\nexport const VIEWER_CONTROLS_HEIGHT_PX = 40;\n\nexport function ViewerControls({\n  className,\n  title,\n  subtitle,\n  position,\n  zoom,\n  rotate,\n  downloads,\n  onDownloadError,\n  loading = false,\n  size = \"default\",\n  extra,\n  ...props\n}: ViewerControlsProps) {\n  const hasDownloads = Boolean(downloads?.length);\n  const hasControls = Boolean(zoom || rotate || hasDownloads || extra);\n  const hasPlainTitle = typeof title === \"string\" || typeof title === \"number\";\n  const hasMetadataGroup = loading || title != null || subtitle != null;\n\n  return (\n    <div\n      data-slot=\"viewer-controls\"\n      className={cn(\n        \"bg-card flex h-10 flex-shrink-0 items-center gap-1 border-b px-2\",\n        size === \"sm\" && \"h-9 px-2\",\n        className,\n      )}\n      {...props}\n    >\n      {hasMetadataGroup ? (\n        <div className=\"flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden\">\n          {loading ? (\n            <span\n              aria-hidden\n              className=\"bg-primary size-2 flex-shrink-0 animate-pulse rounded-full\"\n            />\n          ) : null}\n          {title != null ? (\n            <div\n              className={cn(\n                \"min-w-0 px-1\",\n                hasPlainTitle && \"truncate text-xs font-medium\",\n              )}\n            >\n              {title}\n            </div>\n          ) : null}\n          {subtitle != null ? (\n            <span className=\"text-muted-foreground hidden min-w-0 truncate px-1 text-xs tabular-nums sm:inline\">\n              {subtitle}\n            </span>\n          ) : null}\n          {position ? (\n            <span className=\"text-muted-foreground flex-shrink-0 px-1 text-xs tabular-nums\">\n              {formatViewerControlPosition(position)}\n            </span>\n          ) : null}\n        </div>\n      ) : position ? (\n        <span className=\"text-muted-foreground flex-shrink-0 px-1 text-xs tabular-nums\">\n          {formatViewerControlPosition(position)}\n        </span>\n      ) : (\n        <div className=\"min-w-0 flex-1\" />\n      )}\n\n      {hasControls ? (\n        <div className=\"ml-auto flex flex-shrink-0 items-center gap-1\">\n          {zoom ? <ViewerZoomControls zoom={zoom} /> : null}\n          {zoom && rotate ? <ViewerControlSeparator /> : null}\n          {rotate ? <ViewerRotateControlButton rotate={rotate} /> : null}\n          {(zoom || rotate) && hasDownloads ? <ViewerControlSeparator /> : null}\n          {hasDownloads ? (\n            <ViewerDownloadControl\n              actions={downloads ?? []}\n              onError={onDownloadError}\n            />\n          ) : null}\n          {(zoom || rotate || hasDownloads) && extra ? (\n            <ViewerControlSeparator />\n          ) : null}\n          {extra}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function ViewerControlsSkeleton({\n  className,\n  title = false,\n  subtitle = false,\n  position = false,\n  zoom = false,\n  rotate = false,\n  download = false,\n  extra,\n  ...props\n}: ViewerControlsSkeletonProps) {\n  const hasControls = zoom || rotate || download || Boolean(extra);\n\n  return (\n    <div\n      data-slot=\"viewer-controls-skeleton\"\n      className={cn(\n        \"bg-card flex h-10 flex-shrink-0 items-center gap-1 border-b px-2\",\n        className,\n      )}\n      {...props}\n    >\n      <div className=\"flex min-w-0 flex-1 items-center gap-1.5 overflow-hidden\">\n        {title ? (\n          <span className=\"min-w-0 truncate px-1\">\n            <Skeleton className=\"inline-block h-3 w-24 align-middle\" />\n          </span>\n        ) : null}\n        {subtitle ? (\n          <span className=\"hidden min-w-0 truncate px-1 sm:inline\">\n            <Skeleton className=\"inline-block h-3 w-16 align-middle\" />\n          </span>\n        ) : null}\n        {position ? (\n          <span className=\"flex-shrink-0 px-1\">\n            <Skeleton className=\"inline-block h-3 w-12 align-middle\" />\n          </span>\n        ) : null}\n      </div>\n\n      {hasControls ? (\n        <div className=\"ml-auto flex flex-shrink-0 items-center gap-1\">\n          {zoom ? (\n            <>\n              <ViewerControlButton\n                disabled\n                aria-hidden\n                tabIndex={-1}\n                label=\"Zoom out\"\n              >\n                <Minus />\n              </ViewerControlButton>\n              <span className=\"w-12 text-center\">\n                <Skeleton className=\"inline-block h-3 w-8 align-middle\" />\n              </span>\n              <ViewerControlButton\n                disabled\n                aria-hidden\n                tabIndex={-1}\n                label=\"Zoom in\"\n              >\n                <Plus />\n              </ViewerControlButton>\n              <ViewerControlButton\n                disabled\n                aria-hidden\n                tabIndex={-1}\n                label=\"Fit width\"\n              >\n                <Maximize />\n              </ViewerControlButton>\n            </>\n          ) : null}\n          {zoom && rotate ? <ViewerControlSeparator /> : null}\n          {rotate ? (\n            <ViewerControlButton\n              disabled\n              aria-hidden\n              tabIndex={-1}\n              label=\"Rotate\"\n            >\n              <RotateCw />\n            </ViewerControlButton>\n          ) : null}\n          {(zoom || rotate) && download ? <ViewerControlSeparator /> : null}\n          {download ? (\n            <ViewerControlButton\n              disabled\n              aria-hidden\n              tabIndex={-1}\n              label=\"Download\"\n            >\n              <Download />\n            </ViewerControlButton>\n          ) : null}\n          {(zoom || rotate || download) && extra ? (\n            <ViewerControlSeparator />\n          ) : null}\n          {extra}\n        </div>\n      ) : null}\n    </div>\n  );\n}\n\nexport function ViewerControlButton({\n  label,\n  children,\n  className,\n  ...props\n}: React.ComponentProps<typeof Button> & { label: string }) {\n  return (\n    <Button\n      variant=\"ghost\"\n      size=\"iconSm\"\n      className={cn(\"size-7\", className)}\n      aria-label={label}\n      title={label}\n      {...props}\n    >\n      {children}\n    </Button>\n  );\n}\n\nexport function formatViewerControlPosition(position: ViewerControlPosition) {\n  if (\"label\" in position) return position.label;\n\n  const label =\n    position.kind === \"page\"\n      ? \"Page\"\n      : position.kind === \"slide\"\n        ? \"Slide\"\n        : \"Frame\";\n  const current =\n    position.total == null\n      ? position.current\n      : Math.min(position.current, position.total);\n\n  return position.total == null\n    ? `${label} ${current}`\n    : `${label} ${current} of ${position.total}`;\n}\n\nfunction ViewerZoomControls({ zoom }: { zoom: ViewerZoomControl }) {\n  const fitLabel = zoom.fitLabel ?? \"Fit width\";\n\n  return (\n    <>\n      <ViewerControlButton\n        label=\"Zoom out\"\n        onClick={zoom.onZoomOut}\n        disabled={zoom.isDisabled}\n      >\n        <Minus />\n      </ViewerControlButton>\n      {zoom.onReset ? (\n        <button\n          type=\"button\"\n          className=\"text-muted-foreground hover:text-foreground w-12 text-center text-xs tabular-nums disabled:pointer-events-none disabled:opacity-64\"\n          title=\"Reset zoom\"\n          onClick={zoom.onReset}\n          disabled={zoom.isDisabled}\n        >\n          {zoom.scale == null ? \"Fit\" : `${Math.round(zoom.scale * 100)}%`}\n        </button>\n      ) : (\n        <span className=\"text-muted-foreground w-12 text-center text-xs tabular-nums\">\n          {zoom.scale == null ? \"Fit\" : `${Math.round(zoom.scale * 100)}%`}\n        </span>\n      )}\n      <ViewerControlButton\n        label=\"Zoom in\"\n        onClick={zoom.onZoomIn}\n        disabled={zoom.isDisabled}\n      >\n        <Plus />\n      </ViewerControlButton>\n      {zoom.onFit ? (\n        <ViewerControlButton\n          label={fitLabel}\n          onClick={zoom.onFit}\n          disabled={zoom.isDisabled}\n        >\n          <Maximize />\n        </ViewerControlButton>\n      ) : null}\n    </>\n  );\n}\n\nfunction ViewerRotateControlButton({\n  rotate,\n}: {\n  rotate: ViewerRotateControl;\n}) {\n  return (\n    <ViewerControlButton\n      label=\"Rotate\"\n      onClick={rotate.onRotate}\n      disabled={rotate.isDisabled}\n    >\n      <RotateCw />\n    </ViewerControlButton>\n  );\n}\n\nfunction ViewerControlSeparator() {\n  return <Separator orientation=\"vertical\" className=\"mx-1 h-4\" />;\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-controls.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/viewer-download.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { Download } from \"lucide-react\";\n\nimport { cn } from \"@/lib/utils\";\nimport {\n  ViewerDownloadError,\n  type ViewerDownloadAction,\n  type ViewerDownloadPayload,\n} from \"@/lib/viewer-download-actions\";\n\nimport { Spinner } from \"@/components/ui/spinner\";\n\nimport { Button, buttonVariants } from \"./button\";\nimport {\n  DropdownMenu,\n  DropdownMenuContent,\n  DropdownMenuItem,\n  DropdownMenuTrigger,\n} from \"./dropdown-menu\";\nimport { useKeyedMountEffect } from \"@/hooks/use-keyed-mount-effect\";\nimport { joinEffectKey } from \"@/lib/effect-key\";\n\nexport interface TriggerViewerDownloadOptions {\n  signal?: AbortSignal;\n}\n\nexport async function triggerViewerDownload(\n  action: ViewerDownloadAction,\n  options?: TriggerViewerDownloadOptions,\n): Promise<void> {\n  if (action.isDisabled) {\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"disabled\",\n      message: \"This download is disabled.\",\n    });\n  }\n\n  let payload: ViewerDownloadPayload;\n  try {\n    payload = await action.getPayload(options);\n  } catch (error) {\n    if (isAbortError(error)) {\n      throw new ViewerDownloadError({\n        actionId: action.id,\n        kind: \"aborted\",\n        message: \"Download was cancelled.\",\n        cause: error,\n      });\n    }\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"payload_failed\",\n      message: \"Could not prepare this download.\",\n      cause: error,\n    });\n  }\n\n  if (payload.kind === \"none\") return;\n  if (payload.kind === \"href\") {\n    try {\n      clickDownload(payload.href, action.fileName);\n    } catch (error) {\n      throw new ViewerDownloadError({\n        actionId: action.id,\n        kind: \"unsupported\",\n        message: \"Could not start this download.\",\n        cause: error,\n      });\n    }\n    return;\n  }\n\n  try {\n    const blob =\n      payload.kind === \"blob\"\n        ? payload.blob\n        : new Blob([payload.text], {\n            type: payload.mimeType ?? \"text/plain;charset=utf-8\",\n          });\n    const url = URL.createObjectURL(blob);\n    try {\n      clickDownload(url, action.fileName);\n    } finally {\n      URL.revokeObjectURL(url);\n    }\n  } catch (error) {\n    throw new ViewerDownloadError({\n      actionId: action.id,\n      kind: \"unsupported\",\n      message: \"Could not start this download.\",\n      cause: error,\n    });\n  }\n}\n\nexport type ViewerDownloadErrorHandler = (\n  error: ViewerDownloadError,\n  action: ViewerDownloadAction,\n) => void;\n\nexport interface ViewerDownloadTrigger {\n  pendingActionId: string | null;\n  triggerDownload: (action: ViewerDownloadAction) => void;\n}\n\nexport interface ViewerDownloadTriggerOptions {\n  /** Reports non-aborted action failures; visible failure UI belongs to consumers. */\n  onError?: ViewerDownloadErrorHandler;\n  resetKey?: unknown;\n}\n\nexport interface ViewerDownloadControlProps {\n  actions: Array<ViewerDownloadAction | null | undefined>;\n  variant?: React.ComponentProps<typeof Button>[\"variant\"];\n  size?: React.ComponentProps<typeof Button>[\"size\"];\n  className?: string;\n  showLabel?: boolean;\n  /** Reports non-aborted action failures; visible failure UI belongs to consumers. */\n  onError?: ViewerDownloadErrorHandler;\n}\n\nexport interface ViewerDownloadButtonProps\n  extends Omit<ViewerDownloadControlProps, \"actions\"> {\n  action: ViewerDownloadAction | null;\n}\n\nexport interface ViewerDownloadMenuProps\n  extends Omit<ViewerDownloadControlProps, \"actions\"> {\n  actions: ViewerDownloadAction[];\n}\n\nexport function useViewerDownloadHref(\n  action: ViewerDownloadAction | null,\n): string | null {\n  const shouldCreateHref = action?.origin !== \"derived\";\n  const payload = shouldCreateHref ? getSynchronousPayload(action) : null;\n\n  return shouldCreateHref && payload?.kind === \"href\" ? payload.href : null;\n}\n\nexport function useViewerDownloadTrigger({\n  onError,\n  resetKey = \"\",\n}: ViewerDownloadTriggerOptions = {}): ViewerDownloadTrigger {\n  const [pendingActionId, setPendingActionId] = React.useState<string | null>(\n    null,\n  );\n  const abortControllerRef = React.useRef<AbortController | null>(null);\n\n  useKeyedMountEffect(joinEffectKey([resetKey]), () => {\n    return () => {\n      abortControllerRef.current?.abort();\n      abortControllerRef.current = null;\n    };\n  });\n\n  const triggerDownload = React.useCallback(\n    (action: ViewerDownloadAction) => {\n      abortControllerRef.current?.abort();\n      const abortController = new AbortController();\n      abortControllerRef.current = abortController;\n      setPendingActionId(action.id);\n      void triggerViewerDownload(action, { signal: abortController.signal })\n        .catch((error) => {\n          reportDownloadError(error, action, onError);\n        })\n        .finally(() => {\n          if (abortControllerRef.current === abortController) {\n            abortControllerRef.current = null;\n            setPendingActionId(null);\n          }\n        });\n    },\n    [onError],\n  );\n\n  return { pendingActionId, triggerDownload };\n}\n\nexport function ViewerDownloadControl({\n  actions,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadControlProps) {\n  const enabledActions = actions.filter(\n    (action): action is ViewerDownloadAction => Boolean(action),\n  );\n\n  if (enabledActions.length <= 1) {\n    return (\n      <ViewerDownloadButton\n        action={enabledActions[0] ?? null}\n        variant={variant}\n        size={size}\n        className={className}\n        showLabel={showLabel}\n        onError={onError}\n      />\n    );\n  }\n\n  return (\n    <ViewerDownloadMenu\n      actions={enabledActions}\n      variant={variant}\n      size={size}\n      className={className}\n      showLabel={showLabel}\n      onError={onError}\n    />\n  );\n}\n\nexport function ViewerDownloadButton({\n  action,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadButtonProps) {\n  const href = useViewerDownloadHref(action);\n  const label = action?.label ?? \"Download\";\n  const disabled = !action || action.isDisabled;\n  const hasDownloadHref = Boolean(href);\n  const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({\n    onError,\n    resetKey: action,\n  });\n  const isPending = Boolean(action && pendingActionId === action.id);\n\n  const handleClick = React.useCallback(() => {\n    if (!action || hasDownloadHref) return;\n    triggerDownload(action);\n  }, [action, hasDownloadHref, triggerDownload]);\n\n  if (href) {\n    return (\n      <a\n        href={href}\n        download={action?.fileName}\n        className={cn(buttonVariants({ variant, size }), className)}\n        aria-label={label}\n        title={label}\n        data-slot=\"button\"\n      >\n        <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n        {showLabel ? label : null}\n      </a>\n    );\n  }\n\n  return (\n    <Button\n      variant={variant}\n      size={size}\n      className={className}\n      aria-label={label}\n      title={label}\n      disabled={disabled || isPending}\n      onClick={handleClick}\n    >\n      {isPending ? (\n        <Spinner className=\"size-4 animate-spin\" />\n      ) : (\n        <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n      )}\n      {showLabel ? label : null}\n    </Button>\n  );\n}\n\nexport function ViewerDownloadMenu({\n  actions,\n  variant = \"ghost\",\n  size = \"iconSm\",\n  className = \"size-7\",\n  showLabel = false,\n  onError,\n}: ViewerDownloadMenuProps) {\n  const actionSetKey = actions.map((action) => action.id).join(\"\\u0000\");\n  const label = \"Download\";\n  const { pendingActionId, triggerDownload } = useViewerDownloadTrigger({\n    onError,\n    resetKey: actionSetKey,\n  });\n\n  return (\n    <DropdownMenu>\n      <DropdownMenuTrigger asChild>\n        <Button\n          variant={variant}\n          size={size}\n          className={className}\n          aria-label={label}\n          title={label}\n          disabled={pendingActionId != null}\n        >\n          {pendingActionId != null ? (\n            <Spinner className=\"size-4 animate-spin\" />\n          ) : (\n            <Download className={showLabel ? \"mr-1.5 size-4\" : undefined} />\n          )}\n          {showLabel ? label : null}\n        </Button>\n      </DropdownMenuTrigger>\n      <DropdownMenuContent align=\"end\">\n        {actions.map((action) => (\n          <DropdownMenuItem\n            key={action.id}\n            disabled={action.isDisabled || pendingActionId != null}\n            onClick={() => triggerDownload(action)}\n          >\n            <Download />\n            <span>{action.label}</span>\n          </DropdownMenuItem>\n        ))}\n      </DropdownMenuContent>\n    </DropdownMenu>\n  );\n}\n\nfunction clickDownload(href: string, fileName: string) {\n  const anchor = document.createElement(\"a\");\n  anchor.href = href;\n  anchor.download = fileName;\n  anchor.rel = \"noreferrer\";\n  document.body.appendChild(anchor);\n  anchor.click();\n  anchor.remove();\n}\n\nfunction getSynchronousPayload(\n  action: ViewerDownloadAction | null,\n): ViewerDownloadPayload | null {\n  if (!action || action.isDisabled) return null;\n  const payload = action.getPayload();\n  return payload instanceof Promise ? null : payload;\n}\n\nfunction reportDownloadError(\n  error: unknown,\n  action: ViewerDownloadAction,\n  onError: ViewerDownloadErrorHandler | undefined,\n) {\n  if (!(error instanceof ViewerDownloadError)) return;\n  if (error.kind === \"aborted\") return;\n  onError?.(error, action);\n}\n\nfunction isAbortError(error: unknown) {\n  return error instanceof DOMException && error.name === \"AbortError\";\n}\n",
      "type": "registry:ui",
      "target": "@ui/viewer-download.tsx"
    },
    {
      "path": "registry/new-york-v4/lib/viewer-download-actions.ts",
      "content": "export type ViewerDownloadOrigin = \"original\" | \"derived\";\n\nexport type ViewerDownloadPayload =\n  | { kind: \"href\"; href: string }\n  | { kind: \"blob\"; blob: Blob }\n  | { kind: \"text\"; text: string; mimeType?: string }\n  | { kind: \"none\" };\n\nexport interface ViewerDownloadAction {\n  id: string;\n  label: string;\n  fileName: string;\n  origin: ViewerDownloadOrigin;\n  isDisabled?: boolean;\n  getPayload: (options?: {\n    signal?: AbortSignal;\n  }) => ViewerDownloadPayload | Promise<ViewerDownloadPayload>;\n}\n\nexport type ViewerDownloadErrorKind =\n  | \"disabled\"\n  | \"aborted\"\n  | \"payload_failed\"\n  | \"unsupported\";\n\nexport class ViewerDownloadError extends Error {\n  readonly kind: ViewerDownloadErrorKind;\n  readonly actionId: string;\n  override readonly cause?: unknown;\n\n  constructor({\n    actionId,\n    kind,\n    message,\n    cause,\n  }: {\n    actionId: string;\n    kind: ViewerDownloadErrorKind;\n    message: string;\n    cause?: unknown;\n  }) {\n    super(message);\n    this.name = \"ViewerDownloadError\";\n    this.actionId = actionId;\n    this.kind = kind;\n    this.cause = cause;\n  }\n}\n\nexport function createHrefDownloadAction({\n  id,\n  label = \"Download\",\n  href,\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  href: string;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"href\", href }),\n  };\n}\n\nexport function createBlobDownloadAction({\n  id,\n  label = \"Download\",\n  blob,\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  blob: Blob;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"blob\", blob }),\n  };\n}\n\nexport function createTextDownloadAction({\n  id,\n  label = \"Download\",\n  text,\n  fileName,\n  mimeType,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  text: string;\n  fileName: string;\n  mimeType?: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    getPayload: () => ({ kind: \"text\", text, mimeType }),\n  };\n}\n\nexport function createDisabledDownloadAction({\n  id,\n  label = \"Download\",\n  fileName,\n  origin = \"original\",\n}: {\n  id: string;\n  label?: string;\n  fileName: string;\n  origin?: ViewerDownloadOrigin;\n}): ViewerDownloadAction {\n  return {\n    id,\n    label,\n    fileName,\n    origin,\n    isDisabled: true,\n    getPayload: () => ({ kind: \"none\" }),\n  };\n}\n",
      "type": "registry:lib",
      "target": "@lib/viewer-download-actions.ts"
    },
    {
      "path": "registry/new-york-v4/ui/button.tsx",
      "content": "import * as React from \"react\";\nimport { cva, type VariantProps } from \"class-variance-authority\";\nimport { Slot } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nconst buttonVariants = cva(\n  \"inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n  {\n    variants: {\n      variant: {\n        default: \"bg-primary text-primary-foreground hover:bg-primary/90\",\n        destructive:\n          \"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40\",\n        outline:\n          \"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50\",\n        secondary:\n          \"bg-secondary text-secondary-foreground hover:bg-secondary/80\",\n        ghost:\n          \"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50\",\n        link: \"text-primary underline-offset-4 hover:underline\",\n      },\n      size: {\n        default: \"h-9 px-4 py-2 has-[>svg]:px-3\",\n        xs: \"h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3\",\n        sm: \"h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5\",\n        lg: \"h-10 rounded-md px-6 has-[>svg]:px-4\",\n        icon: \"size-9\",\n        iconXs: \"size-6 rounded-md [&_svg:not([class*='size-'])]:size-3\",\n        iconSm: \"size-8\",\n        iconLg: \"size-10\",\n      },\n    },\n    defaultVariants: {\n      variant: \"default\",\n      size: \"default\",\n    },\n  },\n);\n\nfunction Button({\n  className,\n  variant = \"default\",\n  size = \"default\",\n  asChild = false,\n  ...props\n}: React.ComponentProps<\"button\"> &\n  VariantProps<typeof buttonVariants> & {\n    asChild?: boolean;\n  }) {\n  const Comp = asChild ? Slot.Root : \"button\";\n\n  return (\n    <Comp\n      data-slot=\"button\"\n      data-variant={variant}\n      data-size={size}\n      className={cn(buttonVariants({ variant, size, className }))}\n      {...props}\n    />\n  );\n}\n\nexport { Button, buttonVariants };\n",
      "type": "registry:ui",
      "target": "@ui/button.tsx"
    },
    {
      "path": "registry/new-york-v4/ui/dropdown-menu.tsx",
      "content": "\"use client\";\n\nimport * as React from \"react\";\nimport { CheckIcon, ChevronRightIcon, CircleIcon } from \"lucide-react\";\nimport { DropdownMenu as DropdownMenuPrimitive } from \"radix-ui\";\n\nimport { cn } from \"@/lib/utils\";\n\nfunction DropdownMenu({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {\n  return <DropdownMenuPrimitive.Root data-slot=\"dropdown-menu\" {...props} />;\n}\n\nfunction DropdownMenuPortal({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {\n  return (\n    <DropdownMenuPrimitive.Portal data-slot=\"dropdown-menu-portal\" {...props} />\n  );\n}\n\nfunction DropdownMenuTrigger({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {\n  return (\n    <DropdownMenuPrimitive.Trigger\n      data-slot=\"dropdown-menu-trigger\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuContent({\n  className,\n  sideOffset = 4,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {\n  return (\n    <DropdownMenuPrimitive.Portal>\n      <DropdownMenuPrimitive.Content\n        data-slot=\"dropdown-menu-content\"\n        sideOffset={sideOffset}\n        className={cn(\n          \"bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md\",\n          className,\n        )}\n        {...props}\n      />\n    </DropdownMenuPrimitive.Portal>\n  );\n}\n\nfunction DropdownMenuGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {\n  return (\n    <DropdownMenuPrimitive.Group data-slot=\"dropdown-menu-group\" {...props} />\n  );\n}\n\nfunction DropdownMenuItem({\n  className,\n  inset,\n  variant = \"default\",\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {\n  inset?: boolean;\n  variant?: \"default\" | \"destructive\";\n}) {\n  return (\n    <DropdownMenuPrimitive.Item\n      data-slot=\"dropdown-menu-item\"\n      data-inset={inset}\n      data-variant={variant}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive! relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuCheckboxItem({\n  className,\n  children,\n  checked,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {\n  return (\n    <DropdownMenuPrimitive.CheckboxItem\n      data-slot=\"dropdown-menu-checkbox-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      checked={checked}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CheckIcon className=\"size-4\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.CheckboxItem>\n  );\n}\n\nfunction DropdownMenuRadioGroup({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {\n  return (\n    <DropdownMenuPrimitive.RadioGroup\n      data-slot=\"dropdown-menu-radio-group\"\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuRadioItem({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {\n  return (\n    <DropdownMenuPrimitive.RadioItem\n      data-slot=\"dropdown-menu-radio-item\"\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      <span className=\"pointer-events-none absolute left-2 flex size-3.5 items-center justify-center\">\n        <DropdownMenuPrimitive.ItemIndicator>\n          <CircleIcon className=\"size-2 fill-current\" />\n        </DropdownMenuPrimitive.ItemIndicator>\n      </span>\n      {children}\n    </DropdownMenuPrimitive.RadioItem>\n  );\n}\n\nfunction DropdownMenuLabel({\n  className,\n  inset,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.Label\n      data-slot=\"dropdown-menu-label\"\n      data-inset={inset}\n      className={cn(\n        \"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSeparator({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {\n  return (\n    <DropdownMenuPrimitive.Separator\n      data-slot=\"dropdown-menu-separator\"\n      className={cn(\"bg-border -mx-1 my-1 h-px\", className)}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuShortcut({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"dropdown-menu-shortcut\"\n      className={cn(\n        \"text-muted-foreground ml-auto text-xs tracking-widest\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nfunction DropdownMenuSub({\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {\n  return <DropdownMenuPrimitive.Sub data-slot=\"dropdown-menu-sub\" {...props} />;\n}\n\nfunction DropdownMenuSubTrigger({\n  className,\n  inset,\n  children,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {\n  inset?: boolean;\n}) {\n  return (\n    <DropdownMenuPrimitive.SubTrigger\n      data-slot=\"dropdown-menu-sub-trigger\"\n      data-inset={inset}\n      className={cn(\n        \"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n      <ChevronRightIcon className=\"ml-auto size-4\" />\n    </DropdownMenuPrimitive.SubTrigger>\n  );\n}\n\nfunction DropdownMenuSubContent({\n  className,\n  ...props\n}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {\n  return (\n    <DropdownMenuPrimitive.SubContent\n      data-slot=\"dropdown-menu-sub-content\"\n      className={cn(\n        \"bg-popover text-popover-foreground data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg\",\n        className,\n      )}\n      {...props}\n    />\n  );\n}\n\nexport {\n  DropdownMenu,\n  DropdownMenuPortal,\n  DropdownMenuTrigger,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuLabel,\n  DropdownMenuItem,\n  DropdownMenuCheckboxItem,\n  DropdownMenuRadioGroup,\n  DropdownMenuRadioItem,\n  DropdownMenuSeparator,\n  DropdownMenuShortcut,\n  DropdownMenuSub,\n  DropdownMenuSubTrigger,\n  DropdownMenuSubContent,\n};\n",
      "type": "registry:ui",
      "target": "@ui/dropdown-menu.tsx"
    }
  ],
  "type": "registry:ui"
}