{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "viewport-bookmarks",
  "type": "registry:component",
  "title": "Viewport Bookmarks",
  "description": "Saved-view list for the canvas — pinned spatial locations with optional active state.",
  "dependencies": [
    "@vllnt/ui@^0.2.1"
  ],
  "registryDependencies": [],
  "files": [
    {
      "path": "registry/default/viewport-bookmarks/viewport-bookmarks.tsx",
      "content": "\"use client\";\n\nimport {\n  type ComponentPropsWithoutRef,\n  forwardRef,\n  type ReactNode,\n} from \"react\";\n\nimport { cn } from \"@vllnt/ui\";\n\n/**\n * One saved viewport.\n *\n * @public\n */\nexport type ViewportBookmark = {\n  /** Optional accent color for the row glyph. */\n  color?: string;\n  /** Optional secondary line (zoom level, last-visited, owner). */\n  detail?: ReactNode;\n  /** Stable identifier — used as the React key. */\n  id: string;\n  /** Display name for the bookmark. */\n  label: ReactNode;\n};\n\n/**\n * Localizable strings.\n *\n * @public\n */\nexport type ViewportBookmarksLabels = {\n  /** Empty-state copy. Defaults to `\"No saved views\"`. */\n  empty?: string;\n  /** Aria-label override. Defaults to `\"Viewport bookmarks\"`. */\n  region?: string;\n};\n\nconst DEFAULT_LABELS = {\n  empty: \"No saved views\",\n  region: \"Viewport bookmarks\",\n} as const satisfies Required<ViewportBookmarksLabels>;\n\n/**\n * Props for {@link ViewportBookmarks}.\n *\n * @public\n */\nexport type ViewportBookmarksProps = {\n  /** Optional active bookmark id — renders the row in the selected state. */\n  activeId?: string;\n  /** Bookmark entries in render order. */\n  bookmarks: ViewportBookmark[];\n  /** Localizable strings. */\n  labels?: ViewportBookmarksLabels;\n  /** Click handler — receives the activated bookmark id. */\n  onSelect?: (id: string) => void;\n  /** Optional title rendered above the rows. Defaults to `\"Saved views\"`. */\n  title?: ReactNode;\n} & ComponentPropsWithoutRef<\"section\">;\n\nconst Row = (props: {\n  active: boolean;\n  bookmark: ViewportBookmark;\n  onSelect?: (id: string) => void;\n}): React.ReactElement => {\n  const { active, bookmark, onSelect } = props;\n  const handleClick = (): void => {\n    onSelect?.(bookmark.id);\n  };\n  const rowClass =\n    \"flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs transition-colors\";\n  const activeClass = active\n    ? \"bg-muted/60 text-foreground\"\n    : \"text-muted-foreground hover:bg-muted/30 hover:text-foreground\";\n  if (onSelect) {\n    return (\n      <button\n        aria-pressed={active}\n        className={cn(\n          rowClass,\n          activeClass,\n          \"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring\",\n        )}\n        data-viewport-bookmark={bookmark.id}\n        data-viewport-bookmark-active={active}\n        onClick={handleClick}\n        type=\"button\"\n      >\n        <RowBody bookmark={bookmark} />\n      </button>\n    );\n  }\n  return (\n    <span\n      className={cn(rowClass, activeClass)}\n      data-viewport-bookmark={bookmark.id}\n      data-viewport-bookmark-active={active}\n    >\n      <RowBody bookmark={bookmark} />\n    </span>\n  );\n};\n\nconst RowBody = (props: { bookmark: ViewportBookmark }): React.ReactElement => {\n  const { bookmark } = props;\n  return (\n    <>\n      <span\n        aria-hidden=\"true\"\n        className=\"size-1.5 rounded-full\"\n        style={{ backgroundColor: bookmark.color ?? \"hsl(var(--foreground))\" }}\n      />\n      <span className=\"flex flex-1 flex-col text-left\">\n        <span className=\"truncate font-medium\">{bookmark.label}</span>\n        {bookmark.detail ? (\n          <span\n            className=\"truncate text-[10px] text-muted-foreground\"\n            data-viewport-bookmark-detail\n          >\n            {bookmark.detail}\n          </span>\n        ) : null}\n      </span>\n    </>\n  );\n};\n\n/**\n * Saved-view list for the canvas — the spatial parallel of a tab\n * bar's pinned tabs. Each bookmark stores a viewport target the host\n * resolves to a pan / zoom transition. Pure presentation; the host\n * owns the bookmark store and the camera animation.\n *\n * @example\n * ```tsx\n * <ViewportBookmarks\n *   activeId={active}\n *   bookmarks={[\n *     { id: \"home\", label: \"Home base\", color: \"#5b8def\" },\n *     { id: \"incidents\", label: \"Incidents\", detail: \"5 open\", color: \"#ef4444\" },\n *   ]}\n *   onSelect={jumpTo}\n * />\n * ```\n *\n * @public\n */\nexport const ViewportBookmarks = forwardRef<\n  HTMLElement,\n  ViewportBookmarksProps\n>((props, ref) => {\n  const {\n    activeId,\n    bookmarks,\n    className,\n    labels,\n    onSelect,\n    title = \"Saved views\",\n    ...rest\n  } = props;\n  const resolvedLabels = { ...DEFAULT_LABELS, ...labels };\n  return (\n    <section\n      aria-label={resolvedLabels.region}\n      className={cn(\n        \"flex w-full flex-col gap-1 rounded-lg border border-border bg-background p-2 text-foreground\",\n        className,\n      )}\n      data-viewport-bookmarks\n      ref={ref}\n      {...rest}\n    >\n      <header className=\"px-2 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground\">\n        {title}\n      </header>\n      {bookmarks.length === 0 ? (\n        <p\n          className=\"px-2 py-3 text-center text-[11px] text-muted-foreground\"\n          data-viewport-bookmarks-state=\"empty\"\n        >\n          {resolvedLabels.empty}\n        </p>\n      ) : (\n        <ul className=\"space-y-0.5\">\n          {bookmarks.map((bookmark) => (\n            <li key={bookmark.id}>\n              <Row\n                active={activeId === bookmark.id}\n                bookmark={bookmark}\n                onSelect={onSelect}\n              />\n            </li>\n          ))}\n        </ul>\n      )}\n    </section>\n  );\n});\nViewportBookmarks.displayName = \"ViewportBookmarks\";\n",
      "type": "registry:component"
    }
  ],
  "version": "0.2.1",
  "stability": "stable"
}
