{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "attachment",
  "title": "Attachment",
  "description": "File and image cards for uploads, with progress, states, actions, a full-card trigger and names that keep their extension.",
  "dependencies": [
    "@base-ui/react@^1.8.0",
    "@tabler/icons-react",
    "class-variance-authority",
    "cn"
  ],
  "registryDependencies": [
    "https://hextaui.com/r/theme.json",
    "https://hextaui.com/r/aspect-ratio.json",
    "https://hextaui.com/r/button.json",
    "https://hextaui.com/r/motion.json",
    "https://hextaui.com/r/progress.json",
    "https://hextaui.com/r/scroll-area.json"
  ],
  "files": [
    {
      "path": "components/ui/attachment.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport { mergeProps } from \"@base-ui/react/merge-props\"\nimport { useRender } from \"@base-ui/react/use-render\"\nimport { IconFile } from \"@tabler/icons-react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport { cn } from \"cn\"\n\nimport { duration, easeOut, easeSpring } from \"@/lib/motion\"\n\nimport { AspectRatio } from \"@/components/ui/aspect-ratio\"\nimport { Button } from \"@/components/ui/button\"\nimport { Progress } from \"@/components/ui/progress\"\nimport { ScrollArea, type ScrollAreaProps } from \"@/components/ui/scroll-area\"\n\ntype AttachmentState = \"idle\" | \"uploading\" | \"processing\" | \"error\" | \"done\"\n\ntype AttachmentOrientation = \"horizontal\" | \"vertical\"\n\nconst AttachmentContext = React.createContext<{\n  state: AttachmentState\n  titleId: string\n  orientation: AttachmentOrientation\n}>({ state: \"done\", titleId: \"\", orientation: \"horizontal\" })\n\nconst layoutDuration = duration.morph\n\nfunction subscribe() {\n  return () => {}\n}\n\nfunction prefersReducedMotion() {\n  return (\n    typeof window.matchMedia !== \"function\" ||\n    window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches\n  )\n}\n\nconst attachmentVariants = cva(\n  \"group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-(--attachment-radius) border bg-card text-card-foreground transition-[background-color,border-color,box-shadow,scale] duration-150 ease-out-quint [--attachment-inset:1px] [--attachment-radius:var(--radius-xl)] has-[>[data-slot=attachment-trigger]]:hover:bg-muted/50 has-[>[data-slot=attachment-trigger]:focus-visible]:border-ring has-[>[data-slot=attachment-trigger]:focus-visible]:ring-3 has-[>[data-slot=attachment-trigger]:focus-visible]:ring-focus-ring data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed motion-safe:has-[>[data-slot=attachment-trigger]:active]:scale-[0.99]\",\n  {\n    variants: {\n      size: {\n        default:\n          \"gap-2 text-sm has-data-[slot=attachment-content]:px-2.5 has-data-[slot=attachment-content]:py-2 has-data-[slot=attachment-media]:p-2 has-data-[slot=attachment-media]:[--attachment-inset:calc(var(--spacing)*2+1px)]\",\n        sm: \"gap-2.5 text-xs has-data-[slot=attachment-content]:px-2 has-data-[slot=attachment-content]:py-1.5 has-data-[slot=attachment-media]:p-1.5 has-data-[slot=attachment-media]:[--attachment-inset:calc(var(--spacing)*1.5+1px)]\",\n        xs: \"gap-1.5 text-xs [--attachment-radius:var(--radius-lg)] has-data-[slot=attachment-content]:px-1.5 has-data-[slot=attachment-content]:py-1 has-data-[slot=attachment-media]:p-1 has-data-[slot=attachment-media]:[--attachment-inset:calc(var(--spacing)+1px)]\",\n      },\n      orientation: {\n        horizontal: \"min-w-40 items-center\",\n        vertical: \"w-24 flex-col has-data-[slot=attachment-content]:w-30\",\n      },\n    },\n    defaultVariants: {\n      size: \"default\",\n      orientation: \"horizontal\",\n    },\n  }\n)\n\ntype AttachmentProps = React.ComponentProps<\"div\"> &\n  VariantProps<typeof attachmentVariants> & {\n    state?: AttachmentState\n    progress?: number\n  }\n\nfunction clampProgress(progress: number) {\n  return Number.isFinite(progress) ? Math.min(100, Math.max(0, progress)) : 0\n}\n\nfunction Attachment({\n  className,\n  state = \"done\",\n  size = \"default\",\n  orientation = \"horizontal\",\n  progress,\n  children,\n  ref,\n  ...props\n}: AttachmentProps) {\n  const titleId = React.useId()\n  const resolvedOrientation = orientation ?? \"horizontal\"\n  const isHydrating = React.useSyncExternalStore(\n    subscribe,\n    () => false,\n    () => true\n  )\n  const [popIn] = React.useState(!isHydrating)\n  const context = React.useMemo(\n    () => ({ state, titleId, orientation: resolvedOrientation }),\n    [state, titleId, resolvedOrientation]\n  )\n  const value =\n    state === \"uploading\" && progress !== undefined\n      ? clampProgress(progress)\n      : null\n\n  return (\n    <AttachmentContext.Provider value={context}>\n      <div\n        ref={ref}\n        data-slot=\"attachment\"\n        data-state={state}\n        data-size={size}\n        data-orientation={orientation}\n        className={cn(\n          attachmentVariants({ size, orientation }),\n          popIn &&\n            \"motion-safe:animate-in motion-safe:ease-out-quint motion-safe:animation-duration-200 motion-safe:fade-in-0 motion-safe:zoom-in-95\",\n          className\n        )}\n        {...props}\n      >\n        {children}\n        {value !== null ? (\n          <span className=\"pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]\">\n            <Progress\n              value={value}\n              size=\"xs\"\n              aria-labelledby={titleId}\n              data-slot=\"attachment-progress\"\n              className=\"absolute inset-x-0 bottom-0 [&>[data-slot=progress-track]]:bg-transparent\"\n            />\n          </span>\n        ) : null}\n      </div>\n    </AttachmentContext.Provider>\n  )\n}\n\nconst attachmentMediaVariants = cva(\n  \"relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--attachment-radius)-var(--attachment-inset)))] bg-muted text-foreground [--attachment-media-radius:max(calc(var(--radius-sm)*0.5),calc(var(--attachment-radius)-var(--attachment-inset)))] group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5\",\n  {\n    variants: {\n      variant: {\n        icon: \"\",\n        image:\n          \"opacity-60 transition-opacity duration-300 ease-out-quint group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100\",\n      },\n    },\n    defaultVariants: {\n      variant: \"icon\",\n    },\n  }\n)\n\nfunction AttachmentMedia({\n  className,\n  variant = \"icon\",\n  children,\n  ...props\n}: React.ComponentProps<\"div\"> & VariantProps<typeof attachmentMediaVariants>) {\n  if (variant === \"image\") {\n    return (\n      <AspectRatio\n        ratio={1}\n        fallback={<IconFile />}\n        data-slot=\"attachment-media\"\n        data-variant=\"image\"\n        className={cn(attachmentMediaVariants({ variant }), className)}\n        {...props}\n      >\n        {children}\n      </AspectRatio>\n    )\n  }\n\n  return (\n    <div\n      data-slot=\"attachment-media\"\n      data-variant={variant}\n      className={cn(attachmentMediaVariants({ variant }), className)}\n      {...props}\n    >\n      {children}\n    </div>\n  )\n}\n\nfunction AttachmentContent({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  return (\n    <div\n      data-slot=\"attachment-content\"\n      className={cn(\n        \"max-w-full min-w-0 flex-1 leading-tight group-data-[orientation=vertical]/attachment:px-1\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction splitFileName(name: string) {\n  const dot = name.lastIndexOf(\".\")\n  if (dot <= 0 || dot === name.length - 1 || name.length - dot > 8) {\n    return null\n  }\n  return { base: name.slice(0, dot), extension: name.slice(dot) }\n}\n\nfunction AttachmentTitle({\n  className,\n  children,\n  id,\n  title,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  const { titleId } = React.useContext(AttachmentContext)\n  const parts = typeof children === \"string\" ? splitFileName(children) : null\n\n  return (\n    <span\n      data-slot=\"attachment-title\"\n      id={id ?? (titleId || undefined)}\n      title={title ?? (typeof children === \"string\" ? children : undefined)}\n      className={cn(\n        \"max-w-full min-w-0 font-medium group-data-[state=processing]/attachment:shimmer group-data-[state=uploading]/attachment:shimmer\",\n        parts ? \"flex\" : \"block truncate\",\n        className\n      )}\n      {...props}\n    >\n      {parts ? (\n        <>\n          <span className=\"min-w-0 truncate\">{parts.base}</span>\n          <span className=\"shrink-0\">{parts.extension}</span>\n        </>\n      ) : (\n        children\n      )}\n    </span>\n  )\n}\n\nfunction AttachmentDescription({\n  className,\n  ...props\n}: React.ComponentProps<\"span\">) {\n  return (\n    <span\n      data-slot=\"attachment-description\"\n      className={cn(\n        \"mt-0.5 block max-w-full min-w-0 truncate text-xs text-muted-foreground group-data-[state=error]/attachment:text-destructive\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AttachmentActions({\n  className,\n  ...props\n}: React.ComponentProps<\"div\">) {\n  const { orientation } = React.useContext(AttachmentContext)\n\n  return (\n    <div\n      data-slot=\"attachment-actions\"\n      className={cn(\n        \"relative z-20 flex shrink-0 items-center\",\n        orientation === \"vertical\" &&\n          \"absolute end-3 top-3 gap-1 transition-opacity duration-150 *:data-[slot=attachment-action]:rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--attachment-radius)-var(--attachment-inset)-4px))] *:data-[slot=attachment-action]:bg-background/80 *:data-[slot=attachment-action]:shadow-xs *:data-[slot=attachment-action]:backdrop-blur-sm *:data-[slot=attachment-action]:hover:bg-background [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-focus-within/attachment:opacity-100 [@media(hover:hover)]:group-hover/attachment:opacity-100\",\n        className\n      )}\n      {...props}\n    />\n  )\n}\n\nfunction AttachmentAction({\n  variant,\n  size = \"icon-xs\",\n  ...props\n}: React.ComponentProps<typeof Button>) {\n  return (\n    <Button\n      data-slot=\"attachment-action\"\n      variant={variant ?? \"ghost\"}\n      size={size}\n      {...props}\n    />\n  )\n}\n\nfunction AttachmentTrigger({\n  className,\n  render,\n  type,\n  ...props\n}: useRender.ComponentProps<\"button\">) {\n  return useRender({\n    defaultTagName: \"button\",\n    render,\n    props: mergeProps<\"button\">(\n      {\n        type: render ? type : (type ?? \"button\"),\n        className: cn(\n          \"absolute inset-0 z-10 rounded-[inherit] outline-none focus-visible:outline-hidden\",\n          className\n        ),\n      },\n      props,\n      { \"data-slot\": \"attachment-trigger\" } as Record<string, string>\n    ),\n  })\n}\n\ntype Box = { left: number; top: number; width: number; height: number }\n\nfunction useGroupMotion(containerRef: React.RefObject<HTMLDivElement | null>) {\n  const boxesRef = React.useRef(new Map<HTMLElement, Box>())\n\n  React.useLayoutEffect(() => {\n    const container = containerRef.current\n    if (!container) {\n      return\n    }\n    const reduce =\n      prefersReducedMotion() || typeof container.animate !== \"function\"\n    const previous = boxesRef.current\n    const next = new Map<HTMLElement, Box>()\n\n    for (const child of Array.from(container.children)) {\n      if (\n        !(child instanceof HTMLElement) ||\n        child.dataset.slot !== \"attachment\"\n      ) {\n        continue\n      }\n      const box = {\n        left: child.offsetLeft,\n        top: child.offsetTop,\n        width: child.offsetWidth,\n        height: child.offsetHeight,\n      }\n      next.set(child, box)\n      const before = previous.get(child)\n      if (!reduce && before && before.left !== box.left) {\n        child.animate(\n          [\n            { translate: `${before.left - box.left}px 0` },\n            { translate: \"0 0\" },\n          ],\n          { duration: layoutDuration, easing: easeSpring }\n        )\n      }\n    }\n\n    if (!reduce) {\n      for (const [element, box] of previous) {\n        if (next.has(element) || element.isConnected) {\n          continue\n        }\n        const ghost = element.cloneNode(true) as HTMLElement\n        ghost.removeAttribute(\"id\")\n        ghost\n          .querySelectorAll(\"[id]\")\n          .forEach((node) => node.removeAttribute(\"id\"))\n        ghost.setAttribute(\"aria-hidden\", \"true\")\n        ghost.inert = true\n        Object.assign(ghost.style, {\n          position: \"absolute\",\n          left: `${box.left}px`,\n          top: `${box.top}px`,\n          width: `${box.width}px`,\n          height: `${box.height}px`,\n          margin: \"0\",\n          pointerEvents: \"none\",\n        })\n        container.appendChild(ghost)\n        const exit = ghost.animate(\n          [\n            { opacity: 1, scale: 1 },\n            { opacity: 0, scale: 0.92 },\n          ],\n          { duration: 180, easing: easeOut, fill: \"forwards\" }\n        )\n        exit.onfinish = () => ghost.remove()\n      }\n    }\n\n    boxesRef.current = next\n  })\n}\n\nfunction AttachmentGroup({\n  className,\n  children,\n  ...props\n}: Omit<ScrollAreaProps, \"scrollbars\">) {\n  const containerRef = React.useRef<HTMLDivElement>(null)\n  useGroupMotion(containerRef)\n\n  return (\n    <ScrollArea\n      data-slot=\"attachment-group\"\n      scrollbars=\"horizontal\"\n      className={cn(\n        \"min-w-0 [&>[data-slot=scroll-area-viewport]]:snap-x [&>[data-slot=scroll-area-viewport]]:snap-mandatory [&>[data-slot=scroll-area-viewport]]:scroll-px-1\",\n        className\n      )}\n      {...props}\n    >\n      <div\n        ref={containerRef}\n        data-slot=\"attachment-group-list\"\n        className=\"relative flex w-max gap-3 p-1 *:data-[slot=attachment]:flex-none *:data-[slot=attachment]:snap-start\"\n      >\n        {children}\n      </div>\n    </ScrollArea>\n  )\n}\n\nexport {\n  Attachment,\n  AttachmentGroup,\n  AttachmentMedia,\n  AttachmentContent,\n  AttachmentTitle,\n  AttachmentDescription,\n  AttachmentActions,\n  AttachmentAction,\n  AttachmentTrigger,\n  attachmentVariants,\n}\nexport type { AttachmentProps, AttachmentState }\n",
      "type": "registry:ui"
    }
  ],
  "categories": [
    "chat"
  ],
  "type": "registry:ui"
}