{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "saved-views",
  "title": "Saved Views",
  "description": "Switch, create, rename, duplicate, and save table and list view configurations.",
  "dependencies": [
    "lucide-react"
  ],
  "registryDependencies": [
    "utils",
    "button",
    "input",
    "badge",
    "popover",
    "@drift-ui/use-controllable-state",
    "@drift-ui/shape"
  ],
  "files": [
    {
      "path": "registry/default/ui/saved-views.tsx",
      "content": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  CheckIcon,\n  ChevronDownIcon,\n  CopyIcon,\n  MoreHorizontalIcon,\n  PencilIcon,\n  PlusIcon,\n  RotateCcwIcon,\n  SaveIcon,\n  StarIcon,\n  Trash2Icon,\n} from \"lucide-react\"\n\nimport { cn } from \"@/registry/default/lib/utils\"\nimport { useControllableState } from \"@/registry/default/hooks/use-controllable-state\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport { Input } from \"@/registry/default/ui/input\"\nimport {\n  Popover,\n  PopoverContent,\n  PopoverTrigger,\n} from \"@/registry/default/ui/popover\"\nimport {\n  createSavedView,\n  deleteSavedView,\n  duplicateSavedView,\n  getActiveSavedView,\n  isViewDirty,\n  renameSavedView,\n  restoreSavedView,\n  setDefaultSavedView,\n  type SavedView,\n} from \"@/registry/default/lib/saved-views.ts\"\n\nexport type { SavedView }\n\nexport type SavedViewAction =\n  \"create\" | \"rename\" | \"duplicate\" | \"delete\" | \"setDefault\" | \"save\"\n\ntype MaybePromise = void | PromiseLike<unknown>\n\nexport interface SavedViewsProps<T = unknown> extends Omit<\n  React.ComponentProps<\"div\">,\n  \"defaultValue\" | \"onReset\" | \"onError\"\n> {\n  views?: SavedView<T>[]\n  defaultViews?: SavedView<T>[]\n  onViewsChange?: (views: SavedView<T>[]) => void\n\n  activeId?: string\n  defaultActiveId?: string\n  onActiveChange?: (activeId: string) => void\n\n  currentData?: T\n  isEqual?: (saved: T, current: T) => boolean\n\n  isDirty?: boolean\n  onSave?: (view: SavedView<T>) => MaybePromise\n  onReset?: (view: SavedView<T>) => void\n\n  onCreate?: (view: SavedView<T>) => MaybePromise\n  onRename?: (view: SavedView<T>, newName: string) => MaybePromise\n  onDuplicate?: (\n    newView: SavedView<T>,\n    sourceView: SavedView<T>\n  ) => MaybePromise\n  onDelete?: (deletedViewId: string) => MaybePromise\n  onSetDefault?: (defaultViewId: string) => MaybePromise\n  onError?: (error: unknown, action: SavedViewAction) => void\n\n  variant?: \"tabs\" | \"select\"\n  showReset?: boolean\n  allowCreate?: boolean\n  allowRename?: boolean\n  allowDuplicate?: boolean\n  allowDelete?: boolean\n  allowSetDefault?: boolean\n  placeholder?: string\n  \"aria-label\"?: string\n}\n\nfunction SavedViews<T = unknown>({\n  views: viewsProp,\n  defaultViews = [],\n  onViewsChange,\n  activeId: activeIdProp,\n  defaultActiveId,\n  onActiveChange,\n  currentData,\n  isEqual,\n  isDirty: isDirtyProp,\n  onSave,\n  onReset,\n  onCreate,\n  onRename,\n  onDuplicate,\n  onDelete,\n  onSetDefault,\n  onError,\n  variant = \"tabs\",\n  showReset = true,\n  allowCreate = true,\n  allowRename = true,\n  allowDuplicate = true,\n  allowDelete = true,\n  allowSetDefault = true,\n  placeholder = \"New view name...\",\n  \"aria-label\": ariaLabel = \"Saved views\",\n  className,\n  ...props\n}: SavedViewsProps<T>) {\n  const { value: views, setValue: setViews } = useControllableState<\n    SavedView<T>[]\n  >({\n    value: viewsProp,\n    defaultValue: defaultViews,\n    onChange: onViewsChange,\n  })\n\n  const initialActive = React.useMemo(() => {\n    if (defaultActiveId) return defaultActiveId\n    const def = views.find((v) => v.isDefault)\n    return def ? def.id : (views[0]?.id ?? \"\")\n  }, [defaultActiveId, views])\n\n  const { value: activeId, setValue: setActiveId } =\n    useControllableState<string>({\n      value: activeIdProp,\n      defaultValue: initialActive,\n      onChange: onActiveChange,\n    })\n\n  const activeView = React.useMemo(\n    () => getActiveSavedView(views, activeId),\n    [views, activeId]\n  )\n\n  const isDirty = React.useMemo(\n    () => isDirtyProp ?? isViewDirty(activeView, currentData, isEqual),\n    [isDirtyProp, activeView, currentData, isEqual]\n  )\n\n  const viewsRef = React.useRef(views)\n  const activeIdRef = React.useRef(activeId)\n  const settersRef = React.useRef({ setViews, setActiveId })\n  React.useLayoutEffect(() => {\n    viewsRef.current = views\n    activeIdRef.current = activeId\n    settersRef.current = { setViews, setActiveId }\n  })\n\n  const [pending, setPending] = React.useState<\n    ReadonlyMap<string, SavedViewAction>\n  >(() => new Map())\n  const [failure, setFailure] = React.useState<string | null>(null)\n  const isActivePending = activeView ? pending.has(activeView.id) : false\n  const isDefaultPending = [...pending.values()].includes(\"setDefault\")\n\n  function canDelete(id: string) {\n    return views.some((v) => {\n      const action = pending.get(v.id)\n      return v.id !== id && action !== \"create\" && action !== \"duplicate\"\n    })\n  }\n\n  function applyViews(next: SavedView<T>[]) {\n    viewsRef.current = next\n    settersRef.current.setViews(next)\n  }\n\n  function applyActive(id: string) {\n    activeIdRef.current = id\n    settersRef.current.setActiveId(id)\n  }\n\n  function track(\n    persist: () => MaybePromise,\n    {\n      action,\n      viewId,\n      message,\n      revert,\n    }: {\n      action: SavedViewAction\n      viewId: string\n      message: string\n      revert: () => boolean\n    }\n  ) {\n    setFailure(null)\n    let result: MaybePromise\n    try {\n      result = persist()\n    } catch (error) {\n      result = Promise.reject(error)\n    }\n    if (!result || typeof result.then !== \"function\") return\n    setPending((prev) => new Map(prev).set(viewId, action))\n    const settle = () =>\n      setPending((prev) => {\n        const next = new Map(prev)\n        next.delete(viewId)\n        return next\n      })\n    result.then(settle, (error: unknown) => {\n      settle()\n      const undone = revert()\n      setFailure(undone ? `${message} The change was undone.` : message)\n      if (onError) onError(error, action)\n      else console.error(error)\n    })\n  }\n\n  const [createOpen, setCreateOpen] = React.useState(false)\n  const [selectOpen, setSelectOpen] = React.useState(false)\n  const [newViewName, setNewViewName] = React.useState(\"\")\n  const [editingId, setEditingId] = React.useState<string | null>(null)\n  const [editingName, setEditingName] = React.useState(\"\")\n  const tabRefs = React.useRef<Map<string, HTMLButtonElement>>(new Map())\n  const prevEditingIdRef = React.useRef<string | null>(null)\n\n  React.useEffect(() => {\n    if (!editingId && prevEditingIdRef.current) {\n      tabRefs.current.get(prevEditingIdRef.current)?.focus()\n    }\n    prevEditingIdRef.current = editingId\n  }, [editingId])\n\n  function handleSelect(id: string) {\n    setActiveId(id)\n    setSelectOpen(false)\n  }\n\n  function handleCreateSubmit(e?: React.FormEvent) {\n    e?.preventDefault()\n    const name = newViewName.trim() || `View ${views.length + 1}`\n    const newView = createSavedView<T>(name, { data: currentData })\n    addView(newView, \"create\", `Couldn't create \"${newView.name}\".`, () =>\n      onCreate?.(newView)\n    )\n    setNewViewName(\"\")\n    setCreateOpen(false)\n    setSelectOpen(false)\n  }\n\n  function addView(\n    newView: SavedView<T>,\n    action: \"create\" | \"duplicate\",\n    message: string,\n    persist: () => MaybePromise\n  ) {\n    const previousActiveId = activeIdRef.current\n    applyViews([...views, newView])\n    applyActive(newView.id)\n    track(persist, {\n      action,\n      viewId: newView.id,\n      message,\n      revert: () => {\n        const current = viewsRef.current\n        const reverted = current.filter((v) => v.id !== newView.id)\n        if (reverted.length === current.length) return false\n        applyViews(reverted)\n        if (activeIdRef.current === newView.id) {\n          const fallback = getActiveSavedView(reverted, previousActiveId)\n          if (fallback) applyActive(fallback.id)\n        }\n        return true\n      },\n    })\n  }\n\n  function handleStartRename(view: SavedView<T>) {\n    if (pending.has(view.id)) return\n    setEditingId(view.id)\n    setEditingName(view.name)\n  }\n\n  function handleRenameSubmit(id: string) {\n    const trimmed = editingName.trim()\n    const target = views.find((v) => v.id === id)\n    setEditingId(null)\n    if (!trimmed || !target || trimmed === target.name) return\n    applyViews(renameSavedView(views, id, trimmed))\n    track(() => onRename?.({ ...target, name: trimmed }, trimmed), {\n      action: \"rename\",\n      viewId: id,\n      message: `Couldn't rename \"${target.name}\".`,\n      revert: () => {\n        const current = viewsRef.current\n        if (!current.some((v) => v.id === id)) return false\n        applyViews(\n          current.map((v) => (v.id === id ? { ...v, name: target.name } : v))\n        )\n        return true\n      },\n    })\n  }\n\n  function handleDuplicate(view: SavedView<T>) {\n    if (pending.has(view.id)) return\n    const duplicated = duplicateSavedView(view, views)\n    addView(duplicated, \"duplicate\", `Couldn't duplicate \"${view.name}\".`, () =>\n      onDuplicate?.(duplicated, view)\n    )\n  }\n\n  function handleDelete(id: string) {\n    if (!canDelete(id) || pending.has(id)) return\n    const index = views.findIndex((v) => v.id === id)\n    const target = views[index]\n    if (!target || target.isLocked) return\n    const wasActive = activeView?.id === id\n    const { views: nextViews, nextActiveId } = deleteSavedView(\n      views,\n      id,\n      activeView?.id\n    )\n    applyViews(nextViews)\n    if (wasActive && nextActiveId) applyActive(nextActiveId)\n    track(() => onDelete?.(id), {\n      action: \"delete\",\n      viewId: id,\n      message: `Couldn't delete \"${target.name}\".`,\n      revert: () => {\n        const current = viewsRef.current\n        const restored = restoreSavedView(current, target, index)\n        if (restored === current) return false\n        applyViews(restored)\n        if (wasActive && activeIdRef.current === nextActiveId) applyActive(id)\n        return true\n      },\n    })\n  }\n\n  function handleSetDefault(id: string) {\n    const target = views.find((v) => v.id === id)\n    if (!target || isDefaultPending) return\n    const previousDefaultId = views.find((v) => v.isDefault)?.id\n    applyViews(setDefaultSavedView(views, id))\n    track(() => onSetDefault?.(id), {\n      action: \"setDefault\",\n      viewId: id,\n      message: `Couldn't make \"${target.name}\" the default.`,\n      revert: () => {\n        const current = viewsRef.current\n        if (current.find((v) => v.isDefault)?.id !== id) return false\n        applyViews(\n          current.map((v) => ({ ...v, isDefault: v.id === previousDefaultId }))\n        )\n        return true\n      },\n    })\n  }\n\n  function handleSave() {\n    if (!activeView || isActivePending) return\n    const previousData = activeView.data\n    const toSave =\n      currentData !== undefined\n        ? { ...activeView, data: currentData }\n        : activeView\n    if (currentData !== undefined) {\n      applyViews(views.map((v) => (v.id === activeView.id ? toSave : v)))\n    }\n    track(() => onSave?.(toSave), {\n      action: \"save\",\n      viewId: activeView.id,\n      message: `Couldn't save \"${activeView.name}\".`,\n      revert: () => {\n        const current = viewsRef.current\n        if (\n          currentData === undefined ||\n          !current.some((v) => v.id === toSave.id)\n        ) {\n          return false\n        }\n        applyViews(\n          current.map((v) =>\n            v.id === toSave.id ? { ...v, data: previousData } : v\n          )\n        )\n        return true\n      },\n    })\n  }\n\n  function handleTabKeyDown(\n    e: React.KeyboardEvent<HTMLButtonElement>,\n    index: number,\n    view: SavedView<T>\n  ) {\n    if (e.key === \"F2\" && allowRename && !view.isLocked) {\n      e.preventDefault()\n      handleStartRename(view)\n      return\n    }\n\n    if (e.key === \"ArrowRight\") {\n      e.preventDefault()\n      const nextIndex = (index + 1) % views.length\n      const nextView = views[nextIndex]\n      if (nextView) {\n        setActiveId(nextView.id)\n        tabRefs.current.get(nextView.id)?.focus()\n      }\n    } else if (e.key === \"ArrowLeft\") {\n      e.preventDefault()\n      const prevIndex = (index - 1 + views.length) % views.length\n      const prevView = views[prevIndex]\n      if (prevView) {\n        setActiveId(prevView.id)\n        tabRefs.current.get(prevView.id)?.focus()\n      }\n    } else if (e.key === \"Home\") {\n      e.preventDefault()\n      const firstView = views[0]\n      if (firstView) {\n        setActiveId(firstView.id)\n        tabRefs.current.get(firstView.id)?.focus()\n      }\n    } else if (e.key === \"End\") {\n      e.preventDefault()\n      const lastView = views[views.length - 1]\n      if (lastView) {\n        setActiveId(lastView.id)\n        tabRefs.current.get(lastView.id)?.focus()\n      }\n    }\n  }\n\n  const dirtyActions = isDirty && activeView && (\n    <div\n      data-slot=\"saved-views-dirty-actions\"\n      className=\"flex items-center gap-1.5\"\n    >\n      <Badge\n        variant=\"outline\"\n        className=\"border-amber-300 text-amber-600 dark:border-amber-700 dark:text-amber-400\"\n      >\n        Unsaved\n      </Badge>\n      {showReset && (\n        <Button\n          type=\"button\"\n          variant=\"ghost\"\n          size=\"sm\"\n          className=\"h-control-sm gap-1 px-2 text-xs\"\n          onClick={() => onReset?.(activeView)}\n        >\n          <RotateCcwIcon className=\"size-3.5\" />\n          Discard\n        </Button>\n      )}\n      <Button\n        type=\"button\"\n        size=\"sm\"\n        className=\"h-control-sm gap-1 px-2.5 text-xs shadow-control aria-disabled:opacity-50 motion-safe:aria-busy:animate-pulse\"\n        aria-disabled={isActivePending || undefined}\n        aria-busy={isActivePending || undefined}\n        onClick={handleSave}\n      >\n        <SaveIcon className=\"size-3.5\" />\n        Save\n      </Button>\n    </div>\n  )\n\n  const failureMessage = failure && (\n    <div\n      data-slot=\"saved-views-error\"\n      role=\"alert\"\n      className=\"flex basis-full items-center gap-2 text-xs text-destructive\"\n    >\n      <span>{failure}</span>\n      <Button\n        type=\"button\"\n        variant=\"ghost\"\n        size=\"sm\"\n        className=\"h-6 px-1.5 text-xs\"\n        onClick={() => setFailure(null)}\n      >\n        Dismiss\n      </Button>\n    </div>\n  )\n\n  if (variant === \"select\") {\n    return (\n      <div\n        data-slot=\"saved-views\"\n        className={cn(\"flex flex-wrap items-center gap-2\", className)}\n        {...props}\n      >\n        <Popover open={selectOpen} onOpenChange={setSelectOpen}>\n          <PopoverTrigger\n            type=\"button\"\n            className=\"flex h-control items-center gap-2 rounded-control border border-input bg-background px-3 py-1 text-sm shadow-control transition-colors outline-none hover:bg-accent hover:text-accent-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50\"\n            aria-label={`${ariaLabel}: ${activeView?.name ?? \"Select view\"}`}\n          >\n            <span className=\"flex items-center gap-1.5 font-medium\">\n              {activeView?.isDefault && (\n                <StarIcon\n                  className=\"size-3.5 fill-amber-500 text-amber-500\"\n                  aria-hidden=\"true\"\n                />\n              )}\n              {activeView?.name ?? \"Select view\"}\n            </span>\n            {isDirty && (\n              <span\n                className=\"size-2 rounded-full bg-amber-500\"\n                aria-label=\"Unsaved changes\"\n              />\n            )}\n            <ChevronDownIcon className=\"size-4 opacity-50\" />\n          </PopoverTrigger>\n          <PopoverContent align=\"start\" className=\"w-56 p-1\">\n            <div role=\"listbox\" aria-label={ariaLabel} className=\"space-y-0.5\">\n              {views.map((view) => {\n                const isActive = view.id === activeView?.id\n                const isEditing = editingId === view.id\n\n                if (isEditing) {\n                  return (\n                    <form\n                      key={view.id}\n                      onSubmit={(e) => {\n                        e.preventDefault()\n                        handleRenameSubmit(view.id)\n                      }}\n                      className=\"flex items-center gap-1 p-1\"\n                    >\n                      <Input\n                        autoFocus\n                        value={editingName}\n                        onChange={(e) => setEditingName(e.target.value)}\n                        onBlur={() => handleRenameSubmit(view.id)}\n                        onKeyDown={(e) => {\n                          if (e.key === \"Escape\") {\n                            e.preventDefault()\n                            setEditingId(null)\n                          }\n                        }}\n                        className=\"h-7 text-xs\"\n                        aria-label=\"Rename view\"\n                      />\n                    </form>\n                  )\n                }\n\n                return (\n                  <div\n                    key={view.id}\n                    role=\"option\"\n                    aria-selected={isActive}\n                    aria-busy={pending.has(view.id) || undefined}\n                    data-pending={pending.has(view.id) || undefined}\n                    className={cn(\n                      \"group flex items-center justify-between rounded-sm px-2 py-1.5 text-sm outline-hidden transition-colors hover:bg-accent hover:text-accent-foreground data-pending:opacity-60 motion-safe:data-pending:animate-pulse\",\n                      isActive && \"bg-accent/60 font-medium\"\n                    )}\n                  >\n                    <button\n                      type=\"button\"\n                      className=\"flex flex-1 items-center gap-2 text-left\"\n                      onClick={() => handleSelect(view.id)}\n                    >\n                      {isActive ? (\n                        <CheckIcon\n                          className=\"size-3.5 text-primary\"\n                          aria-hidden=\"true\"\n                        />\n                      ) : (\n                        <span className=\"size-3.5\" aria-hidden=\"true\" />\n                      )}\n                      <span className=\"truncate\">{view.name}</span>\n                      {view.isDefault && (\n                        <StarIcon\n                          className=\"size-3 fill-amber-500 text-amber-500\"\n                          aria-hidden=\"true\"\n                        />\n                      )}\n                    </button>\n                    {(allowRename ||\n                      allowDuplicate ||\n                      allowSetDefault ||\n                      allowDelete) && (\n                      <ViewOptionsMenu\n                        view={view}\n                        isOnlyView={!canDelete(view.id)}\n                        isPending={pending.has(view.id)}\n                        allowRename={allowRename}\n                        allowDuplicate={allowDuplicate}\n                        allowSetDefault={allowSetDefault && !isDefaultPending}\n                        allowDelete={allowDelete}\n                        onStartRename={() => handleStartRename(view)}\n                        onDuplicate={() => handleDuplicate(view)}\n                        onSetDefault={() => handleSetDefault(view.id)}\n                        onDelete={() => handleDelete(view.id)}\n                      />\n                    )}\n                  </div>\n                )\n              })}\n            </div>\n\n            {allowCreate && (\n              <>\n                <div className=\"my-1 h-px bg-border\" />\n                <form\n                  onSubmit={handleCreateSubmit}\n                  className=\"flex items-center gap-1 p-1\"\n                >\n                  <Input\n                    size={1}\n                    value={newViewName}\n                    onChange={(e) => setNewViewName(e.target.value)}\n                    placeholder={placeholder}\n                    className=\"h-7 text-xs\"\n                  />\n                  <Button\n                    type=\"submit\"\n                    size=\"sm\"\n                    variant=\"ghost\"\n                    className=\"h-7 px-2 text-xs\"\n                    disabled={!newViewName.trim()}\n                  >\n                    Add\n                  </Button>\n                </form>\n              </>\n            )}\n          </PopoverContent>\n        </Popover>\n\n        {dirtyActions}\n        {failureMessage}\n      </div>\n    )\n  }\n\n  return (\n    <div\n      data-slot=\"saved-views\"\n      className={cn(\n        \"flex flex-wrap items-center justify-between gap-3\",\n        className\n      )}\n      {...props}\n    >\n      <div className=\"flex max-w-full items-center gap-1 overflow-x-auto rounded-lg border bg-muted/40 p-1\">\n        <div\n          role=\"tablist\"\n          aria-label={ariaLabel}\n          className=\"flex items-center gap-1\"\n        >\n          {views.map((view, index) => {\n            if (editingId === view.id) return null\n            const isActive = view.id === activeView?.id\n            return (\n              <button\n                key={view.id}\n                ref={(el) => {\n                  if (el) tabRefs.current.set(view.id, el)\n                  else tabRefs.current.delete(view.id)\n                }}\n                role=\"tab\"\n                type=\"button\"\n                aria-selected={isActive}\n                aria-busy={pending.has(view.id) || undefined}\n                data-pending={pending.has(view.id) || undefined}\n                tabIndex={isActive ? 0 : -1}\n                onClick={() => handleSelect(view.id)}\n                onDoubleClick={() => {\n                  if (allowRename && !view.isLocked) {\n                    handleStartRename(view)\n                  }\n                }}\n                onKeyDown={(e) => handleTabKeyDown(e, index, view)}\n                className={cn(\n                  \"flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium transition-colors outline-none focus-visible:ring-2 focus-visible:ring-ring data-pending:opacity-60 motion-safe:data-pending:animate-pulse\",\n                  isActive\n                    ? \"bg-background text-foreground shadow-xs\"\n                    : \"text-muted-foreground hover:bg-background/50 hover:text-foreground\"\n                )}\n              >\n                {view.isDefault && (\n                  <StarIcon\n                    className=\"size-3 fill-amber-500 text-amber-500\"\n                    aria-hidden=\"true\"\n                  />\n                )}\n                <span>{view.name}</span>\n                {isActive && isDirty && (\n                  <span\n                    className=\"size-1.5 rounded-full bg-amber-500\"\n                    aria-label=\"Unsaved changes\"\n                  />\n                )}\n              </button>\n            )\n          })}\n        </div>\n\n        {editingId && (\n          <form\n            onSubmit={(e) => {\n              e.preventDefault()\n              handleRenameSubmit(editingId)\n            }}\n            className=\"flex items-center px-1\"\n          >\n            <Input\n              autoFocus\n              value={editingName}\n              onChange={(e) => setEditingName(e.target.value)}\n              onBlur={() => handleRenameSubmit(editingId)}\n              onKeyDown={(e) => {\n                if (e.key === \"Escape\") {\n                  e.preventDefault()\n                  setEditingId(null)\n                }\n              }}\n              className=\"h-7 w-28 px-2 py-0 text-xs md:text-xs\"\n              aria-label=\"Rename view\"\n            />\n          </form>\n        )}\n\n        {(allowRename || allowDuplicate || allowSetDefault || allowDelete) &&\n          activeView &&\n          !editingId && (\n            <ViewOptionsMenu\n              view={activeView}\n              isOnlyView={!canDelete(activeView.id)}\n              isPending={isActivePending}\n              allowRename={allowRename}\n              allowDuplicate={allowDuplicate}\n              allowSetDefault={allowSetDefault && !isDefaultPending}\n              allowDelete={allowDelete}\n              onStartRename={() => handleStartRename(activeView)}\n              onDuplicate={() => handleDuplicate(activeView)}\n              onSetDefault={() => handleSetDefault(activeView.id)}\n              onDelete={() => handleDelete(activeView.id)}\n            />\n          )}\n\n        {allowCreate && !editingId && (\n          <Popover open={createOpen} onOpenChange={setCreateOpen}>\n            <PopoverTrigger\n              type=\"button\"\n              className=\"flex size-7 items-center justify-center rounded-md border border-dashed border-muted-foreground/30 text-muted-foreground transition-colors outline-none hover:border-foreground hover:bg-background hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring\"\n              aria-label=\"Create new view\"\n            >\n              <PlusIcon className=\"size-3.5\" />\n            </PopoverTrigger>\n            <PopoverContent align=\"start\" className=\"w-64 p-3\">\n              <form onSubmit={handleCreateSubmit} className=\"space-y-3\">\n                <div className=\"space-y-1\">\n                  <h4 className=\"text-sm font-semibold\">Create new view</h4>\n                  <p className=\"text-xs text-muted-foreground\">\n                    Save the current filters and configuration as a named view.\n                  </p>\n                </div>\n                <Input\n                  autoFocus\n                  value={newViewName}\n                  onChange={(e) => setNewViewName(e.target.value)}\n                  placeholder={placeholder}\n                  className=\"h-control-sm py-0 text-xs\"\n                />\n                <div className=\"flex justify-end gap-1.5\">\n                  <Button\n                    type=\"button\"\n                    variant=\"outline\"\n                    size=\"sm\"\n                    className=\"h-7 px-2 text-xs\"\n                    onClick={() => setCreateOpen(false)}\n                  >\n                    Cancel\n                  </Button>\n                  <Button\n                    type=\"submit\"\n                    size=\"sm\"\n                    className=\"h-7 px-2 text-xs\"\n                    disabled={!newViewName.trim()}\n                  >\n                    Create view\n                  </Button>\n                </div>\n              </form>\n            </PopoverContent>\n          </Popover>\n        )}\n      </div>\n\n      {dirtyActions}\n      {failureMessage}\n    </div>\n  )\n}\n\nfunction ViewOptionsMenu({\n  view,\n  isOnlyView,\n  isPending,\n  allowRename,\n  allowDuplicate,\n  allowSetDefault,\n  allowDelete,\n  onStartRename,\n  onDuplicate,\n  onSetDefault,\n  onDelete,\n}: {\n  view: SavedView\n  isOnlyView: boolean\n  isPending: boolean\n  allowRename: boolean\n  allowDuplicate: boolean\n  allowSetDefault: boolean\n  allowDelete: boolean\n  onStartRename: () => void\n  onDuplicate: () => void\n  onSetDefault: () => void\n  onDelete: () => void\n}) {\n  const [open, setOpen] = React.useState(false)\n  const menuRef = React.useRef<HTMLDivElement>(null)\n  const triggerRef = React.useRef<HTMLButtonElement>(null)\n\n  const items: {\n    label: string\n    icon: React.ReactNode\n    onClick: () => void\n    destructive?: boolean\n  }[] = []\n\n  if (allowRename && !view.isLocked) {\n    items.push({\n      label: \"Rename\",\n      icon: <PencilIcon className=\"size-3.5 text-muted-foreground\" />,\n      onClick: onStartRename,\n    })\n  }\n\n  if (allowDuplicate) {\n    items.push({\n      label: \"Duplicate\",\n      icon: <CopyIcon className=\"size-3.5 text-muted-foreground\" />,\n      onClick: onDuplicate,\n    })\n  }\n\n  if (allowSetDefault && !view.isDefault) {\n    items.push({\n      label: \"Set as default\",\n      icon: <StarIcon className=\"size-3.5 fill-amber-500 text-amber-500\" />,\n      onClick: onSetDefault,\n    })\n  }\n\n  if (allowDelete && !view.isLocked && !isOnlyView) {\n    items.push({\n      label: \"Delete\",\n      icon: <Trash2Icon className=\"size-3.5\" />,\n      onClick: onDelete,\n      destructive: true,\n    })\n  }\n\n  React.useEffect(() => {\n    if (open) {\n      const timer = setTimeout(() => {\n        const first = menuRef.current?.querySelector<HTMLButtonElement>(\n          '[role=\"menuitem\"]:not([disabled])'\n        )\n        first?.focus()\n      }, 0)\n      return () => clearTimeout(timer)\n    }\n  }, [open])\n\n  function handleItemKeyDown(\n    e: React.KeyboardEvent<HTMLButtonElement>,\n    index: number\n  ) {\n    if (e.key === \"ArrowDown\") {\n      e.preventDefault()\n      e.stopPropagation()\n      const nextIndex = (index + 1) % items.length\n      const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitem\"]:not([disabled])'\n      )\n      buttons?.[nextIndex]?.focus()\n    } else if (e.key === \"ArrowUp\") {\n      e.preventDefault()\n      e.stopPropagation()\n      const prevIndex = (index - 1 + items.length) % items.length\n      const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitem\"]:not([disabled])'\n      )\n      buttons?.[prevIndex]?.focus()\n    } else if (e.key === \"Home\") {\n      e.preventDefault()\n      e.stopPropagation()\n      const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitem\"]:not([disabled])'\n      )\n      buttons?.[0]?.focus()\n    } else if (e.key === \"End\") {\n      e.preventDefault()\n      e.stopPropagation()\n      const buttons = menuRef.current?.querySelectorAll<HTMLButtonElement>(\n        '[role=\"menuitem\"]:not([disabled])'\n      )\n      buttons?.[buttons.length - 1]?.focus()\n    } else if (e.key === \"Escape\") {\n      e.preventDefault()\n      e.stopPropagation()\n      setOpen(false)\n      triggerRef.current?.focus()\n    }\n  }\n\n  return (\n    <Popover\n      open={open && !isPending}\n      onOpenChange={(next) => setOpen(next && !isPending)}\n    >\n      <PopoverTrigger\n        ref={triggerRef}\n        type=\"button\"\n        aria-haspopup=\"menu\"\n        aria-expanded={open && !isPending}\n        aria-disabled={isPending || undefined}\n        aria-label={`Options for ${view.name}`}\n        className=\"flex size-7 items-center justify-center rounded-md text-muted-foreground opacity-70 transition-opacity outline-none hover:opacity-100 focus-visible:opacity-100 focus-visible:ring-1 focus-visible:ring-ring aria-disabled:opacity-40\"\n      >\n        <MoreHorizontalIcon className=\"size-3.5\" />\n      </PopoverTrigger>\n      <PopoverContent align=\"start\" className=\"w-44 p-1\">\n        <div\n          ref={menuRef}\n          role=\"menu\"\n          aria-label={`Options for ${view.name}`}\n          className=\"space-y-0.5 text-xs outline-none\"\n        >\n          {items.map((item, index) => (\n            <React.Fragment key={item.label}>\n              {item.destructive && <div className=\"my-1 h-px bg-border\" />}\n              <button\n                type=\"button\"\n                role=\"menuitem\"\n                onClick={() => {\n                  setOpen(false)\n                  item.onClick()\n                }}\n                onKeyDown={(e) => handleItemKeyDown(e, index)}\n                className={cn(\n                  \"flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-left transition-colors outline-none\",\n                  item.destructive\n                    ? \"text-destructive hover:bg-destructive/10 focus:bg-destructive/10\"\n                    : \"hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground\"\n                )}\n              >\n                {item.icon}\n                {item.label}\n              </button>\n            </React.Fragment>\n          ))}\n        </div>\n      </PopoverContent>\n    </Popover>\n  )\n}\n\nexport { SavedViews }\n",
      "type": "registry:ui"
    },
    {
      "path": "registry/default/lib/saved-views.ts",
      "content": "/**\n * Represents a saved view configuration.\n * Note: `data` must be structured-cloneable (serializable) if using duplicateSavedView.\n */\nexport interface SavedView<T = unknown> {\n  id: string\n  name: string\n  isDefault?: boolean\n  isLocked?: boolean\n  icon?: string\n  data?: T\n}\n\nexport function generateViewId(): string {\n  return \"view-\" + Math.random().toString(36).slice(2, 9)\n}\n\nexport function findUniqueViewName(\n  baseName: string,\n  existingViews: { name: string }[]\n): string {\n  const trimmed = baseName.trim()\n  if (!trimmed) return \"Untitled View\"\n\n  const existingNames = new Set(\n    existingViews.map((v) => v.name.trim().toLowerCase())\n  )\n\n  if (!existingNames.has(trimmed.toLowerCase())) {\n    return trimmed\n  }\n\n  // If already ends with (Copy) or (Copy N)\n  const copyMatch = trimmed.match(/^(.*?) \\(Copy(?: (\\d+))?\\)$/)\n  const prefix = copyMatch ? copyMatch[1] : trimmed\n  let count = copyMatch && copyMatch[2] ? parseInt(copyMatch[2], 10) + 1 : 1\n\n  while (true) {\n    const candidate =\n      count === 1 ? `${prefix} (Copy)` : `${prefix} (Copy ${count})`\n    if (!existingNames.has(candidate.toLowerCase())) {\n      return candidate\n    }\n    count++\n  }\n}\n\nexport function createSavedView<T>(\n  name: string,\n  options?: {\n    id?: string\n    data?: T\n    isDefault?: boolean\n    isLocked?: boolean\n    icon?: string\n  }\n): SavedView<T> {\n  return {\n    id: options?.id || generateViewId(),\n    name: name.trim() || \"Untitled View\",\n    ...(options?.isDefault ? { isDefault: true } : {}),\n    ...(options?.isLocked ? { isLocked: true } : {}),\n    ...(options?.icon ? { icon: options.icon } : {}),\n    ...(options?.data !== undefined ? { data: options.data } : {}),\n  }\n}\n\nexport function duplicateSavedView<T>(\n  sourceView: SavedView<T>,\n  existingViews: SavedView<T>[]\n): SavedView<T> {\n  const baseDuplicateName = / \\(Copy(?: \\d+)?\\)$/.test(sourceView.name)\n    ? sourceView.name\n    : `${sourceView.name} (Copy)`\n  const newName = findUniqueViewName(baseDuplicateName, existingViews)\n  return {\n    id: generateViewId(),\n    name: newName,\n    isDefault: false,\n    ...(sourceView.icon ? { icon: sourceView.icon } : {}),\n    ...(sourceView.data !== undefined\n      ? {\n          data:\n            typeof sourceView.data === \"object\" && sourceView.data !== null\n              ? structuredClone(sourceView.data)\n              : sourceView.data,\n        }\n      : {}),\n  }\n}\n\nexport function renameSavedView<T>(\n  views: SavedView<T>[],\n  id: string,\n  nextName: string\n): SavedView<T>[] {\n  const trimmed = nextName.trim()\n  if (!trimmed) return views\n  return views.map((view) =>\n    view.id === id ? { ...view, name: trimmed } : view\n  )\n}\n\nexport function deleteSavedView<T>(\n  views: SavedView<T>[],\n  id: string,\n  currentActiveId?: string\n): { views: SavedView<T>[]; nextActiveId: string | null } {\n  const remaining = views.filter((view) => view.id !== id)\n  if (remaining.length === views.length) {\n    return { views, nextActiveId: currentActiveId ?? null }\n  }\n\n  let nextActiveId = currentActiveId ?? null\n  if (currentActiveId === id) {\n    const defaultView = remaining.find((v) => v.isDefault)\n    nextActiveId = defaultView ? defaultView.id : (remaining[0]?.id ?? null)\n  }\n\n  return { views: remaining, nextActiveId }\n}\n\nexport function setDefaultSavedView<T>(\n  views: SavedView<T>[],\n  id: string\n): SavedView<T>[] {\n  return views.map((view) => ({\n    ...view,\n    isDefault: view.id === id,\n  }))\n}\n\nexport function getActiveSavedView<T>(\n  views: SavedView<T>[],\n  activeId?: string | null\n): SavedView<T> | undefined {\n  if (activeId) {\n    const found = views.find((v) => v.id === activeId)\n    if (found) return found\n  }\n  return views.find((v) => v.isDefault) ?? views[0]\n}\n\nexport function restoreSavedView<T>(\n  views: SavedView<T>[],\n  view: SavedView<T>,\n  index: number\n): SavedView<T>[] {\n  if (views.some((v) => v.id === view.id)) return views\n  const restored =\n    view.isDefault && views.some((v) => v.isDefault)\n      ? { ...view, isDefault: false }\n      : view\n  const at = Math.min(Math.max(index, 0), views.length)\n  return [...views.slice(0, at), restored, ...views.slice(at)]\n}\n\nfunction definedKeys(value: object): string[] {\n  return Object.keys(value).filter(\n    (key) => (value as Record<string, unknown>)[key] !== undefined\n  )\n}\n\nexport function isViewDataEqual(a: unknown, b: unknown): boolean {\n  return equal(a, b, new WeakMap())\n}\n\nfunction equal(\n  a: unknown,\n  b: unknown,\n  seen: WeakMap<object, WeakSet<object>>\n): boolean {\n  if (a === b || Object.is(a, b)) return true\n  if (typeof a !== \"object\" || typeof b !== \"object\" || !a || !b) return false\n  if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false\n  if (seen.get(a)?.has(b)) return true\n  const pairs = seen.get(a) ?? new WeakSet<object>()\n  seen.set(a, pairs.add(b))\n  const result = equalObjects(a, b, seen)\n  pairs.delete(b)\n  return result\n}\n\nfunction equalObjects(\n  a: object,\n  b: object,\n  seen: WeakMap<object, WeakSet<object>>\n): boolean {\n  if (a instanceof Date && b instanceof Date) {\n    return a.getTime() === b.getTime()\n  }\n  if (a instanceof RegExp && b instanceof RegExp) {\n    return String(a) === String(b)\n  }\n  if (a instanceof Set && b instanceof Set) {\n    if (a.size !== b.size) return false\n    const unmatched = [...b]\n    return [...a].every((item) => {\n      const index = unmatched.findIndex((other) => equal(item, other, seen))\n      if (index === -1) return false\n      unmatched.splice(index, 1)\n      return true\n    })\n  }\n  if (a instanceof Map && b instanceof Map) {\n    return (\n      a.size === b.size &&\n      [...a].every(\n        ([key, value]) => b.has(key) && equal(value, b.get(key), seen)\n      )\n    )\n  }\n  if (Array.isArray(a) && Array.isArray(b)) {\n    return (\n      a.length === b.length && a.every((item, i) => equal(item, b[i], seen))\n    )\n  }\n  const keysA = definedKeys(a)\n  const keysB = definedKeys(b)\n  return (\n    keysA.length === keysB.length &&\n    keysA.every((key) =>\n      equal(\n        (a as Record<string, unknown>)[key],\n        (b as Record<string, unknown>)[key],\n        seen\n      )\n    )\n  )\n}\n\nexport function isViewDirty<T>(\n  view: SavedView<T> | undefined,\n  currentData: T | undefined,\n  isEqual: (saved: T, current: T) => boolean = isViewDataEqual\n): boolean {\n  if (view?.data === undefined || currentData === undefined) return false\n  return !isEqual(view.data, currentData)\n}\n\nexport function readViewParam<T>(\n  search: string | URLSearchParams,\n  views: SavedView<T>[],\n  key = \"view\"\n): string | undefined {\n  const id = new URLSearchParams(search).get(key)\n  return getActiveSavedView(views, id)?.id\n}\n\nexport function writeViewParam(\n  search: string | URLSearchParams,\n  activeId: string,\n  key = \"view\"\n): string {\n  const params = new URLSearchParams(search)\n  params.set(key, activeId)\n  return params.toString()\n}\n",
      "type": "registry:lib"
    }
  ],
  "type": "registry:ui"
}