{
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
  "name": "tree",
  "title": "Tree",
  "description": "Branches open on a spring with their rows falling in, and the selected row's key glides.",
  "dependencies": [
    "@ui-sdk/core@0.1.0-beta.2",
    "@ui-sdk/react@0.1.0-beta.2"
  ],
  "registryDependencies": [
    "@keyframery/motion"
  ],
  "files": [
    {
      "path": "registry/components/keyframery/primitives/tree.tsx",
      "content": "\"use client\"\n\n/* Animated drop-in for `@ui-sdk/react/tree`: same parts, same props. Swap the import and keep your code.\n   This file adds motion only. Colours, radii, shadows and spacing stay yours.\n   - Glide: the selected row's key springs from row to row when the selection moves. It is the tree's\n     `::after`; give it a surface with `after:` classes. A lighter hover highlight glides under the\n     pointer and keyboard focus as the tree's `::before`. Both default to a tint of the text colour.\n   - The chevron turns on a spring.\n   - Height and Cascade: a branch opens to its height and its rows fall in one after another. */\n\nimport {\n  Tree as TreeRoot,\n  TreeGroup as TreeGroupPart,\n  TreeItemIndicator as TreeItemIndicatorPart,\n  type TreeMode,\n  type TreeProps,\n} from \"@ui-sdk/react/tree\"\nimport * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nexport * from \"@ui-sdk/react/tree\"\n\nconst ROW = '[data-slot=\"tree-label\"]'\nconst MARK = \"data-kf-tree-hl\"\n\n/* Moves the hover mark to `row`, or hides the highlight where it is (so it can glide on from there). */\nfunction mark(root: HTMLElement, row: Element | null) {\n  if (!row || row.hasAttribute(\"data-disabled\")) {\n    root.setAttribute(\"data-kf-idle\", \"\")\n    return\n  }\n  for (const old of root.querySelectorAll(`[${MARK}]`)) if (old !== row) old.removeAttribute(MARK)\n  row.setAttribute(MARK, \"\")\n  root.removeAttribute(\"data-kf-idle\")\n  if (!root.hasAttribute(\"data-kf-glide\")) setTimeout(() => root.setAttribute(\"data-kf-glide\", \"\"), 40)\n}\n\n/* The row under the pointer, or the row of the focused item (focus stays on the item). */\nfunction rowFrom(root: HTMLElement, target: EventTarget | null) {\n  if (!(target instanceof Element)) return null\n  const row =\n    target.closest(ROW) ??\n    (target.matches('[data-slot=\"tree-item\"]') ? target.querySelector(`:scope > ${ROW}`) : null)\n  return row && root.contains(row) ? row : null\n}\n\nfunction Tree<Mode extends TreeMode = \"single\">({\n  className,\n  onPointerOver,\n  onPointerLeave,\n  onFocus,\n  onBlur,\n  ...props\n}: TreeProps<Mode>) {\n  /* Hold the selection's glide until the rows are laid out, so the key is simply there on load. */\n  const [ready, setReady] = React.useState(false)\n  React.useEffect(() => {\n    const timer = setTimeout(() => setReady(true), 150)\n    return () => clearTimeout(timer)\n  }, [])\n  const hovering = React.useRef(false)\n\n  return (\n    <TreeRoot\n      data-kf-ready={ready || undefined}\n      className={cn(\n        \"relative isolate [anchor-scope:--kf-tree-hl,--kf-tree-sel]\",\n        /* Which row each highlight rests on. A row can hold both. */\n        \"[&_[data-kf-tree-hl]]:[anchor-name:--kf-tree-hl]\",\n        \"[&_[data-slot=tree-label]:is([data-selected],[data-current])]:[anchor-name:--kf-tree-sel]\",\n        \"[&_[data-slot=tree-label][data-kf-tree-hl]:is([data-selected],[data-current])]:[anchor-name:--kf-tree-hl,--kf-tree-sel]\",\n        /* Hover highlight. */\n        \"before:pointer-events-none before:absolute before:-z-10 before:bg-[color-mix(in_oklch,currentColor_6%,transparent)]\",\n        \"before:[position-anchor:--kf-tree-hl] before:top-[anchor(top)] before:right-[anchor(right)] before:bottom-[anchor(bottom)] before:left-[anchor(left)]\",\n        \"before:transition-[opacity,scale] before:duration-[calc(200ms*var(--kf-speed,1))] before:ease-house\",\n        \"data-kf-glide:before:[transition:top_calc(360ms*var(--kf-speed,1))_var(--ease-spring),right_calc(360ms*var(--kf-speed,1))_var(--ease-spring),bottom_calc(360ms*var(--kf-speed,1))_var(--ease-spring),left_calc(360ms*var(--kf-speed,1))_var(--ease-spring),opacity_calc(200ms*var(--kf-speed,1))_var(--ease-house),scale_calc(200ms*var(--kf-speed,1))_var(--ease-house)]\",\n        \"data-kf-idle:before:scale-[0.96] data-kf-idle:before:opacity-0 not-has-[[data-kf-tree-hl]]:before:opacity-0\",\n        /* Selection key. It hides while its row is inside a closing branch. */\n        \"after:pointer-events-none after:absolute after:-z-10 after:bg-[color-mix(in_oklch,currentColor_10%,transparent)]\",\n        \"after:[position-anchor:--kf-tree-sel] after:top-[anchor(top)] after:right-[anchor(right)] after:bottom-[anchor(bottom)] after:left-[anchor(left)]\",\n        \"after:transition-[opacity] after:duration-[calc(200ms*var(--kf-speed,1))] after:ease-house\",\n        \"data-kf-ready:after:[transition:top_calc(380ms*var(--kf-speed,1))_var(--ease-spring),right_calc(380ms*var(--kf-speed,1))_var(--ease-spring),bottom_calc(380ms*var(--kf-speed,1))_var(--ease-spring),left_calc(380ms*var(--kf-speed,1))_var(--ease-spring),opacity_calc(200ms*var(--kf-speed,1))_var(--ease-house)]\",\n        \"not-has-[[data-slot=tree-label]:is([data-selected],[data-current])]:after:opacity-0\",\n        \"has-[[data-ending-style]_[data-slot=tree-label]:is([data-selected],[data-current])]:after:opacity-0\",\n        \"motion-reduce:before:[transition:none]! motion-reduce:after:[transition:none]!\",\n        className,\n      )}\n      onPointerOver={(event) => {\n        onPointerOver?.(event)\n        hovering.current = true\n        const row = rowFrom(event.currentTarget, event.target)\n        if (row) mark(event.currentTarget, row)\n      }}\n      onPointerLeave={(event) => {\n        onPointerLeave?.(event)\n        hovering.current = false\n        const root = event.currentTarget\n        mark(root, rowFrom(root, root.querySelector(\":focus-visible\")))\n      }}\n      onFocus={(event) => {\n        onFocus?.(event)\n        const target = event.target as Element\n        if (!hovering.current && target.matches(\":focus-visible\")) mark(event.currentTarget, rowFrom(event.currentTarget, target))\n      }}\n      onBlur={(event) => {\n        onBlur?.(event)\n        const root = event.currentTarget\n        if (!hovering.current && !root.contains(event.relatedTarget as Node | null)) mark(root, null)\n      }}\n      {...props}\n    />\n  )\n}\n\nfunction TreeItemIndicator({ className, ...props }: React.ComponentProps<typeof TreeItemIndicatorPart>) {\n  return (\n    <TreeItemIndicatorPart\n      className={cn(\n        \"[transition:rotate_calc(520ms*var(--kf-speed,1))_var(--ease-pop),color_calc(150ms*var(--kf-speed,1))_var(--ease-house),background-color_calc(150ms*var(--kf-speed,1))_var(--ease-house)] data-expanded:rotate-90\",\n        \"motion-reduce:[transition:none]!\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/* Opens to its height while its rows cascade in; closes a little faster, all at once. */\nfunction TreeGroup({ className, ...props }: React.ComponentProps<typeof TreeGroupPart>) {\n  return (\n    <TreeGroupPart\n      className={cn(\n        \"h-(--tree-group-height,auto) [clip-path:inset(-4px_-100vw)]\",\n        \"transition-[height] duration-[calc(380ms*var(--kf-speed,1))] ease-house\",\n        \"data-starting-style:h-0 data-ending-style:h-0 data-ending-style:duration-[calc(260ms*var(--kf-speed,1))]\",\n        \"*:transition-[opacity,translate,filter] *:duration-[calc(420ms*var(--kf-speed,1))] *:ease-spring\",\n        \"*:[transition-delay:calc((min(sibling-index(),10)-1)*calc(36ms*var(--kf-speed,1))+calc(30ms*var(--kf-speed,1)))]\",\n        \"data-starting-style:*:translate-y-2 data-starting-style:*:opacity-0 data-starting-style:*:blur-[4px] data-starting-style:*:transition-none\",\n        \"data-ending-style:*:opacity-0 data-ending-style:*:blur-[2px] data-ending-style:*:duration-[calc(200ms*var(--kf-speed,1))] data-ending-style:*:ease-house data-ending-style:*:[transition-delay:0ms]\",\n        \"motion-reduce:transition-none! motion-reduce:*:transition-none\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\nexport { Tree, TreeGroup, TreeItemIndicator }\n",
      "type": "registry:component",
      "target": "components/keyframery/primitives/tree.tsx"
    },
    {
      "path": "registry/components/keyframery/tree.tsx",
      "content": "\"use client\"\n\n/* Styled tree: the animated primitive plus the Keyframery look. */\n\nimport * as React from \"react\"\n\nimport {\n  Tree as TreeRoot,\n  TreeGroup as TreeGroupPart,\n  TreeItem as TreeItemPart,\n  TreeItemCheckbox as TreeItemCheckboxPart,\n  TreeItemIndicator as TreeItemIndicatorPart,\n  TreeItemLabel as TreeItemLabelPart,\n  treeFromPaths,\n  type TreeItemData,\n  type TreeMode,\n  type TreeProps,\n} from \"@/components/keyframery/primitives/tree\"\nimport { cn } from \"@/lib/utils\"\n\nconst icon = {\n  viewBox: \"0 0 16 16\",\n  fill: \"none\",\n  stroke: \"currentColor\",\n  strokeWidth: 1.5,\n  strokeLinecap: \"round\",\n  strokeLinejoin: \"round\",\n} as const\n\n/* Lets rows rendered from data know whether the tree has checkboxes. */\nconst CheckableContext = React.createContext(false)\n\nfunction Tree<Mode extends TreeMode = \"single\">({\n  className,\n  children,\n  ...props\n}: TreeProps<Mode>) {\n  const { items, paths } = props\n  const data = React.useMemo(\n    () => items ?? (paths ? treeFromPaths(paths) : undefined),\n    [items, paths],\n  )\n  return (\n    <CheckableContext.Provider value={!!props.checkable}>\n      <TreeRoot\n        className={cn(\n          \"flex min-w-0 flex-col text-[13px] outline-none\",\n          /* The two gliding surfaces: a light one under the pointer, the selected row's key. */\n          \"before:rounded-[8px] before:bg-accent/45 dark:before:bg-accent/40\",\n          \"after:rounded-[8px] after:bg-accent dark:after:bg-accent\",\n          className,\n        )}\n        {...props}\n      >\n        {children ?? (data ? <TreeItems items={data} /> : null)}\n      </TreeRoot>\n    </CheckableContext.Provider>\n  )\n}\n\n/* UI SDK calls an item a branch when it finds its own TreeGroup inside. Ours wraps that\n   part, so the item looks for either one and says so itself. */\nfunction hasGroup(children: React.ReactNode): boolean {\n  return React.Children.toArray(children).some(\n    (child) =>\n      React.isValidElement<{ children?: React.ReactNode }>(child) &&\n      (child.type === TreeGroup ||\n        child.type === TreeGroupPart ||\n        hasGroup(child.props.children)),\n  )\n}\n\nfunction TreeItem({\n  className,\n  branch,\n  children,\n  ...props\n}: React.ComponentProps<typeof TreeItemPart>) {\n  return (\n    <TreeItemPart\n      branch={\n        branch ?? (typeof children === \"function\" ? undefined : hasGroup(children) || undefined)\n      }\n      className={cn(\n        \"outline-none\",\n        /* Focus stays on the item, and the ring is drawn on its own row. */\n        \"[&:focus-visible>[data-slot=tree-label]]:ring-2 [&:focus-visible>[data-slot=tree-label]]:ring-ring\",\n        \"[&:focus-visible>[data-slot=tree-label]]:ring-offset-2 [&:focus-visible>[data-slot=tree-label]]:ring-offset-background\",\n        className,\n      )}\n      {...props}\n    >\n      {children}\n    </TreeItemPart>\n  )\n}\n\n/* The row. A leaf is padded by one chevron so it lines up with the branches beside it. */\nfunction TreeItemLabel({\n  className,\n  ...props\n}: React.ComponentProps<typeof TreeItemLabelPart>) {\n  return (\n    <TreeItemLabelPart\n      className={cn(\n        \"relative mt-px flex h-7 min-w-0 cursor-pointer items-center gap-2 rounded-[8px] pr-2 pl-2 not-data-branch:pl-8\",\n        \"text-[13px] leading-[1.35] font-[450] text-foreground no-underline select-none\",\n        \"[&_svg]:size-4 [&_svg]:shrink-0\",\n        \"transition-[color] duration-[calc(150ms*var(--kf-speed,1))] motion-reduce:transition-none!\",\n        \"data-selected:text-accent-foreground data-current:text-accent-foreground\",\n        \"data-disabled:cursor-not-allowed data-disabled:opacity-40 data-pending:cursor-progress\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/* The chevron. It turns as the branch opens, and gives way to a spinner while children load. */\nfunction TreeItemIndicator({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TreeItemIndicatorPart>) {\n  return (\n    <TreeItemIndicatorPart\n      className={cn(\n        \"group/indicator -m-0.5 grid size-5 shrink-0 place-items-center rounded-[6px] text-muted-foreground\",\n        \"hover:bg-foreground/[0.08] hover:text-foreground data-failed:text-destructive\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <svg {...icon} className=\"group-data-loading/indicator:hidden\">\n            <path d=\"M6 3.75 10.25 8 6 12.25\" />\n          </svg>\n          <svg\n            {...icon}\n            className=\"hidden animate-spin [animation-duration:calc(700ms*var(--kf-speed,1))] group-data-loading/indicator:block motion-reduce:[animation-duration:calc(1600ms*var(--kf-speed,1))]\"\n          >\n            <circle cx=\"8\" cy=\"8\" r=\"5.75\" opacity=\"0.28\" />\n            <path d=\"M8 2.25A5.75 5.75 0 0 1 13.75 8\" />\n          </svg>\n        </>\n      )}\n    </TreeItemIndicatorPart>\n  )\n}\n\n/* The check draws itself. A branch with only some of its items checked shows a dash. */\nfunction TreeItemCheckbox({\n  className,\n  children,\n  ...props\n}: React.ComponentProps<typeof TreeItemCheckboxPart>) {\n  return (\n    <TreeItemCheckboxPart\n      className={cn(\n        \"group/checkbox grid size-4 shrink-0 place-items-center rounded-[5px] bg-background text-primary-foreground *:[grid-area:1/1]\",\n        \"shadow-[0_0_0_1px_rgb(0_0_0/0.14),0_1px_2px_rgb(0_0_0/0.05)]\",\n        \"dark:bg-accent/40 dark:shadow-[0_0_0_1px_rgb(255_255_255/0.14)]\",\n        \"not-data-disabled:data-unchecked:hover:shadow-[0_0_0_1px_rgb(0_0_0/0.24),0_1px_2px_rgb(0_0_0/0.06)]\",\n        \"dark:not-data-disabled:data-unchecked:hover:shadow-[0_0_0_1px_rgb(255_255_255/0.24)]\",\n        \"transition-[background-color,box-shadow] duration-[calc(150ms*var(--kf-speed,1))] ease-house motion-reduce:transition-none!\",\n        \"data-checked:bg-primary data-indeterminate:bg-primary dark:data-checked:bg-primary dark:data-indeterminate:bg-primary\",\n        \"data-checked:shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25)] data-indeterminate:shadow-[inset_0_1px_0_rgb(255_255_255/0.14),0_1px_2px_rgb(0_0_0/0.25)]\",\n        \"dark:data-checked:shadow-[inset_0_1px_0_rgb(255_255_255/0.7),0_1px_2px_rgb(0_0_0/0.5)] dark:data-indeterminate:shadow-[inset_0_1px_0_rgb(255_255_255/0.7),0_1px_2px_rgb(0_0_0/0.5)]\",\n        \"data-disabled:opacity-50\",\n        className,\n      )}\n      {...props}\n    >\n      {children ?? (\n        <>\n          <svg {...icon} strokeWidth={1.75}>\n            <path\n              d=\"M4.25 8.4 6.9 11l4.85-5.75\"\n              pathLength={1}\n              className=\"transition-[stroke-dashoffset] duration-[calc(200ms*var(--kf-speed,1))] ease-house [stroke-dasharray:1] [stroke-dashoffset:1] group-data-checked/checkbox:[stroke-dashoffset:0] motion-reduce:transition-none!\"\n            />\n          </svg>\n          <svg {...icon} strokeWidth={1.75}>\n            <path\n              d=\"M4.75 8h6.5\"\n              pathLength={1}\n              className=\"transition-[stroke-dashoffset] duration-[calc(200ms*var(--kf-speed,1))] ease-house [stroke-dasharray:1] [stroke-dashoffset:1] group-data-indeterminate/checkbox:[stroke-dashoffset:0] motion-reduce:transition-none!\"\n            />\n          </svg>\n        </>\n      )}\n    </TreeItemCheckboxPart>\n  )\n}\n\n/* A branch's children, behind a hairline guide that drops from the chevron above.\n   The clip reaches 4px past the box, so a focus ring on a row inside is never cut. */\nfunction TreeGroup({\n  className,\n  ...props\n}: React.ComponentProps<typeof TreeGroupPart>) {\n  return (\n    <TreeGroupPart\n      className={cn(\n        \"relative ml-[15.5px] flex flex-col pl-[6.5px]\",\n        \"before:absolute before:inset-y-1 before:left-0 before:w-px before:bg-border\",\n        className,\n      )}\n      {...props}\n    />\n  )\n}\n\n/* Rows from data: what `<Tree items={...} />` renders when it has no children. */\nfunction TreeItems({ items }: { items: TreeItemData[] }) {\n  const checkable = React.useContext(CheckableContext)\n  return items.map((item) => (\n    <TreeItem\n      key={item.value}\n      value={item.value}\n      branch={item.children !== undefined}\n      disabled={item.disabled}\n    >\n      <TreeItemLabel render={item.href ? <a href={item.href} /> : undefined}>\n        {item.children && <TreeItemIndicator />}\n        {checkable && <TreeItemCheckbox />}\n        <span className=\"truncate\">{item.label}</span>\n      </TreeItemLabel>\n      {item.children && (\n        <TreeGroup>\n          <TreeItems items={item.children} />\n        </TreeGroup>\n      )}\n    </TreeItem>\n  ))\n}\n\nexport {\n  Tree,\n  TreeGroup,\n  TreeItem,\n  TreeItemCheckbox,\n  TreeItemIndicator,\n  TreeItemLabel,\n  TreeItems,\n}\n",
      "type": "registry:component",
      "target": "components/keyframery/tree.tsx"
    }
  ],
  "type": "registry:component"
}