Overview
MCP Server
Custom Shadcn File Type for React and Tailwind CSS. A shadcn file type icon in brand and family colors, outline, solid, light and mono variants, 6 sizes and resolution from a file name, path, URL or MIME type.
import { FileType, getFileType } from "@/components/reui/file-type"Browse 15 production-ready Shadcn File Type components for dashboards, forms, and product UI. These examples use Base UI primitives from @base-ui/react and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.
Browse all 15 Shadcn File Type components for copy-ready layouts, dashboards, and forms built with Tailwind CSS in the ReUI library.
<FileType type="pdf" />
<FileType type={getFileType("Q3 report.xlsx")} variant="solid" />Hand your agent this brief or the ReUI MCP, not a screenshot. Free to install.
When to use
When not to use
Agent notes
type takes an extension, not a file name: report.pdf passed straight in draws a neutral page labeled REPORT.PDF, so run names through getFileTypeFileType is not focusable and has no keyboard behavior, so an action goes on a wrapping Button or link with its own accessible namehttps://reui.io/docs/components/base/file-type.mdget_component("file-type")npx shadcn@latest add @reui/file-typeEvery built-in format by family, an attachment list, an upload queue, custom labels, custom colors and unknown types, an export format picker, custom icons, custom badges and dotfile names are on the File Type page.
FileType renders a single span with role="img". Inside it, an inline SVG
draws the page, its folded corner and the family glyph, and an HTML band in
front of the page carries the label (in solid, the label sits on the page
with no band). There are no sub-components and no Base
UI or Radix part, so both bases install the same file, and the only
dependencies are class-variance-authority and cn. It has no state, no
hooks and no "use client" directive, so it renders in a Server Component,
and the helpers run on the server or the client.
type takes 1 of 171 built-in extensions, 1 of 13 family names, or any other
string. It ignores case, surrounding spaces and leading dots, so "PDF",
".pdf" and "pdf" render the same icon.
dockerfile DOCK, makefile MAKE, graphql GQL, parquet
PARQ, safetensors SAFE and numbers NUM."empty", or no type, shows a blank page."dwg", shows a neutral page (text-zinc-500)
labelled with the string in upper case. Its family is file, which has no
glyph.The family color paints a family type's glyph, and an extension without a color of its own.
A format takes its brand's color (Word blue, Excel green, PowerPoint red, the
GitHub language colors for code), else its family's color, and a format with
neither, such as an archive, plain text, data, config, an installer or a font, is neutral (text-zinc-500). The mono variant sets every type in the theme
gray instead. Every brand and family color is the 600 step, so
the set reads as one palette and the white label text holds 3:1 or better on
all of them.
Yellow (png, svg, JavaScript and mdx) is the exception: white text on it stays
under 3:1 until the 700 step, so it is text-yellow-500 with dark label text,
as the JavaScript logo is.
Config, build and infrastructure files (env, toml, dockerfile, tf)
and data formats such as jsonl are code, as json and yaml are. Model
weights (safetensors, gguf, onnx, pt) are packed binaries, so they
take the archive glyph, and parquet, sqlite and db take the spreadsheet
grid.
Both lists are exported: FILE_TYPE_EXTENSION_NAMES holds the 171 extensions
and FILE_TYPE_CATEGORIES the 13 families. FileTypeName is the union of
both, and type accepts any string beside it, so an editor still
autocompletes the built-in names.
All span props are supported, except children: the component takes none.
--file-type-foreground is white by default (the page color in mono), and the
grey outline is --file-type-border. In dark mode the outline and light glyph mixes 30% white
into the type color so a 1px line stays visible on a dark page, while the
badge keeps the shade its white text needs. A neutral type fills its solid
page 55% lighter (--file-type-fill) and draws its glyph and label in dark
gray (--file-type-glyph), so it does not read as a dark slab next to the
colored ones.
mono ignores the type color and draws every type in the theme's
muted-foreground, for a list where color would compete with the content. A
text-* class in className still recolors it.
<FileType type="xlsx" variant="solid" />Every size draws the same design: the page and the label band with its text.
The root is an inline-size container and everything inside is measured in
cqw against its width, so the band, its text, padding and corners and the
line all scale with the box.
The band has the same geometry on every type: 45% from the top, 5% from the
left, 35% of the box high and at most 95% of it wide, with 7% padding on each
side, 10.5% in Maia, Luma and Rhea so the pill's round ends clear the text. Its
text is 24% of the width, so its capitals fill half the band, and shrinks only
when the characters would run past the band: at 40px WOFF2 renders at 9px and
SKETCH at 7.5px (8.2px and 6.9px with the pill's padding). The capitals are centered on their own
height rather than the font's line, and the band moves by up to half a pixel
so that browsers, which draw text on whole pixels, keep them centered. The
line is 1px up to 40px, then 2.5% of the width.
In solid there is no band: the text sits on the page, two thirds of the box
wide, and every label up to 4 characters is 20.5% of the width (8.2px at
40px), so a row of mixed types reads even. A longer label shrinks to fit the
page: at 40px WOFF2 renders at 6.6px and SKETCH at 5.5px. Up to 20px each
label fits the page on its own, semibold: PDF at 4.4px and DOCX at 3.3px
at 18px.
Up to 20px the icon is tuned to stay legible in a table row: the band is 45% of the box from 40% down, its text is semibold at a third of the box (5.8px at 18px), the outline is darker, and a glyph is drawn at full size instead of 75%.
For a size off the scale, put a plain Tailwind size class in className. It
replaces the preset box, and the label and the line scale with it.
<FileType type="pdf" className="size-20" />
<FileType type="pdf" className="size-[72px]" />
<div className="size-24">
<FileType type="pdf" className="size-full" />
</div>fileTypeVariants({ size }) returns the root's classes for a size, for a
wrapper that needs to match them.
The label band is HTML text, the extension in upper case by default (the
short form for the 6 long keys, such as DOCK for dockerfile). label
replaces it with any node, such as a version tag ("V3") or a document code
("CA-1042"). A string longer than 4 characters shrinks to fit the band, so
keep it short. A label that is not a string is sized as 4 characters and
truncated if it runs past the band.
<FileType type="docx" label="V3" />
<FileType type="empty" variant="light" label="CA-1042" />
<FileType type="pdf" label={null} />
<FileType type="pdf" glyph />label joins the accessible name: type="docx" label="V3" reads
"V3, DOCX file", and type="empty" label="CA-1042" reads "CA-1042, File". A
label that only repeats the extension is not read twice, and a label that is
not a string leaves the name as it is.label={null} or label="" hides the band. The family glyph shows in its
place when the type has one; an unknown type or "empty" shows a blank page.glyph shows the family glyph instead of the label at any size. A family
type, or a type with no label text, shows its glyph without it.There are 12 glyphs, one per family except empty: document, spreadsheet,
presentation, image, video, audio, code, archive, design, executable, font
and folder. They share one 10-unit box under the fold on the 24-unit page
grid, drawn at 75% of it, and draw with the page's line, so every family sits
at the same weight. The document glyph is 3 uneven, left-aligned lines.
icon draws any icon on the page in place of the glyph and the label, in the
glyph's color: the type color in outline and light, the foreground in
solid, the theme gray in mono. It is sized like a glyph and decorative, so the type still names the
icon; pass an aria-label when the icon adds meaning ("Locked PDF file").
<FileType type="pdf" icon={<LockIcon />} aria-label="Locked PDF file" />Resolves a file name, path, URL, bare extension, family name or MIME type to
a type, always in lower case. It returns FileTypeName | (string & {}).
A URL's query and hash are dropped first. Outside a URL a ? or # can be
part of the name, so "invoice #12.pdf" is a PDF, and they start a query
only when the value does not already end in an extension or a slash
("/uploads/scan.png?v=3").
The exact MIME map covers the types whose subtype does not name the extension:
PDF, Word, Excel (macro-enabled included), PowerPoint, OpenDocument, RTF and
Keynote; ZIP, RAR, 7-Zip, tar, gzip and bzip2, with their x- forms
(application/x-gzip, application/x-rar-compressed, application/x-bzip2);
JSON, XML, YAML, RSS, JavaScript, PHP and shell scripts; Windows, Android and
Debian packages, Apple disk images and ISO images; TTF and WOFF fonts;
Photoshop, PostScript, JPEG, SVG and icons; QuickTime, AVI, Matroska and WMV;
MP3, WAV, M4A, FLAC and AIFF; and plain text, Markdown, Python, C, CSV, TSV,
HTML and CSS. It also covers Node's CommonJS type, Jupyter notebooks, NDJSON,
SQLite, HLS playlists, SubRip subtitles and email (message/rfc822); Java
archives, RPM packages, PEM files and X.509 certificates, Zstandard and XZ;
and C++, Kotlin, Swift, R, Lua, Scala, diffs, patches and makefiles.
Well-known names resolve before the extension. .env and every .env.* file
(.env.local, .env.example) are env; Dockerfile, Containerfile and
Dockerfile.* are dockerfile; Makefile and *.mk are makefile;
.gitignore, .gitattributes and the other Git files are git, and any other
.*ignore file is ignore; bun.lockb and go.sum are lock, and go.mod
is go; Gemfile, Podfile and the other Ruby build files are rb; shell
profiles (.bashrc, .zshrc, .envrc) take their shell; .editorconfig and
.npmrc are ini; and README, LICENSE, CHANGELOG, CODEOWNERS and
version pins such as .nvmrc are txt. A name pattern holds only while the
file ends in no known extension of its own, so Dockerfile.md is Markdown.
.cer reads as crt, and .sqlite3 as sqlite.
import { FileType, getFileType } from "@/components/reui/file-type"
function AttachmentRow({ file }: { file: File }) {
return (
<div className="flex items-center gap-2">
<FileType type={getFileType(file.name)} size="mini" aria-hidden="true" />
<span className="truncate">{file.name}</span>
</div>
)
}A browser File also carries a MIME type in file.type, which can be empty
or a generic application/octet-stream, so the name is usually the better
input. A folder name is not a file name: "v2.1 exports" would resolve to a
"1 exports" extension, so pass "folder" for folders yourself.
Returns the family of a type, for grouping or filtering a file list: 1 of
the 13 families, "file" for an unknown type, or "empty" for none. The
return type, FileTypeCategory, is those 14 values. It takes a type, not a
file name, so resolve the name with getFileType first.
getFileTypeCategory("xlsx") // "spreadsheet"
getFileTypeCategory(getFileType("Q3 report.pdf")) // "document"
getFileTypeCategory("dwg") // "file"
getFileTypeCategory() // "empty"The type color is the root's text color, and the label band, the glyph and
the solid fill all follow it, so a single text-* class in className
replaces the type's color. For white label text at 4.5:1, every Tailwind 700
shade works. At 600, orange, amber, yellow, lime, green, emerald, teal, cyan
and sky fall short, and most 500 shades are too light. For a light color, set
--file-type-foreground to a dark one instead.
<FileType type="xlsx" className="text-green-700" />
<FileType type="pdf" className="text-lime-400 [--file-type-foreground:black]" />
<FileType
type="sketch"
variant="solid"
className="text-amber-400 [--file-type-foreground:black]"
/>The variables below are read from the root, and a class in className
replaces any of them.
The label band's corners follow the installed style in the ReUI Badge's
proportions: 10cqw (4px at 40px) in Vega, Nova and Mira, a pill in Maia,
Luma and Rhea, and square in Lyra and Sera. --file-type-radius sets them for
every style. In Lyra and Sera the page, its folded corner and the solid flap are
square too, so the whole icon follows the style.
labelClassName is merged last onto the badge, so any of its styles can be
replaced: background, radius, padding, font size and weight, or position. The
text is a child of the badge, so recolor it with *:text-*.
<FileType type="pdf" labelClassName="rounded-full" />
<FileType type="docx" label="DRAFT" labelClassName="bg-amber-400 *:text-amber-950" />
<FileType type="xlsx" labelClassName="bg-foreground *:text-background" /><FileType type="pdf" className="[--file-type-radius:9999px]" />
<FileType
type="pdf"
className="[--file-type-border:var(--border)] [--file-type-stroke:1.5px]"
/>label joins the name ("V3, DOCX file"). Pass aria-label to name
the icon yourself; it replaces the generated one.aria-hidden="true" when the text next to the icon already names the
type, such as a file name in a list row or a family caption, so it is not
read twice.outline and
light, 135 of the 171 extensions meet that, and every one has 3:1 or
better. The 600 shades of orange, amber, green, emerald, teal, cyan and sky
fall between 3:1 and 4.5:1 with white text: pdf, html, htm, java,
rs, rss, svelte, astro, ipynb, jar and ai (3.6:1), go and
gradle (3.6:1), vue and dart (3.7:1), pages and sketch (3.2:1),
the green spreadsheets, the shells, cu, makefile, apk and aab
(3.2:1), and psd, sqlite, prisma and r (4.0:1). solid puts the
same white text on the type color, so the same ratios hold, except the 54 neutral
types: their dark gray label on the lighter page is 4.1:1. Yellow types
take dark text, 7.6:1. The ratios are for Tailwind's default palette. In mono the label is the page color on
muted-foreground: 4.7:1 in light mode and 7.6:1 in dark with the default
neutral theme.text-* class (text-green-700 on
xlsx reaches 4.9:1). See Customization.png or svg glyph sits
under 3:1 on a white page. In dark mode the outline and light glyph is
lifted toward white to stay visible on a dark page.mini and xs the label text is 5.8px, so keep the file name or
the extension in text next to the icon.import {
FileType,
type FileTypeName,
} from "@/components/reui/file-type"
const formats = [
"pdf",
"docx",
"xlsx",
"pptx",
"jpg",
"mp4",
"mp3",
"zip",
"fig",
"html",
] as const satisfies readonly FileTypeName[]
export function Pattern() {
// No captions: each icon names itself ("PDF file", "MP4 file"), so a screen
// reader still hears every format.
return (
<div className="mx-auto grid w-fit grid-cols-5 gap-x-4 gap-y-6">
{formats.map((type) => (
<FileType key={type} type={type} />
))}
</div>
)
}
"use client"
import { useMemo, useState } from "react"
import {
DataGrid,
DataGridContainer,
dataGridFeatures,
type DataGridFeatures,
} from "@/components/reui/data-grid/data-grid"
import { DataGridColumnHeader } from "@/components/reui/data-grid/data-grid-column-header"
import { DataGridScrollArea } from "@/components/reui/data-grid/data-grid-scroll-area"
import { DataGridTable } from "@/components/reui/data-grid/data-grid-table"
import {
FileType,
getFileType,
getFileTypeCategory,
type FileTypeCategory,
} from "@/components/reui/file-type"
import { useTable } from "@tanstack/react-table"
import type { ColumnDef, SortingState } from "@tanstack/react-table"
interface FileItem {
id: string
name: string
kind: "file" | "folder"
/** Bytes. Files only: a folder has an item count instead. */
size?: number
items?: number
/** ISO date. */
modified: string
owner: string
}
const files: FileItem[] = [
{
id: "1",
name: "Brand assets",
kind: "folder",
items: 24,
modified: "2026-10-02",
owner: "Ava Patel",
},
{
id: "2",
name: "Legal",
kind: "folder",
items: 6,
modified: "2026-09-21",
owner: "Marco Silva",
},
{
id: "3",
name: "Q3 financial report.pdf",
kind: "file",
size: 2516582,
modified: "2026-10-05",
owner: "Marco Silva",
},
{
id: "4",
name: "Customer interviews - synthesis and next steps.docx",
kind: "file",
size: 190464,
modified: "2026-10-06",
owner: "You",
},
{
id: "5",
name: "Revenue forecast 2027.xlsx",
kind: "file",
size: 933888,
modified: "2026-10-01",
owner: "Lena Fischer",
},
{
id: "6",
name: "Waitlist export.csv",
kind: "file",
size: 5033165,
modified: "2026-09-29",
owner: "You",
},
{
id: "7",
name: "Launch keynote.pptx",
kind: "file",
size: 19503513,
modified: "2026-10-04",
owner: "Ava Patel",
},
{
id: "8",
name: "Homepage hero.png",
kind: "file",
size: 1363149,
modified: "2026-09-30",
owner: "Ava Patel",
},
{
id: "9",
name: "Team offsite.jpg",
kind: "file",
size: 3250586,
modified: "2026-09-12",
owner: "Lena Fischer",
},
{
id: "10",
name: "Product walkthrough.mp4",
kind: "file",
size: 149631795,
modified: "2026-10-03",
owner: "Noah Kim",
},
{
id: "11",
name: "Podcast episode 12.mp3",
kind: "file",
size: 40055603,
modified: "2026-09-24",
owner: "Noah Kim",
},
{
id: "12",
name: "Press kit.zip",
kind: "file",
size: 59139686,
modified: "2026-10-02",
owner: "Ava Patel",
},
{
id: "13",
name: "Design system.fig",
kind: "file",
size: 24012390,
modified: "2026-10-06",
owner: "Ava Patel",
},
{
id: "14",
name: "app.config.json",
kind: "file",
size: 4301,
modified: "2026-10-06",
owner: "Noah Kim",
},
{
id: "15",
name: "Office floor plan.dwg",
kind: "file",
size: 7864320,
modified: "2026-08-27",
owner: "Lena Fischer",
},
]
const FAMILY_LABELS: Record<Exclude<FileTypeCategory, "file">, string> = {
archive: "Archive",
audio: "Audio",
code: "Code",
design: "Design",
document: "Document",
empty: "File",
executable: "Application",
folder: "Folder",
font: "Font",
image: "Image",
presentation: "Presentation",
spreadsheet: "Spreadsheet",
video: "Video",
}
// A folder name is not a file name: "v2.1 exports" would resolve to a
// "1 exports" extension, so folders never go through getFileType.
function fileTypeOf(file: FileItem) {
return file.kind === "folder" ? "folder" : getFileType(file.name)
}
function kindOf(file: FileItem) {
const type = fileTypeOf(file)
const category = getFileTypeCategory(type)
// An unknown extension has no family to name, so it names the kind itself.
return category === "file"
? `${type.toUpperCase()} file`
: FAMILY_LABELS[category]
}
const UNITS = ["B", "KB", "MB", "GB"]
const sizeNumber = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 })
function formatBytes(bytes: number) {
let value = bytes
let unit = 0
while (value >= 1024 && unit < UNITS.length - 1) {
value /= 1024
unit += 1
}
return `${sizeNumber.format(value)} ${UNITS[unit]}`
}
// A date-only ISO string parses as UTC midnight, so it is formatted in UTC:
// a local zone west of it would show the previous day, and differ from the
// server render.
const dateFormat = new Intl.DateTimeFormat("en-US", {
month: "short",
day: "numeric",
year: "numeric",
timeZone: "UTC",
})
// A fixed locale keeps the order identical on the server and the client, and
// numeric puts "episode 2" before "episode 12".
const collator = new Intl.Collator("en", { numeric: true, sensitivity: "base" })
// Folders group ahead of files, as in a file manager. A descending sort
// reverses the whole order, so they then move to the end.
function compareByName(a: FileItem, b: FileItem) {
if (a.kind !== b.kind) return a.kind === "folder" ? -1 : 1
return collator.compare(a.name, b.name)
}
export function Pattern() {
const [sorting, setSorting] = useState<SortingState>([
{ id: "name", desc: false },
])
const columns = useMemo<ColumnDef<DataGridFeatures, FileItem>[]>(
() => [
{
accessorKey: "name",
id: "name",
header: ({ column }) => (
<DataGridColumnHeader title="Name" column={column} />
),
cell: ({ row }) => {
const file = row.original
// The name already reads its extension, so a file's icon is
// hidden. A folder name says nothing about what the row is, so
// the folder icon keeps its "Folder" name.
return (
<div className="flex min-w-0 items-center gap-2">
<FileType
type={fileTypeOf(file)}
size="mini"
aria-hidden={file.kind === "folder" ? undefined : true}
/>
<span className="truncate font-medium" title={file.name}>
{file.name}
</span>
</div>
)
},
sortFn: (a, b) => compareByName(a.original, b.original),
size: 300,
},
{
id: "type",
accessorFn: kindOf,
header: ({ column }) => (
<DataGridColumnHeader title="Type" column={column} />
),
size: 130,
},
{
accessorKey: "size",
header: ({ column }) => (
<DataGridColumnHeader title="Size" column={column} />
),
cell: ({ row }) => {
const { size, items } = row.original
return size === undefined ? (
<span className="text-muted-foreground">{items} items</span>
) : (
formatBytes(size)
)
},
// Folders have no byte size, so they sit last in both directions.
sortFn: "basic",
sortUndefined: "last",
size: 100,
// The header is a padded button in a flex row: justify-end moves it
// to the end, and -me-2 mirrors the header's own -ms-2, so the sort
// arrow lines up with the figures below.
meta: {
headerClassName: "text-end [&>div]:-me-2 [&>div]:justify-end",
cellClassName: "text-end tabular-nums",
},
},
{
accessorKey: "modified",
header: ({ column }) => (
<DataGridColumnHeader title="Modified" column={column} />
),
cell: ({ row }) => dateFormat.format(new Date(row.original.modified)),
// ISO dates sort chronologically as plain text.
sortFn: "text",
size: 130,
},
{
accessorKey: "owner",
header: ({ column }) => (
<DataGridColumnHeader title="Owner" column={column} />
),
size: 140,
},
],
[]
)
const table = useTable({
features: dataGridFeatures,
// The shared bundle registers a pagination row model; manualPagination
// says the data is already the page, so every file renders.
manualPagination: true,
columns,
data: files,
getRowId: (row: FileItem) => row.id,
state: { sorting },
onSortingChange: setSorting,
})
return (
<DataGrid table={table} recordCount={files.length}>
<DataGridContainer>
<DataGridScrollArea>
<DataGridTable />
</DataGridScrollArea>
</DataGridContainer>
</DataGrid>
)
}
"use client"
import { useId, useRef, useState } from "react"
import {
FileType,
getFileType,
getFileTypeCategory,
type FileTypeVariant,
} from "@/components/reui/file-type"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardFooter } from "@/components/ui/card"
import { Field, FieldLabel } from "@/components/ui/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/components/ui/input-group"
import { XIcon } from 'lucide-react'
// One sample per kind of input getFileType reads: a file name, a path in
// another case, a MIME type, a folder, a well-known name and a URL with a
// query. The long URL goes last, so the wrap never strands a short chip on a
// row of its own.
const SAMPLES = [
"Q3 report.pdf",
"photos/IMG_2041.HEIC",
"video/quicktime",
"assets/",
".env.local",
"https://example.com/data.csv?v=2",
]
const VARIANTS: { variant: FileTypeVariant; label: string }[] = [
{ variant: "outline", label: "Outline" },
{ variant: "solid", label: "Solid" },
{ variant: "light", label: "Light" },
{ variant: "mono", label: "Mono" },
]
// Only the two fallbacks need words: a value with nothing to resolve draws a
// blank page, and an unknown extension draws a neutral page with its label.
function describe(value: string, type: string) {
if (!value.trim()) return "Nothing to resolve yet, so it shows a blank page."
if (type === "empty") {
return "No extension or known MIME type, so it shows a blank page."
}
if (getFileTypeCategory(type) === "file") {
return "Unknown extension, so it shows a neutral page with its label."
}
return null
}
export function Pattern() {
const id = useId()
const inputRef = useRef<HTMLInputElement>(null)
// Opens on a sample so the card shows a resolved type; Clear reaches the
// blank state.
const [value, setValue] = useState("Q3 report.pdf")
// getFileType is a pure string parse, so the icons, the readout and the
// hint all derive from the raw value on each render, with no effect.
const type = getFileType(value)
const family = getFileTypeCategory(type)
const hint = describe(value, type)
return (
<div className="flex w-full max-w-md flex-col gap-6">
<div className="flex flex-col gap-3">
<Field>
<FieldLabel htmlFor={`${id}-value`}>
File name, path, URL or MIME type
</FieldLabel>
<InputGroup>
<InputGroupAddon>
{/* The field already shows the name and the status below
announces the result, so this icon is decorative here. */}
<FileType type={type} size="mini" aria-hidden="true" />
</InputGroupAddon>
<InputGroupInput
ref={inputRef}
id={`${id}-value`}
value={value}
onChange={(event) => setValue(event.target.value)}
placeholder="invoice.docx or image/png"
autoComplete="off"
spellCheck={false}
/>
{value ? (
<InputGroupAddon align="inline-end">
<InputGroupButton
size="icon-xs"
aria-label="Clear"
onClick={() => {
// The button unmounts once the field is empty, so focus
// goes back to the field instead of falling to the body.
setValue("")
inputRef.current?.focus()
}}
>
<XIcon aria-hidden="true" />
</InputGroupButton>
</InputGroupAddon>
) : null}
</InputGroup>
</Field>
<div
role="group"
aria-labelledby={`${id}-samples`}
className="flex flex-wrap items-center gap-1.5"
>
<span
id={`${id}-samples`}
className="text-muted-foreground me-1 text-xs"
>
Try
</span>
{SAMPLES.map((sample) => (
<Button
key={sample}
variant="outline"
size="xs"
className="max-w-full"
onClick={() => setValue(sample)}
>
{/* The span, not the button, truncates: a long URL then ends
in an ellipsis on a narrow card instead of overflowing. */}
<span className="truncate">{sample}</span>
</Button>
))}
</div>
</div>
<Card>
<CardContent>
<div className="grid grid-cols-4 justify-items-center gap-4">
{VARIANTS.map(({ variant, label }) => (
<div key={variant} className="flex flex-col items-center gap-2">
<FileType type={type} variant={variant} size="lg" />
<span className="text-muted-foreground text-xs">{label}</span>
</div>
))}
</div>
</CardContent>
<CardFooter>
{/* A polite live region holding text only: the icons sit outside
it, so each change is read once as "Type pdf, Family document". */}
<div role="status" className="flex min-w-0 flex-1 flex-col gap-1">
<dl className="flex min-w-0 flex-wrap gap-x-6 gap-y-1">
<div className="flex min-w-0 gap-2">
<dt className="text-muted-foreground">Type</dt>
<dd className="truncate font-mono">{type}</dd>
</div>
<div className="flex gap-2">
<dt className="text-muted-foreground">Family</dt>
<dd className="font-mono">{family}</dd>
</div>
</dl>
{hint ? (
<p className="text-muted-foreground text-xs">{hint}</p>
) : null}
</div>
</CardFooter>
</Card>
</div>
)
}