{
	"$schema": "https://shadcn-svelte.com/schema/registry-item.json",
	"name": "file-tree",
	"title": "File Tree",
	"type": "registry:block",
	"description": "An interactive file tree component with expandable folders, selectable files, and smooth animations.",
	"dependencies": [
		"@lucide/svelte",
		"motion-sv"
	],
	"files": [
		{
			"content": "<script lang=\"ts\" module>\n\texport type TreeViewElement = {\n\t\tid: string;\n\t\tname: string;\n\t\tisSelectable?: boolean;\n\t\tchildren?: TreeViewElement[];\n\t};\n</script>\n\n<script lang=\"ts\">\n\timport { setContext, getContext } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\timport type { Snippet } from \"svelte\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tinterface TreeProps extends HTMLAttributes<HTMLDivElement> {\n\t\t/**\n\t\t * Initial selected item ID\n\t\t */\n\t\tinitialSelectedId?: string;\n\t\t/**\n\t\t * Initial expanded items\n\t\t */\n\t\tinitialExpandedItems?: string[];\n\t\t/**\n\t\t * Tree elements data\n\t\t */\n\t\telements?: TreeViewElement[];\n\t\t/**\n\t\t * Show indicator line\n\t\t */\n\t\tindicator?: boolean;\n\t\t/**\n\t\t * Open folder icon\n\t\t */\n\t\topenIcon?: Snippet;\n\t\t/**\n\t\t * Close folder icon\n\t\t */\n\t\tcloseIcon?: Snippet;\n\t\t/**\n\t\t * Text direction\n\t\t */\n\t\tdir?: \"rtl\" | \"ltr\";\n\t\t/**\n\t\t * Children content\n\t\t */\n\t\tchildren: Snippet;\n\t\t/**\n\t\t * Additional CSS classes\n\t\t */\n\t\tclass?: string;\n\t}\n\n\tlet {\n\t\tinitialSelectedId,\n\t\tinitialExpandedItems = [],\n\t\telements,\n\t\tindicator = true,\n\t\topenIcon,\n\t\tcloseIcon,\n\t\tdir = \"ltr\",\n\t\tchildren,\n\t\tclass: className,\n\t\t...props\n\t}: TreeProps = $props();\n\n\tlet selectedId = $state(initialSelectedId);\n\tlet expandedItems = $state<string[]>(initialExpandedItems);\n\n\tconst selectItem = (id: string) => {\n\t\tselectedId = id;\n\t};\n\n\tconst handleExpand = (id: string) => {\n\t\tif (expandedItems.includes(id)) {\n\t\t\texpandedItems = expandedItems.filter((item) => item !== id);\n\t\t} else {\n\t\t\texpandedItems = [...expandedItems, id];\n\t\t}\n\t};\n\n\tconst setExpandedItems = (items: string[]) => {\n\t\texpandedItems = items;\n\t};\n\n\tconst direction = dir === \"rtl\" ? \"rtl\" : \"ltr\";\n\n\tsetContext(\"tree\", {\n\t\tselectedId: () => selectedId,\n\t\texpandedItems: () => expandedItems,\n\t\thandleExpand,\n\t\tselectItem,\n\t\tsetExpandedItems,\n\t\tindicator,\n\t\topenIcon,\n\t\tcloseIcon,\n\t\tdirection,\n\t});\n</script>\n\n<div class={cn(\"size-full\", className)} {...props}>\n\t<div class=\"relative h-full overflow-auto px-2\" dir={direction}>\n\t\t<div class=\"flex flex-col gap-1\">\n\t\t\t{@render children()}\n\t\t</div>\n\t</div>\n</div>\n",
			"type": "registry:component",
			"target": "magic/file-tree/file-tree.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { getContext } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\timport { motion, AnimatePresence } from \"motion-sv\";\n\timport type { Snippet } from \"svelte\";\n\timport type { HTMLAttributes } from \"svelte/elements\";\n\n\tinterface FolderProps extends HTMLAttributes<HTMLDivElement> {\n\t\t/**\n\t\t * Folder name/label\n\t\t */\n\t\telement: string;\n\t\t/**\n\t\t * Unique value for this folder\n\t\t */\n\t\tvalue: string;\n\t\t/**\n\t\t * Whether the folder is selectable\n\t\t */\n\t\tisSelectable?: boolean;\n\t\t/**\n\t\t * Whether the folder is selected\n\t\t */\n\t\tisSelect?: boolean;\n\t\t/**\n\t\t * Children content\n\t\t */\n\t\tchildren: Snippet;\n\t\t/**\n\t\t * Additional CSS classes\n\t\t */\n\t\tclass?: string;\n\t}\n\n\tlet {\n\t\telement,\n\t\tvalue,\n\t\tisSelectable = true,\n\t\tisSelect,\n\t\tchildren,\n\t\tclass: className,\n\t\t...props\n\t}: FolderProps = $props();\n\n\tconst context = getContext<{\n\t\tselectedId: () => string | undefined;\n\t\texpandedItems: () => string[];\n\t\thandleExpand: (id: string) => void;\n\t\tselectItem: (id: string) => void;\n\t\tsetExpandedItems: (items: string[]) => void;\n\t\tindicator: boolean;\n\t\topenIcon?: Snippet;\n\t\tcloseIcon?: Snippet;\n\t\tdirection: \"rtl\" | \"ltr\";\n\t}>(\"tree\");\n\n\tconst isExpanded = $derived(context.expandedItems().includes(value));\n\tconst isSelected = $derived(isSelect ?? context.selectedId() === value);\n\n\t// Default folder icons (SVG)\n\tconst defaultOpenIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/><path d=\"M2 10h20\"/></svg>`;\n\tconst defaultCloseIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z\"/></svg>`;\n</script>\n\n<div class=\"relative h-full overflow-hidden\" {...props}>\n\t<button\n\t\ttype=\"button\"\n\t\tclass={cn(\"flex w-full items-center gap-1 rounded-md text-left text-sm\", className, {\n\t\t\t\"bg-muted rounded-md\": isSelected && isSelectable,\n\t\t\t\"cursor-pointer\": isSelectable,\n\t\t\t\"cursor-not-allowed opacity-50\": !isSelectable,\n\t\t})}\n\t\tdisabled={!isSelectable}\n\t\tonclick={() => context.handleExpand(value)}\n\t>\n\t\t<span class=\"size-4\">\n\t\t\t{#if context.openIcon && isExpanded}\n\t\t\t\t{@render context.openIcon()}\n\t\t\t{:else if context.closeIcon && !isExpanded}\n\t\t\t\t{@render context.closeIcon()}\n\t\t\t{:else}\n\t\t\t\t{@html isExpanded ? defaultOpenIcon : defaultCloseIcon}\n\t\t\t{/if}\n\t\t</span>\n\t\t<span>{element}</span>\n\t</button>\n\n\t<AnimatePresence mode=\"popLayout\">\n\t\t{#if isExpanded}\n\t\t\t<motion.div\n\t\t\t\tinitial={{ height: 0, opacity: 0 }}\n\t\t\t\tanimate={{ height: \"auto\", opacity: 1 }}\n\t\t\t\texit={{ height: 0, opacity: 0 }}\n\t\t\t\ttransition={{ duration: 0.2, ease: \"easeInOut\" }}\n\t\t\t\tclass=\"relative overflow-hidden text-sm\"\n\t\t\t>\n\t\t\t\t{#if element && context.indicator}\n\t\t\t\t\t<div\n\t\t\t\t\t\tdir={context.direction}\n\t\t\t\t\t\tclass=\"bg-muted absolute left-1.5 h-full w-px rounded-md py-3 duration-300 ease-in-out hover:bg-slate-300 rtl:right-1.5\"\n\t\t\t\t\t\taria-hidden=\"true\"\n\t\t\t\t\t></div>\n\t\t\t\t{/if}\n\t\t\t\t<div class=\"ml-5 flex flex-col gap-1 py-1 rtl:mr-5\" dir={context.direction}>\n\t\t\t\t\t{@render children()}\n\t\t\t\t</div>\n\t\t\t</motion.div>\n\t\t{/if}\n\t</AnimatePresence>\n</div>\n",
			"type": "registry:component",
			"target": "magic/file-tree/folder.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { getContext } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\timport type { Snippet } from \"svelte\";\n\timport type { HTMLButtonAttributes } from \"svelte/elements\";\n\n\tinterface FileProps extends HTMLButtonAttributes {\n\t\tvalue: string;\n\t\tisSelectable?: boolean;\n\t\tisSelect?: boolean;\n\t\tfileIcon?: Snippet;\n\t\tchildren: Snippet;\n\t\tclass?: string;\n\t}\n\n\tlet {\n\t\tvalue,\n\t\tisSelectable = true,\n\t\tisSelect,\n\t\tfileIcon,\n\t\tchildren,\n\t\tclass: className,\n\t\t...props\n\t}: FileProps = $props();\n\n\tconst context = getContext<{\n\t\tselectedId: () => string | undefined;\n\t\texpandedItems: () => string[];\n\t\thandleExpand: (id: string) => void;\n\t\tselectItem: (id: string) => void;\n\t\tsetExpandedItems: (items: string[]) => void;\n\t\tindicator: boolean;\n\t\topenIcon?: Snippet;\n\t\tcloseIcon?: Snippet;\n\t\tdirection: \"rtl\" | \"ltr\";\n\t}>(\"tree\");\n\n\tconst isSelected = $derived(isSelect ?? context.selectedId() === value);\n\n\t// Default file icon (SVG)\n\tconst defaultFileIcon = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z\"/><path d=\"M14 2v4a2 2 0 0 0 2 2h4\"/></svg>`;\n</script>\n\n<button\n\ttype=\"button\"\n\tdisabled={!isSelectable}\n\tclass={cn(\n\t\t\"flex w-fit items-center gap-1 rounded-md pr-1 text-sm duration-200 ease-in-out rtl:pr-0 rtl:pl-1\",\n\t\t{\n\t\t\t\"bg-muted\": isSelected && isSelectable,\n\t\t},\n\t\tisSelectable ? \"cursor-pointer\" : \"cursor-not-allowed opacity-50\",\n\t\tcontext.direction === \"rtl\" ? \"rtl\" : \"ltr\",\n\t\tclassName\n\t)}\n\tonclick={() => context.selectItem(value)}\n\t{...props}\n>\n\t<span class=\"size-4\">\n\t\t{#if fileIcon}\n\t\t\t{@render fileIcon()}\n\t\t{:else}\n\t\t\t{@html defaultFileIcon}\n\t\t{/if}\n\t</span>\n\t{@render children()}\n</button>\n",
			"type": "registry:component",
			"target": "magic/file-tree/file.svelte"
		},
		{
			"content": "<script lang=\"ts\">\n\timport { getContext, onMount } from \"svelte\";\n\timport { cn } from \"$UTILS$\";\n\timport type { Snippet } from \"svelte\";\n\timport type { HTMLButtonAttributes } from \"svelte/elements\";\n\timport type { TreeViewElement } from \"./file-tree.svelte\";\n\n\tinterface CollapseButtonProps extends HTMLButtonAttributes {\n\t\t/**\n\t\t * Tree elements to expand/collapse\n\t\t */\n\t\telements: TreeViewElement[];\n\t\t/**\n\t\t * Whether to expand all on mount\n\t\t */\n\t\texpandAll?: boolean;\n\t\t/**\n\t\t * Children content\n\t\t */\n\t\tchildren: Snippet;\n\t\t/**\n\t\t * Additional CSS classes\n\t\t */\n\t\tclass?: string;\n\t}\n\n\tlet {\n\t\telements,\n\t\texpandAll = false,\n\t\tchildren,\n\t\tclass: className,\n\t\t...props\n\t}: CollapseButtonProps = $props();\n\n\tconst context = getContext<{\n\t\tselectedId: () => string | undefined;\n\t\texpandedItems: () => string[];\n\t\thandleExpand: (id: string) => void;\n\t\tselectItem: (id: string) => void;\n\t\tsetExpandedItems: (items: string[]) => void;\n\t\tindicator: boolean;\n\t\topenIcon?: Snippet;\n\t\tcloseIcon?: Snippet;\n\t\tdirection: \"rtl\" | \"ltr\";\n\t}>(\"tree\");\n\n\tconst expendAllTree = (elements: TreeViewElement[]) => {\n\t\tconst expandTree = (element: TreeViewElement) => {\n\t\t\tconst isSelectable = element.isSelectable ?? true;\n\t\t\tif (isSelectable && element.children && element.children.length > 0) {\n\t\t\t\tconst currentExpanded = context.expandedItems();\n\t\t\t\tif (!currentExpanded.includes(element.id)) {\n\t\t\t\t\tcontext.setExpandedItems([...currentExpanded, element.id]);\n\t\t\t\t}\n\t\t\t\telement.children.forEach(expandTree);\n\t\t\t}\n\t\t};\n\n\t\telements.forEach(expandTree);\n\t};\n\n\tconst closeAll = () => {\n\t\tcontext.setExpandedItems([]);\n\t};\n\n\tonMount(() => {\n\t\tif (expandAll) {\n\t\t\texpendAllTree(elements);\n\t\t}\n\t});\n\n\tconst handleClick = () => {\n\t\tconst currentExpanded = context.expandedItems();\n\t\tif (currentExpanded && currentExpanded.length > 0) {\n\t\t\tcloseAll();\n\t\t} else {\n\t\t\texpendAllTree(elements);\n\t\t}\n\t};\n</script>\n\n<button\n\ttype=\"button\"\n\tclass={cn(\"hover:bg-muted absolute right-2 bottom-1 h-8 w-fit rounded-md p-1\", className)}\n\tonclick={handleClick}\n\t{...props}\n>\n\t{@render children()}\n\t<span class=\"sr-only\">Toggle</span>\n</button>\n",
			"type": "registry:component",
			"target": "magic/file-tree/collapse-button.svelte"
		},
		{
			"content": "import Tree from \"./file-tree.svelte\";\nimport Folder from \"./folder.svelte\";\nimport File from \"./file.svelte\";\nimport CollapseButton from \"./collapse-button.svelte\";\nimport type { TreeViewElement } from \"./file-tree.svelte\";\n\nexport { Tree, Folder, File, CollapseButton };\nexport type { TreeViewElement };\n",
			"type": "registry:file",
			"target": "magic/file-tree/index.ts"
		}
	]
}