{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "saved-views",
  "type": "registry:block",
  "title": "Saved views",
  "description": "Persisted named views and namespaced URL state with validation and back navigation.",
  "dependencies": [],
  "registryDependencies": [
    "button",
    "input"
  ],
  "files": [
    {
      "path": "blocks/saved-views.tsx",
      "type": "registry:component",
      "target": "components/shine/saved-views.tsx",
      "content": "\"use client\";\nimport { useEffect,useId,useState,useRef } from \"react\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nexport type SavedView<T>={id:string;name:string;state:T};\n/** The application persists views; URL state uses one namespaced query parameter. */\nexport function SavedViews<T>({namespace,value,onChange,validate,views,onSave,onDelete}:{namespace:string;value:T;onChange:(value:T)=>void;validate:(value:unknown)=>value is T;views:SavedView<T>[];onSave:(view:SavedView<T>)=>Promise<void>;onDelete:(id:string)=>Promise<void>}) {\n const handlers=useRef({onChange,validate});handlers.current={onChange,validate};const lock=useRef(false),id=useId(),[name,setName]=useState(\"\"),[busy,setBusy]=useState(false),[error,setError]=useState(\"\"),[status,setStatus]=useState(\"\");\n useEffect(()=>{function restore(){const raw=new URL(location.href).searchParams.get(namespace);if(raw===null)return;try{const next=JSON.parse(raw);if(!handlers.current.validate(next))throw Error(\"Saved URL has an incompatible view.\");handlers.current.onChange(next);}catch(cause){setError(cause instanceof Error?cause.message:\"Could not restore view.\");}}restore();addEventListener(\"popstate\",restore);return()=>removeEventListener(\"popstate\",restore);},[namespace]);\n async function persist(action:()=>Promise<void>,message:string){if(lock.current)return;lock.current=true;setBusy(true);setError(\"\");setStatus(\"\");try{await action();setStatus(message);}catch(cause){setError(cause instanceof Error?cause.message:\"Could not save changes. Try again.\");}finally{lock.current=false;setBusy(false);}}\n function select(state:T){if(!validate(state)){setError(\"This view is incompatible with the current table.\");return;}const url=new URL(location.href);url.searchParams.set(namespace,JSON.stringify(state));history.pushState(null,\"\",url);onChange(state);setStatus(\"View applied. The URL includes this view.\");}\n return <section data-product-pattern=\"saved-views\" aria-label=\"Saved views\" className=\"space-y-3 rounded-md border border-border p-4\"><div className=\"flex flex-wrap gap-3\">{views.map(view=><div key={view.id} className=\"flex gap-1\"><Button variant=\"outline\" disabled={busy} onClick={()=>select(view.state)}>{view.name}</Button><Button variant=\"ghost\" disabled={busy} aria-label={\"Delete view \"+view.name} onClick={()=>void persist(()=>onDelete(view.id),\"View deleted.\")}>Delete</Button></div>)}</div><form className=\"flex flex-wrap items-end gap-3\" onSubmit={event=>{event.preventDefault();if(!name.trim()||busy)return;void persist(async()=>{await onSave({id:crypto.randomUUID(),name:name.trim(),state:value});setName(\"\");},\"View saved.\");}}><div><label htmlFor={id} className=\"block text-sm\">View name</label><Input id={id} maxLength={80} value={name} disabled={busy} onChange={event=>setName(event.target.value)}/></div><Button disabled={busy||!name.trim()}>Save current view</Button><Button type=\"button\" variant=\"outline\" onClick={()=>select(value)}>Put current view in URL</Button></form><p role=\"alert\" className=\"text-sm text-destructive\">{error}</p><p role=\"status\" className=\"text-sm\">{status}</p></section>;\n}\n"
    }
  ]
}
