{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "chat-composer-attachment",
  "type": "registry:component",
  "title": "ChatComposerAttachment",
  "description": "Composer attachment rail with file previews, upload progress, and removal.",
  "dependencies": [
    "lucide-react@^1.27.0"
  ],
  "registryDependencies": [
    "http://127.0.0.1:3000/r/button.json",
    "http://127.0.0.1:3000/r/core.json",
    "http://127.0.0.1:3000/r/scroll-area.json"
  ],
  "files": [
    {
      "path": "src/registry/sources/control-ui/chat-composer-attachment.tsx",
      "target": "@components/control-ui/chat-composer-attachment.tsx",
      "type": "registry:component",
      "content": "\"use client\";\n\nimport { XIcon } from \"lucide-react\";\nimport type { ComponentProps, CSSProperties, MouseEvent, ReactNode } from \"react\";\nimport { createContext, useContext } from \"react\";\n\nimport { cn } from \"@/components/control-ui/lib/cn\";\nimport { skinSlot } from \"@/components/control-ui/skin\";\nimport { Button } from \"@/components/control-ui/ui/button\";\nimport { ScrollArea } from \"@/components/control-ui/ui/scroll-area\";\n\nexport type ChatComposerAttachmentKind = \"image\" | \"pdf\" | \"spreadsheet\" | \"document\" | \"archive\" | \"audio\" | \"video\" | \"file\";\nexport type ChatComposerAttachmentStatus = \"idle\" | \"uploading\" | \"uploaded\" | \"error\";\nexport type ChatComposerAttachmentVariant = \"auto\" | \"preview\" | \"file\";\n\n/** Typed so custom property is declared rather than asserted. */\ntype ChatComposerAttachmentStyle = CSSProperties & { \"--chat-composer-attachment-progress\"?: string };\n\ntype ChatComposerAttachmentContextValue = {\n  name: string;\n  type?: string;\n  description?: ReactNode;\n  status: ChatComposerAttachmentStatus;\n  progress?: number;\n  previewUrl?: string;\n  kind: ChatComposerAttachmentKind;\n  variant: Exclude<ChatComposerAttachmentVariant, \"auto\">;\n  extension: string;\n  onRemove?: () => void;\n  removeLabel?: string;\n};\n\nconst ChatComposerAttachmentContext = createContext<ChatComposerAttachmentContextValue | null>(null);\n\nfunction useChatComposerAttachmentContext() {\n  const context = useContext(ChatComposerAttachmentContext);\n  if (!context) throw new Error(\"ChatComposerAttachment compound components must be rendered inside <ChatComposerAttachment>.\");\n  return context;\n}\n\nfunction extensionFromName(name: string) {\n  const match = name.match(/\\.([a-z0-9]+)$/i);\n  return match?.[1]?.toLowerCase() ?? \"\";\n}\n\nfunction kindFromAttachment(type: string | undefined, name: string): ChatComposerAttachmentKind {\n  const mime = type?.toLowerCase() ?? \"\";\n  const extension = extensionFromName(name);\n\n  if (mime.startsWith(\"image/\") || [\"avif\", \"gif\", \"heic\", \"jpeg\", \"jpg\", \"png\", \"webp\"].includes(extension)) return \"image\";\n  if (mime === \"application/pdf\" || extension === \"pdf\") return \"pdf\";\n  if (\n    mime.includes(\"spreadsheet\") ||\n    mime.includes(\"excel\") ||\n    mime.includes(\"csv\") ||\n    [\"csv\", \"numbers\", \"ods\", \"tsv\", \"xls\", \"xlsx\"].includes(extension)\n  ) {\n    return \"spreadsheet\";\n  }\n  if (mime.startsWith(\"audio/\") || [\"aac\", \"flac\", \"m4a\", \"mp3\", \"ogg\", \"wav\"].includes(extension)) return \"audio\";\n  if (mime.startsWith(\"video/\") || [\"avi\", \"mov\", \"mp4\", \"webm\"].includes(extension)) return \"video\";\n  if (mime.includes(\"zip\") || [\"7z\", \"gz\", \"rar\", \"tar\", \"zip\"].includes(extension)) return \"archive\";\n  if (mime.includes(\"document\") || mime.includes(\"text\") || [\"doc\", \"docx\", \"md\", \"rtf\", \"txt\"].includes(extension)) return \"document\";\n  return \"file\";\n}\n\nfunction progressValue(progress: number | undefined) {\n  if (typeof progress !== \"number\" || Number.isNaN(progress)) return undefined;\n  return Math.min(100, Math.max(0, progress));\n}\n\nfunction fallbackLabel(kind: ChatComposerAttachmentKind, extension: string) {\n  if (extension && extension.length <= 4) return extension.toUpperCase();\n  const labels: Record<ChatComposerAttachmentKind, string> = {\n    image: \"IMG\",\n    pdf: \"PDF\",\n    spreadsheet: \"XLS\",\n    document: \"DOC\",\n    archive: \"ZIP\",\n    audio: \"AUD\",\n    video: \"VID\",\n    file: \"FILE\",\n  };\n  return labels[kind];\n}\n\nfunction defaultDescription(context: ChatComposerAttachmentContextValue) {\n  const progress = progressValue(context.progress);\n  if (context.description) return context.description;\n  if (context.status === \"uploading\") return `Uploading ${Math.round(progress ?? 0)}%`;\n  if (context.status === \"error\") return \"Upload failed\";\n  return fallbackLabel(context.kind, context.extension);\n}\n\nexport type ChatComposerAttachmentsProps = ComponentProps<\"div\"> & {\n  label?: string;\n  viewportClassName?: string;\n};\n\nexport function ChatComposerAttachments({\n  label = \"Attachments\",\n  className,\n  viewportClassName,\n  children,\n  ...props\n}: ChatComposerAttachmentsProps) {\n  return (\n    <div\n      data-control-ui=\"chat-composer-attachments\"\n      data-slot=\"root\"\n      className={cn(\"min-w-0\", skinSlot(\"chat-composer-attachments\", \"root\", {}), className)}\n      {...props}\n    >\n      <ScrollArea\n        data-control-ui=\"chat-composer-attachments\"\n        data-slot=\"scroll\"\n        aria-label={label}\n        lockAxis=\"y\"\n        mask\n        scrollbarVisibility=\"hover\"\n        className={cn(\"w-full\", skinSlot(\"chat-composer-attachments\", \"scroll\", {}))}\n        viewportClassName={cn(\"pb-2\", viewportClassName)}\n      >\n        <ul\n          data-control-ui=\"chat-composer-attachments\"\n          data-slot=\"list\"\n          className={cn(\"flex w-max min-w-full list-none gap-2 px-3 pt-3\", skinSlot(\"chat-composer-attachments\", \"list\", {}))}\n        >\n          {children}\n        </ul>\n      </ScrollArea>\n    </div>\n  );\n}\n\nexport type ChatComposerAttachmentProps = ComponentProps<\"li\"> & {\n  name: string;\n  type?: string;\n  description?: ReactNode;\n  status?: ChatComposerAttachmentStatus;\n  progress?: number;\n  previewUrl?: string;\n  kind?: ChatComposerAttachmentKind;\n  variant?: ChatComposerAttachmentVariant;\n  onRemove?: () => void;\n  removeLabel?: string;\n};\n\nexport function ChatComposerAttachment({\n  name,\n  type,\n  description,\n  status = \"idle\",\n  progress: progressInput,\n  previewUrl,\n  kind,\n  variant = \"auto\",\n  onRemove,\n  removeLabel,\n  className,\n  children,\n  style,\n  role,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ChatComposerAttachmentProps) {\n  const resolvedKind = kind ?? kindFromAttachment(type, name);\n  let resolvedVariant: Exclude<ChatComposerAttachmentVariant, \"auto\">;\n  if (variant === \"auto\") resolvedVariant = previewUrl || resolvedKind === \"image\" ? \"preview\" : \"file\";\n  else resolvedVariant = variant;\n  const extension = extensionFromName(name);\n  const progress = progressValue(progressInput);\n  const attachmentStyle: ChatComposerAttachmentStyle = {\n    \"--chat-composer-attachment-progress\": `${progress ?? 0}%`,\n    ...style,\n  };\n\n  return (\n    <ChatComposerAttachmentContext.Provider\n      value={{\n        name,\n        type,\n        description,\n        status,\n        progress,\n        previewUrl,\n        kind: resolvedKind,\n        variant: resolvedVariant,\n        extension,\n        onRemove,\n        removeLabel,\n      }}\n    >\n      <li\n        role={role}\n        aria-label={ariaLabel ?? name}\n        data-control-ui=\"chat-composer-attachment\"\n        data-slot=\"root\"\n        data-surface=\"panel\"\n        data-kind={resolvedKind}\n        data-state={status}\n        data-variant={resolvedVariant}\n        className={cn(\n          \"relative shrink-0 overflow-hidden rounded-field border border-border/80 bg-card/92 text-left shadow-sm ring-1 ring-foreground/4 transition-[background-color,border-color,box-shadow] duration-[var(--duration-base)] ease-[var(--ease-standard)]\",\n          \"data-[state=uploading]:border-primary/35 data-[state=uploading]:ring-primary/15 data-[state=error]:border-destructive/45 data-[state=error]:ring-destructive/20\",\n          resolvedVariant === \"preview\"\n            ? \"flex h-14 w-fit max-w-40 items-center p-1\"\n            : \"flex h-14 w-64 max-w-[calc(100vw-5rem)] items-center gap-2 p-1.5 pr-8\",\n          skinSlot(\"chat-composer-attachment\", \"root\", { kind: resolvedKind, status, variant: resolvedVariant }),\n          className,\n        )}\n        style={attachmentStyle}\n        {...props}\n      >\n        {children ?? (\n          <>\n            <ChatComposerAttachmentPreview />\n            {resolvedVariant === \"file\" && (\n              <ChatComposerAttachmentContent>\n                <ChatComposerAttachmentTitle />\n                <ChatComposerAttachmentDescription />\n              </ChatComposerAttachmentContent>\n            )}\n            <ChatComposerAttachmentRemove />\n            <ChatComposerAttachmentProgress />\n          </>\n        )}\n      </li>\n    </ChatComposerAttachmentContext.Provider>\n  );\n}\n\nexport type ChatComposerAttachmentPreviewProps = ComponentProps<\"div\">;\n\nexport function ChatComposerAttachmentPreview({ className, children, ...props }: ChatComposerAttachmentPreviewProps) {\n  const context = useChatComposerAttachmentContext();\n  const label = fallbackLabel(context.kind, context.extension);\n\n  return (\n    <div\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"preview\"\n      className={cn(\n        \"relative grid shrink-0 place-items-center overflow-hidden rounded-[calc(var(--radius-field)-0.25rem)] bg-muted text-muted-foreground\",\n        context.variant === \"preview\" ? \"h-full w-fit min-w-10 max-w-full\" : \"size-10\",\n        context.kind === \"pdf\" && \"bg-destructive/10 text-destructive-text\",\n        context.kind === \"spreadsheet\" && \"bg-primary/10 text-primary-text\",\n        skinSlot(\"chat-composer-attachment\", \"preview\", { kind: context.kind, status: context.status, variant: context.variant }),\n        className,\n      )}\n      {...props}\n    >\n      {children ??\n        (context.previewUrl ? (\n          <img\n            src={context.previewUrl}\n            alt={context.name}\n            loading=\"lazy\"\n            decoding=\"async\"\n            className=\"h-full w-auto max-w-full object-contain\"\n          />\n        ) : (\n          <span className=\"text-micro font-semibold uppercase\">{label}</span>\n        ))}\n      {context.status === \"uploading\" && (\n        <span\n          aria-hidden=\"true\"\n          data-control-ui=\"chat-composer-attachment\"\n          data-slot=\"status\"\n          className={cn(\n            \"absolute right-1 top-1 size-4 rounded-full border-2 border-background/75 border-t-primary animate-spin\",\n            skinSlot(\"chat-composer-attachment\", \"status\", { status: context.status }),\n          )}\n        />\n      )}\n    </div>\n  );\n}\n\nexport type ChatComposerAttachmentContentProps = ComponentProps<\"div\">;\n\nexport function ChatComposerAttachmentContent({ className, ...props }: ChatComposerAttachmentContentProps) {\n  return (\n    <div\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"content\"\n      className={cn(\"min-w-0 flex-1\", skinSlot(\"chat-composer-attachment\", \"content\", {}), className)}\n      {...props}\n    />\n  );\n}\n\nexport type ChatComposerAttachmentTitleProps = ComponentProps<\"div\">;\n\nexport function ChatComposerAttachmentTitle({ className, children, ...props }: ChatComposerAttachmentTitleProps) {\n  const { name } = useChatComposerAttachmentContext();\n\n  return (\n    <div\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"title\"\n      className={cn(\"truncate text-label font-medium text-foreground\", skinSlot(\"chat-composer-attachment\", \"title\", {}), className)}\n      {...props}\n    >\n      {children ?? name}\n    </div>\n  );\n}\n\nexport type ChatComposerAttachmentDescriptionProps = ComponentProps<\"div\">;\n\nexport function ChatComposerAttachmentDescription({ className, children, ...props }: ChatComposerAttachmentDescriptionProps) {\n  const context = useChatComposerAttachmentContext();\n\n  return (\n    <div\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"description\"\n      className={cn(\n        \"truncate text-caption text-muted-foreground data-[state=error]:text-destructive-text\",\n        skinSlot(\"chat-composer-attachment\", \"description\", { status: context.status }),\n        className,\n      )}\n      data-state={context.status}\n      {...props}\n    >\n      {children ?? defaultDescription(context)}\n    </div>\n  );\n}\n\nexport type ChatComposerAttachmentRemoveProps = ComponentProps<typeof Button>;\n\nexport function ChatComposerAttachmentRemove({\n  className,\n  onClick,\n  children,\n  \"aria-label\": ariaLabel,\n  ...props\n}: ChatComposerAttachmentRemoveProps) {\n  const { name, onRemove, removeLabel } = useChatComposerAttachmentContext();\n\n  if (!onRemove) return null;\n\n  function handleClick(event: MouseEvent<HTMLButtonElement>) {\n    onClick?.(event);\n    if (!event.defaultPrevented) onRemove?.();\n  }\n\n  return (\n    <Button\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"remove\"\n      aria-label={ariaLabel ?? removeLabel ?? `Remove ${name}`}\n      size=\"xs\"\n      variant=\"solid\"\n      className={cn(\n        \"absolute right-1 top-1 z-10 size-5 rounded-full bg-foreground p-0 text-background shadow-sm hover:bg-foreground/85\",\n        skinSlot(\"chat-composer-attachment\", \"remove\", {}),\n        className,\n      )}\n      onClick={handleClick}\n      {...props}\n    >\n      {children ?? <XIcon aria-hidden=\"true\" className=\"size-3\" />}\n    </Button>\n  );\n}\n\nexport type ChatComposerAttachmentProgressProps = ComponentProps<\"div\">;\n\nexport function ChatComposerAttachmentProgress({ className, children, ...props }: ChatComposerAttachmentProgressProps) {\n  const { status, progress } = useChatComposerAttachmentContext();\n\n  if (status !== \"uploading\" && progress == null) return null;\n\n  return (\n    <div\n      aria-hidden=\"true\"\n      data-control-ui=\"chat-composer-attachment\"\n      data-slot=\"progress\"\n      className={cn(\"absolute inset-x-0 bottom-0 h-1 bg-foreground/10\", skinSlot(\"chat-composer-attachment\", \"progress\", {}), className)}\n      {...props}\n    >\n      {children ?? (\n        <div\n          data-control-ui=\"chat-composer-attachment\"\n          data-slot=\"progress-indicator\"\n          className={cn(\n            \"h-full w-[var(--chat-composer-attachment-progress)] rounded-full bg-primary transition-[width] duration-[var(--duration-slow)] ease-[var(--ease-emphasized)]\",\n            skinSlot(\"chat-composer-attachment\", \"progress-indicator\", {}),\n          )}\n        />\n      )}\n    </div>\n  );\n}\n"
    }
  ],
  "meta": {}
}
