{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "query-history",
  "title": "QueryHistory",
  "description": "Searchable recent SQL with status, timing, saved queries, expandable statements, copy, and rerun actions.",
  "dependencies": [
    "@hugeicons/react",
    "@hugeicons/core-free-icons",
    "@codemirror/lang-sql",
    "@lezer/highlight"
  ],
  "registryDependencies": [
    "utils",
    "https://ui.neon.com/r/skeleton.json",
    "https://ui.neon.com/r/empty-state.json",
    "https://ui.neon.com/r/tooltip.json",
    "https://ui.neon.com/r/neon-tokens.json"
  ],
  "files": [
    {
      "path": "src/components/query-history/query-history.tsx",
      "content": "\"use client\";\n\nimport { PostgreSQL } from \"@codemirror/lang-sql\";\nimport {\n  Alert02Icon,\n  ArrowDown01Icon,\n  Bookmark01Icon,\n  Cancel01Icon,\n  CheckmarkCircle02Icon,\n  Copy01Icon,\n  Loading03Icon,\n  PlayIcon,\n  Search01Icon,\n  Tick02Icon,\n} from \"@hugeicons/core-free-icons\";\nimport { HugeiconsIcon } from \"@hugeicons/react\";\nimport { highlightTree, tagHighlighter, tags } from \"@lezer/highlight\";\nimport { useMemo, useState } from \"react\";\nimport type { ComponentProps, ReactNode } from \"react\";\n\nimport { EmptyState } from \"@/components/empty-state/empty-state\";\nimport { Skeleton } from \"@/components/ui/skeleton\";\nimport {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger,\n} from \"@/components/ui/tooltip\";\nimport { cn } from \"@/lib/utils\";\n\nexport type QueryHistoryStatus = \"success\" | \"error\" | \"cancelled\";\nexport type QueryHistoryFilter = \"all\" | \"saved\" | \"failed\";\n\nexport interface QueryHistoryEntry {\n  id: string;\n  query: string;\n  status: QueryHistoryStatus;\n  /** Display-ready time, e.g. \"2m ago\" or \"Yesterday, 4:18 PM\". */\n  timestamp: string;\n  /** Machine-readable timestamp for the time element. */\n  executedAt?: string;\n  durationMs?: number;\n  rowCount?: number;\n  command?: string;\n  database?: string;\n  branch?: string;\n  error?: string;\n  saved?: boolean;\n}\n\nexport type QueryHistoryProps = Omit<ComponentProps<\"div\">, \"children\"> & {\n  entries: QueryHistoryEntry[];\n  /** Controlled text filter. */\n  query?: string;\n  /** Initial text filter for uncontrolled usage. */\n  defaultQuery?: string;\n  onQueryChange?: (query: string) => void;\n  /** Controlled history filter. */\n  filter?: QueryHistoryFilter;\n  /** Initial history filter for uncontrolled usage. */\n  defaultFilter?: QueryHistoryFilter;\n  onFilterChange?: (filter: QueryHistoryFilter) => void;\n  /** Controlled expanded row id. Pass null to close all rows. */\n  expandedId?: string | null;\n  /** Initial expanded row id for uncontrolled usage. */\n  defaultExpandedId?: string | null;\n  onExpandedIdChange?: (id: string | null) => void;\n  onRerun?: (entry: QueryHistoryEntry) => void | Promise<void>;\n  /** Controlled id for a query currently rerunning. */\n  runningId?: string | null;\n  onSavedChange?: (entry: QueryHistoryEntry, saved: boolean) => void;\n  onCopy?: (entry: QueryHistoryEntry) => void;\n  isLoading?: boolean;\n  error?: Error | string | null;\n  empty?: ReactNode;\n  label?: string;\n};\n\nconst FILTERS: { label: string; value: QueryHistoryFilter }[] = [\n  { label: \"All\", value: \"all\" },\n  { label: \"Saved\", value: \"saved\" },\n  { label: \"Failed\", value: \"failed\" },\n];\n\nconst STATUS: Record<\n  QueryHistoryStatus,\n  { icon: typeof CheckmarkCircle02Icon; label: string; className: string }\n> = {\n  cancelled: {\n    className: \"text-muted-foreground\",\n    icon: Cancel01Icon,\n    label: \"Cancelled\",\n  },\n  error: {\n    className: \"text-destructive\",\n    icon: Alert02Icon,\n    label: \"Failed\",\n  },\n  success: {\n    className: \"text-primary\",\n    icon: CheckmarkCircle02Icon,\n    label: \"Succeeded\",\n  },\n};\n\nconst COPY_FEEDBACK_MS = 1600;\n\n/* ─────────────────────────────────────────────────────────\n * DISCLOSURE STORYBOARD\n *\n *   0ms   detail begins 4px above its resting position\n * 180ms   height, position, and opacity settle together\n * ───────────────────────────────────────────────────────── */\nconst DISCLOSURE_MOTION = {\n  durationMs: 180,\n  offsetPx: 4,\n};\n\nconst SQL_HIGHLIGHTER = tagHighlighter([\n  { class: \"font-semibold text-primary\", tag: tags.keyword },\n  { class: \"text-[var(--status-scaling)]\", tag: tags.string },\n  { class: \"italic text-muted-foreground\", tag: tags.comment },\n  { class: \"text-foreground\", tag: [tags.name, tags.variableName] },\n  {\n    class: \"text-[var(--status-sleeping)]\",\n    tag: [tags.number, tags.bool, tags.null],\n  },\n  { class: \"text-muted-foreground\", tag: tags.punctuation },\n  { class: \"text-destructive\", tag: tags.invalid },\n]);\n\nconst highlightSQL = (query: string) => {\n  const content: ReactNode[] = [];\n  let position = 0;\n  highlightTree(\n    PostgreSQL.language.parser.parse(query),\n    SQL_HIGHLIGHTER,\n    (from, to, classes) => {\n      if (from > position) {\n        content.push(query.slice(position, from));\n      }\n      content.push(\n        <span className={classes} key={`${from}-${to}`}>\n          {query.slice(from, to)}\n        </span>\n      );\n      position = to;\n    }\n  );\n  if (position < query.length) {\n    content.push(query.slice(position));\n  }\n  return content;\n};\n\nconst useControllableValue = <Value,>({\n  prop,\n  defaultProp,\n  onChange,\n}: {\n  prop: Value | undefined;\n  defaultProp: Value;\n  onChange?: (value: Value) => void;\n}) => {\n  const [internalValue, setInternalValue] = useState(defaultProp);\n  const value = prop === undefined ? internalValue : prop;\n  const setValue = (nextValue: Value) => {\n    if (prop === undefined) {\n      setInternalValue(nextValue);\n    }\n    onChange?.(nextValue);\n  };\n  return [value, setValue] as const;\n};\n\nconst useCopyQuery = (onCopy?: (entry: QueryHistoryEntry) => void) => {\n  const [copiedId, setCopiedId] = useState<string | null>(null);\n  const copy = async (entry: QueryHistoryEntry) => {\n    try {\n      await navigator.clipboard.writeText(entry.query);\n      setCopiedId(entry.id);\n      window.setTimeout(() => setCopiedId(null), COPY_FEEDBACK_MS);\n    } catch {\n      setCopiedId(null);\n    }\n    onCopy?.(entry);\n  };\n  return { copiedId, copy };\n};\n\nconst formatDuration = (durationMs?: number) => {\n  if (durationMs === undefined) {\n    return null;\n  }\n  if (durationMs < 1) {\n    return \"<1 ms\";\n  }\n  if (durationMs < 1000) {\n    return `${Math.round(durationMs)} ms`;\n  }\n  return `${(durationMs / 1000).toFixed(2)} s`;\n};\n\nconst formatRowCount = (rowCount?: number) => {\n  if (rowCount === undefined) {\n    return null;\n  }\n  return `${rowCount.toLocaleString()} ${rowCount === 1 ? \"row\" : \"rows\"}`;\n};\n\nconst summarizeQuery = (query: string) => query.replaceAll(/\\s+/gu, \" \").trim();\n\nconst IconButton = ({\n  label,\n  pressed,\n  className,\n  ...props\n}: ComponentProps<\"button\"> & { label: string; pressed?: boolean }) => (\n  <Tooltip>\n    <TooltipTrigger\n      render={\n        <button\n          aria-label={label}\n          aria-pressed={pressed}\n          className={cn(\n            \"inline-flex size-7 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none transition-colors\",\n            \"[&_svg]:size-3.5\",\n            \"hover:bg-muted/60 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\",\n            \"disabled:pointer-events-none disabled:opacity-50\",\n            pressed &&\n              \"bg-primary/10 text-primary hover:bg-primary/15 hover:text-primary\",\n            className\n          )}\n          type=\"button\"\n          {...props}\n        />\n      }\n    />\n    <TooltipContent>{label}</TooltipContent>\n  </Tooltip>\n);\n\nconst FilterControl = ({\n  value,\n  onChange,\n}: {\n  value: QueryHistoryFilter;\n  onChange: (filter: QueryHistoryFilter) => void;\n}) => (\n  <fieldset className=\"flex h-8 shrink-0 items-center rounded-md border border-border/60 bg-background p-0.5\">\n    <legend className=\"sr-only\">Filter query history</legend>\n    {FILTERS.map((filter) => (\n      <button\n        aria-pressed={value === filter.value}\n        className={cn(\n          \"h-6 rounded-[4px] px-2 font-medium text-[11px] outline-none transition-colors\",\n          \"hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\",\n          value === filter.value\n            ? \"bg-muted text-foreground shadow-xs\"\n            : \"text-muted-foreground\"\n        )}\n        key={filter.value}\n        onClick={() => onChange(filter.value)}\n        type=\"button\"\n      >\n        {filter.label}\n      </button>\n    ))}\n  </fieldset>\n);\n\nconst QueryMetadata = ({\n  entry,\n  statusLabel,\n}: {\n  entry: QueryHistoryEntry;\n  statusLabel: string;\n}) => {\n  const duration = formatDuration(entry.durationMs);\n  const rowCount = formatRowCount(entry.rowCount);\n  const isSlow = entry.durationMs !== undefined && entry.durationMs >= 1000;\n\n  return (\n    <span className=\"mt-0.5 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 pl-[22px] text-[10px] text-muted-foreground tabular-nums\">\n      <span className=\"sr-only\">{statusLabel}.</span>\n      <span className=\"inline-flex items-center gap-2\">\n        {entry.command ? (\n          <span className=\"font-medium text-foreground/80\">\n            {entry.command}\n          </span>\n        ) : null}\n        {rowCount ? <span>{rowCount}</span> : null}\n        {duration ? (\n          <span\n            className={\n              isSlow ? \"text-[var(--status-scaling)]\" : \"text-muted-foreground\"\n            }\n          >\n            {duration}\n          </span>\n        ) : null}\n      </span>\n      {entry.database || entry.branch ? (\n        <span className=\"inline-flex min-w-0 max-w-56 items-center gap-1 rounded-sm bg-muted/50 px-1.5 py-0.5 font-mono text-muted-foreground/80\">\n          {entry.database ? (\n            <span className=\"truncate\">{entry.database}</span>\n          ) : null}\n          {entry.database && entry.branch ? (\n            <span aria-hidden=\"true\" className=\"text-border\">\n              /\n            </span>\n          ) : null}\n          {entry.branch ? (\n            <span className=\"truncate\">{entry.branch}</span>\n          ) : null}\n        </span>\n      ) : null}\n      <time className=\"text-muted-foreground/65\" dateTime={entry.executedAt}>\n        {entry.timestamp}\n      </time>\n    </span>\n  );\n};\n\nconst QueryRow = ({\n  entry,\n  expanded,\n  copied,\n  running,\n  onCopy,\n  onExpandedChange,\n  onRerun,\n  onSavedChange,\n}: {\n  entry: QueryHistoryEntry;\n  expanded: boolean;\n  copied: boolean;\n  running: boolean;\n  onCopy: () => void;\n  onExpandedChange: () => void;\n  onRerun?: () => void | Promise<void>;\n  onSavedChange?: () => void;\n}) => {\n  const status = STATUS[entry.status];\n  const summary = summarizeQuery(entry.query);\n  const detailsId = `query-history-${entry.id}-details`;\n\n  return (\n    <li\n      aria-busy={running || undefined}\n      className=\"border-border/50 border-b last:border-b-0\"\n      data-expanded={expanded}\n      data-running={running}\n      data-slot=\"query-history-item\"\n      data-status={entry.status}\n    >\n      <div className=\"grid min-w-0 grid-cols-[minmax(0,1fr)_auto] items-center gap-2 px-3 py-2.5\">\n        <button\n          aria-controls={detailsId}\n          aria-expanded={expanded}\n          className=\"group/query min-w-0 rounded-sm text-left outline-none focus-visible:ring-2 focus-visible:ring-ring/50\"\n          onClick={onExpandedChange}\n          type=\"button\"\n        >\n          <span className=\"flex min-w-0 items-center gap-2\">\n            <HugeiconsIcon\n              aria-hidden=\"true\"\n              className={cn(\"size-3.5 shrink-0\", status.className)}\n              icon={status.icon}\n              strokeWidth={2}\n            />\n            <span\n              className=\"min-w-0 truncate font-mono text-foreground text-xs leading-5\"\n              title={summary}\n            >\n              {summary}\n            </span>\n            <HugeiconsIcon\n              aria-hidden=\"true\"\n              className={cn(\n                \"size-3 shrink-0 text-muted-foreground/60 transition-transform duration-100 motion-reduce:transition-none\",\n                expanded && \"rotate-180\"\n              )}\n              icon={ArrowDown01Icon}\n              strokeWidth={2}\n            />\n          </span>\n          <QueryMetadata entry={entry} statusLabel={status.label} />\n        </button>\n\n        <div\n          className=\"flex items-center gap-0.5\"\n          data-slot=\"query-history-actions\"\n        >\n          {onSavedChange ? (\n            <IconButton\n              label={entry.saved ? \"Remove from saved queries\" : \"Save query\"}\n              onClick={onSavedChange}\n              pressed={entry.saved}\n            >\n              <HugeiconsIcon icon={Bookmark01Icon} strokeWidth={2} />\n            </IconButton>\n          ) : null}\n          <IconButton label={copied ? \"Copied\" : \"Copy query\"} onClick={onCopy}>\n            <HugeiconsIcon\n              className={copied ? \"text-primary\" : undefined}\n              icon={copied ? Tick02Icon : Copy01Icon}\n              strokeWidth={2}\n            />\n          </IconButton>\n          {onRerun ? (\n            <IconButton\n              disabled={running}\n              label={running ? \"Running query\" : \"Run query again\"}\n              onClick={onRerun}\n            >\n              <HugeiconsIcon\n                className={cn(\n                  running &&\n                    \"animate-spin text-primary motion-reduce:animate-none\"\n                )}\n                icon={running ? Loading03Icon : PlayIcon}\n                strokeWidth={2}\n              />\n            </IconButton>\n          ) : null}\n        </div>\n      </div>\n\n      <div\n        aria-hidden={!expanded}\n        className={cn(\n          \"mx-3 grid transition-[grid-template-rows,opacity,transform] ease-out motion-reduce:transform-none motion-reduce:transition-none\",\n          expanded\n            ? \"grid-rows-[1fr] opacity-100\"\n            : \"pointer-events-none grid-rows-[0fr] opacity-0\"\n        )}\n        data-slot=\"query-history-disclosure\"\n        id={detailsId}\n        style={{\n          transform: expanded\n            ? \"translateY(0)\"\n            : `translateY(-${DISCLOSURE_MOTION.offsetPx}px)`,\n          transitionDuration: `${DISCLOSURE_MOTION.durationMs}ms`,\n        }}\n      >\n        <div className=\"min-h-0 overflow-hidden\">\n          <div\n            className=\"mb-3 overflow-hidden rounded-md border border-border/60 bg-background\"\n            data-slot=\"query-history-details\"\n          >\n            <pre className=\"neon-scroll-fade !m-0 max-h-48 overflow-auto whitespace-pre !rounded-none !border-0 !bg-transparent p-3 font-mono text-[11px] text-foreground leading-relaxed !shadow-none\">\n              <code>{highlightSQL(entry.query)}</code>\n            </pre>\n            {entry.error ? (\n              <div className=\"flex items-start gap-2 border-destructive/30 border-t bg-destructive/5 px-3 py-2 text-[11px] text-destructive\">\n                <HugeiconsIcon\n                  aria-hidden=\"true\"\n                  className=\"mt-0.5 size-3.5 shrink-0\"\n                  icon={Alert02Icon}\n                  strokeWidth={2}\n                />\n                <p className=\"min-w-0 break-words\">{entry.error}</p>\n              </div>\n            ) : null}\n          </div>\n        </div>\n      </div>\n    </li>\n  );\n};\n\nconst QueryHistorySkeleton = ({\n  label,\n  className,\n  ...props\n}: ComponentProps<\"div\"> & { label: string }) => (\n  <div\n    aria-busy=\"true\"\n    className={cn(\n      \"w-full min-w-0 overflow-hidden rounded-lg border border-border/60 bg-card shadow-xs\",\n      className\n    )}\n    data-slot=\"query-history\"\n    data-state=\"loading\"\n    {...props}\n  >\n    <div className=\"flex items-center justify-between gap-3 border-border/50 border-b px-3 py-3\">\n      <span className=\"font-medium text-sm\">{label}</span>\n      <Skeleton className=\"h-4 w-8\" />\n    </div>\n    <div className=\"space-y-px p-3\">\n      <Skeleton className=\"h-12 w-full\" />\n      <Skeleton className=\"h-12 w-full\" />\n      <Skeleton className=\"h-12 w-full\" />\n    </div>\n  </div>\n);\n\nconst getRootState = (error: Error | string | null, count: number) => {\n  if (error) {\n    return \"error\";\n  }\n  return count > 0 ? \"ready\" : \"empty\";\n};\n\nconst QueryHistoryBody = ({\n  entries,\n  visibleEntries,\n  error,\n  empty,\n  copiedId,\n  expandedId,\n  runningId,\n  copy,\n  setExpandedId,\n  onRerun,\n  onSavedChange,\n}: {\n  entries: QueryHistoryEntry[];\n  visibleEntries: QueryHistoryEntry[];\n  error: Error | string | null;\n  empty?: ReactNode;\n  copiedId: string | null;\n  expandedId: string | null;\n  runningId: string | null;\n  copy: (entry: QueryHistoryEntry) => Promise<void>;\n  setExpandedId: (id: string | null) => void;\n  onRerun?: (entry: QueryHistoryEntry) => void | Promise<void>;\n  onSavedChange?: (entry: QueryHistoryEntry, saved: boolean) => void;\n}) => {\n  if (error) {\n    return (\n      <div className=\"flex items-start gap-2 p-4 text-sm\" role=\"alert\">\n        <HugeiconsIcon\n          aria-hidden=\"true\"\n          className=\"mt-0.5 size-4 shrink-0 text-destructive\"\n          icon={Alert02Icon}\n          strokeWidth={2}\n        />\n        <div className=\"min-w-0\">\n          <p className=\"font-medium text-foreground\">History unavailable</p>\n          <p className=\"mt-0.5 text-muted-foreground text-xs\">\n            {typeof error === \"string\" ? error : error.message}\n          </p>\n        </div>\n      </div>\n    );\n  }\n\n  if (visibleEntries.length === 0) {\n    const hasEntries = entries.length > 0;\n    return (\n      <div className=\"p-3\">\n        {empty ?? (\n          <EmptyState\n            description={\n              hasEntries\n                ? \"Try another search or clear the current filter.\"\n                : \"Queries appear here after they run.\"\n            }\n            title={hasEntries ? \"No matches\" : \"No queries yet\"}\n          />\n        )}\n      </div>\n    );\n  }\n\n  return (\n    <ul aria-label=\"Recorded queries\" className=\"min-w-0\">\n      {visibleEntries.map((entry) => (\n        <QueryRow\n          copied={copiedId === entry.id}\n          entry={entry}\n          expanded={expandedId === entry.id}\n          key={entry.id}\n          onCopy={() => copy(entry)}\n          onExpandedChange={() =>\n            setExpandedId(expandedId === entry.id ? null : entry.id)\n          }\n          onRerun={onRerun ? () => onRerun(entry) : undefined}\n          onSavedChange={\n            onSavedChange ? () => onSavedChange(entry, !entry.saved) : undefined\n          }\n          running={runningId === entry.id}\n        />\n      ))}\n    </ul>\n  );\n};\n\nexport const QueryHistory = ({\n  entries,\n  query: queryProp,\n  defaultQuery = \"\",\n  onQueryChange,\n  filter: filterProp,\n  defaultFilter = \"all\",\n  onFilterChange,\n  expandedId: expandedIdProp,\n  defaultExpandedId = null,\n  onExpandedIdChange,\n  onRerun,\n  runningId: runningIdProp,\n  onSavedChange,\n  onCopy,\n  isLoading = false,\n  error = null,\n  empty,\n  label = \"Query history\",\n  className,\n  ...props\n}: QueryHistoryProps) => {\n  const [query, setQuery] = useControllableValue({\n    defaultProp: defaultQuery,\n    onChange: onQueryChange,\n    prop: queryProp,\n  });\n  const [filter, setFilter] = useControllableValue<QueryHistoryFilter>({\n    defaultProp: defaultFilter,\n    onChange: onFilterChange,\n    prop: filterProp,\n  });\n  const [expandedId, setExpandedId] = useControllableValue<string | null>({\n    defaultProp: defaultExpandedId,\n    onChange: onExpandedIdChange,\n    prop: expandedIdProp,\n  });\n  const { copiedId, copy } = useCopyQuery(onCopy);\n  const [internalRunningId, setInternalRunningId] = useState<string | null>(\n    null\n  );\n  const runningId =\n    runningIdProp === undefined ? internalRunningId : runningIdProp;\n\n  const rerun = async (entry: QueryHistoryEntry) => {\n    if (!onRerun) {\n      return;\n    }\n    setInternalRunningId(entry.id);\n    try {\n      await onRerun(entry);\n    } finally {\n      setInternalRunningId(null);\n    }\n  };\n\n  const visibleEntries = useMemo(() => {\n    const normalizedQuery = query.trim().toLocaleLowerCase();\n    return entries.filter((entry) => {\n      const matchesFilter =\n        filter === \"all\" ||\n        (filter === \"saved\" && entry.saved) ||\n        (filter === \"failed\" && entry.status === \"error\");\n      if (!matchesFilter) {\n        return false;\n      }\n      if (!normalizedQuery) {\n        return true;\n      }\n      return [entry.query, entry.database, entry.branch, entry.command]\n        .filter(Boolean)\n        .some((value) => value?.toLocaleLowerCase().includes(normalizedQuery));\n    });\n  }, [entries, filter, query]);\n\n  if (isLoading) {\n    return (\n      <QueryHistorySkeleton className={className} label={label} {...props} />\n    );\n  }\n\n  return (\n    <div\n      className={cn(\n        \"@container w-full min-w-0 overflow-hidden rounded-lg border border-border/60 bg-card shadow-xs\",\n        className\n      )}\n      data-filter={filter}\n      data-slot=\"query-history\"\n      data-state={getRootState(error, visibleEntries.length)}\n      {...props}\n    >\n      <div className=\"flex flex-wrap items-center gap-2 border-border/50 border-b px-3 py-3\">\n        <div className=\"mr-auto min-w-0\">\n          <p className=\"font-medium text-foreground text-sm\">{label}</p>\n          <p className=\"text-[10px] text-muted-foreground tabular-nums\">\n            {entries.length.toLocaleString()} recorded\n          </p>\n        </div>\n        <FilterControl onChange={setFilter} value={filter} />\n      </div>\n\n      <div className=\"border-border/50 border-b p-2.5\">\n        <label className=\"flex h-8 min-w-0 items-center gap-2 rounded-md border border-border/60 bg-background px-2.5 text-muted-foreground focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/30\">\n          <HugeiconsIcon\n            aria-hidden=\"true\"\n            className=\"size-3.5 shrink-0\"\n            icon={Search01Icon}\n            strokeWidth={2}\n          />\n          <span className=\"sr-only\">Search query history</span>\n          <input\n            className=\"min-w-0 flex-1 bg-transparent text-foreground text-xs outline-none placeholder:text-muted-foreground/70 [@media(pointer:coarse)]:text-base\"\n            onChange={(event) => setQuery(event.target.value)}\n            placeholder=\"Search SQL, branch, or database\"\n            type=\"search\"\n            value={query}\n          />\n          {query ? (\n            <button\n              className=\"shrink-0 rounded-sm px-1 text-[10px] text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/50\"\n              onClick={() => setQuery(\"\")}\n              type=\"button\"\n            >\n              Clear\n            </button>\n          ) : null}\n        </label>\n      </div>\n\n      <TooltipProvider>\n        <QueryHistoryBody\n          copiedId={copiedId}\n          copy={copy}\n          empty={empty}\n          entries={entries}\n          error={error}\n          expandedId={expandedId}\n          onRerun={onRerun ? rerun : undefined}\n          onSavedChange={onSavedChange}\n          runningId={runningId}\n          setExpandedId={setExpandedId}\n          visibleEntries={visibleEntries}\n        />\n      </TooltipProvider>\n\n      <p aria-live=\"polite\" className=\"sr-only\">\n        {visibleEntries.length}{\" \"}\n        {visibleEntries.length === 1 ? \"query\" : \"queries\"}\n        {query || filter !== \"all\" ? \" shown\" : \" recorded\"}.\n      </p>\n    </div>\n  );\n};\n",
      "type": "registry:component"
    },
    {
      "path": "src/components/query-history/use-query-history.ts",
      "content": "\"use client\";\n\nimport { useCallback, useEffect, useState } from \"react\";\n\nimport type { QueryHistoryEntry } from \"@/components/query-history/query-history\";\n\nexport interface UseQueryHistoryOptions {\n  endpoint?: string;\n  initialEntries?: QueryHistoryEntry[];\n}\n\nexport const useQueryHistory = ({\n  endpoint = \"/api/query-history\",\n  initialEntries = [],\n}: UseQueryHistoryOptions = {}) => {\n  const [entries, setEntries] = useState<QueryHistoryEntry[]>(initialEntries);\n  const [isLoading, setIsLoading] = useState(initialEntries.length === 0);\n  const [loadError, setLoadError] = useState<Error | null>(null);\n\n  const refresh = useCallback(\n    async (signal?: AbortSignal) => {\n      setIsLoading(true);\n      setLoadError(null);\n\n      try {\n        const response = await fetch(endpoint, { signal });\n        if (!response.ok) {\n          throw new Error(`Could not load query history (${response.status}).`);\n        }\n        const payload = (await response.json()) as {\n          entries: QueryHistoryEntry[];\n        };\n        setEntries(payload.entries);\n      } catch (error) {\n        if (error instanceof DOMException && error.name === \"AbortError\") {\n          return;\n        }\n        setLoadError(\n          error instanceof Error\n            ? error\n            : new Error(\"Could not load query history.\")\n        );\n      } finally {\n        setIsLoading(false);\n      }\n    },\n    [endpoint]\n  );\n\n  useEffect(() => {\n    const controller = new AbortController();\n    let active = true;\n    queueMicrotask(async () => {\n      if (active) {\n        await refresh(controller.signal);\n      }\n    });\n    return () => {\n      active = false;\n      controller.abort();\n    };\n  }, [refresh]);\n\n  return {\n    entries,\n    error: loadError,\n    isLoading,\n    refresh,\n    setEntries,\n  };\n};\n",
      "type": "registry:hook"
    }
  ],
  "type": "registry:component"
}